diff --git a/.gitignore b/.gitignore
index 34b68ca..2b77181 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,7 +22,7 @@ __pycache__/
# Settings files with secrets
settings.ini
vfp/settings.ini
-vfp/output/
+output/
vfp/*.json
*.~pck
.claude/HANDOFF.md
diff --git a/api/.env.example b/api/.env.example
index 0f915aa..eedbf8d 100644
--- a/api/.env.example
+++ b/api/.env.example
@@ -35,14 +35,16 @@ APP_PORT=5003
LOG_LEVEL=INFO
# =============================================================================
-# CALE FISIERE (relative la project root - directorul gomag/)
+# CALE FISIERE
+# Relative: JSON_OUTPUT_DIR la project root, SQLITE_DB_PATH la api/
+# Se pot folosi si cai absolute
# =============================================================================
-# JSON-uri descarcate de VFP
-JSON_OUTPUT_DIR=vfp/output
+# JSON-uri comenzi GoMag
+JSON_OUTPUT_DIR=output
# SQLite tracking DB
-SQLITE_DB_PATH=api/data/import.db
+SQLITE_DB_PATH=data/import.db
# =============================================================================
# ROA - Setari import comenzi (din vfp/settings.ini sectiunea [ROA])
diff --git a/api/app/config.py b/api/app/config.py
index 4ace5c4..58edc8e 100644
--- a/api/app/config.py
+++ b/api/app/config.py
@@ -1,9 +1,12 @@
from pydantic_settings import BaseSettings
+from pydantic import model_validator
from pathlib import Path
import os
-# Resolve .env relative to this file (api/app/config.py → api/.env)
-_env_path = Path(__file__).resolve().parent.parent / ".env"
+# Anchored paths - independent of CWD
+_api_root = Path(__file__).resolve().parent.parent # .../gomag/api/
+_project_root = _api_root.parent # .../gomag/
+_env_path = _api_root / ".env"
class Settings(BaseSettings):
# Oracle
@@ -15,12 +18,12 @@ class Settings(BaseSettings):
TNS_ADMIN: str = ""
# SQLite
- SQLITE_DB_PATH: str = str(Path(__file__).parent.parent / "data" / "import.db")
+ SQLITE_DB_PATH: str = "data/import.db"
# App
APP_PORT: int = 5003
LOG_LEVEL: str = "INFO"
- JSON_OUTPUT_DIR: str = ""
+ JSON_OUTPUT_DIR: str = "output"
# SMTP (optional)
SMTP_HOST: str = ""
@@ -38,6 +41,17 @@ class Settings(BaseSettings):
ID_GESTIUNE: int = 0
ID_SECTIE: int = 0
+ @model_validator(mode="after")
+ def resolve_paths(self):
+ """Resolve relative paths against known roots, independent of CWD."""
+ # SQLITE_DB_PATH: relative to api/ root
+ if self.SQLITE_DB_PATH and not os.path.isabs(self.SQLITE_DB_PATH):
+ self.SQLITE_DB_PATH = str(_api_root / self.SQLITE_DB_PATH)
+ # JSON_OUTPUT_DIR: relative to project root
+ if self.JSON_OUTPUT_DIR and not os.path.isabs(self.JSON_OUTPUT_DIR):
+ self.JSON_OUTPUT_DIR = str(_project_root / self.JSON_OUTPUT_DIR)
+ return self
+
model_config = {"env_file": str(_env_path), "env_file_encoding": "utf-8", "extra": "ignore"}
settings = Settings()
diff --git a/api/app/database.py b/api/app/database.py
index e7f9aeb..104efe8 100644
--- a/api/app/database.py
+++ b/api/app/database.py
@@ -73,7 +73,9 @@ CREATE TABLE IF NOT EXISTS sync_runs (
skipped INTEGER DEFAULT 0,
errors INTEGER DEFAULT 0,
json_files INTEGER DEFAULT 0,
- error_message TEXT
+ error_message TEXT,
+ already_imported INTEGER DEFAULT 0,
+ new_imported INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS orders (
@@ -95,7 +97,13 @@ CREATE TABLE IF NOT EXISTS orders (
shipping_name TEXT,
billing_name TEXT,
payment_method TEXT,
- delivery_method TEXT
+ delivery_method TEXT,
+ factura_serie TEXT,
+ factura_numar TEXT,
+ factura_total_fara_tva REAL,
+ factura_total_tva REAL,
+ factura_total_cu_tva REAL,
+ invoice_checked_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
CREATE INDEX IF NOT EXISTS idx_orders_date ON orders(order_date);
@@ -266,14 +274,20 @@ def init_sqlite():
if col not in cols:
conn.execute(f"ALTER TABLE missing_skus ADD COLUMN {col} {typedef}")
logger.info(f"Migrated missing_skus: added column {col}")
- # Migrate sync_runs: add error_message column
+ # Migrate sync_runs: add columns
cursor = conn.execute("PRAGMA table_info(sync_runs)")
sync_cols = {row[1] for row in cursor.fetchall()}
if "error_message" not in sync_cols:
conn.execute("ALTER TABLE sync_runs ADD COLUMN error_message TEXT")
logger.info("Migrated sync_runs: added column error_message")
+ if "already_imported" not in sync_cols:
+ conn.execute("ALTER TABLE sync_runs ADD COLUMN already_imported INTEGER DEFAULT 0")
+ logger.info("Migrated sync_runs: added column already_imported")
+ if "new_imported" not in sync_cols:
+ conn.execute("ALTER TABLE sync_runs ADD COLUMN new_imported INTEGER DEFAULT 0")
+ logger.info("Migrated sync_runs: added column new_imported")
- # Migrate orders: add shipping/billing/payment/delivery columns
+ # Migrate orders: add shipping/billing/payment/delivery + invoice columns
cursor = conn.execute("PRAGMA table_info(orders)")
order_cols = {row[1] for row in cursor.fetchall()}
for col, typedef in [
@@ -281,6 +295,12 @@ def init_sqlite():
("billing_name", "TEXT"),
("payment_method", "TEXT"),
("delivery_method", "TEXT"),
+ ("factura_serie", "TEXT"),
+ ("factura_numar", "TEXT"),
+ ("factura_total_fara_tva", "REAL"),
+ ("factura_total_tva", "REAL"),
+ ("factura_total_cu_tva", "REAL"),
+ ("invoice_checked_at", "TEXT"),
]:
if col not in order_cols:
conn.execute(f"ALTER TABLE orders ADD COLUMN {col} {typedef}")
diff --git a/api/app/routers/sync.py b/api/app/routers/sync.py
index 8fdddca..49dff7e 100644
--- a/api/app/routers/sync.py
+++ b/api/app/routers/sync.py
@@ -84,6 +84,8 @@ async def sync_status():
"imported": row_dict.get("imported", 0),
"skipped": row_dict.get("skipped", 0),
"errors": row_dict.get("errors", 0),
+ "already_imported": row_dict.get("already_imported", 0),
+ "new_imported": row_dict.get("new_imported", 0),
}
finally:
await db.close()
@@ -147,6 +149,8 @@ async def sync_run_log(run_id: str):
"id_partener": o.get("id_partener"),
"error_message": o.get("error_message"),
"missing_skus": o.get("missing_skus"),
+ "factura_numar": o.get("factura_numar"),
+ "factura_serie": o.get("factura_serie"),
}
for o in orders
]
@@ -179,6 +183,9 @@ def _format_text_log_from_detail(detail: dict) -> str:
if status == "IMPORTED":
id_cmd = o.get("id_comanda", "?")
lines.append(f"#{number} [{order_date}] {customer} → IMPORTAT (ID: {id_cmd})")
+ elif status == "ALREADY_IMPORTED":
+ id_cmd = o.get("id_comanda", "?")
+ lines.append(f"#{number} [{order_date}] {customer} → DEJA IMPORTAT (ID: {id_cmd})")
elif status == "SKIPPED":
missing = o.get("missing_skus", "")
if isinstance(missing, str):
@@ -210,7 +217,12 @@ def _format_text_log_from_detail(detail: dict) -> str:
except (ValueError, TypeError):
pass
- lines.append(f"Finalizat: {imported} importate, {skipped} nemapate, {errors} erori din {total} comenzi{duration_str}")
+ already = run.get("already_imported", 0)
+ new_imp = run.get("new_imported", 0)
+ if already:
+ lines.append(f"Finalizat: {new_imp} importate, {already} deja importate, {skipped} nemapate, {errors} erori din {total} comenzi{duration_str}")
+ else:
+ lines.append(f"Finalizat: {imported} importate, {skipped} nemapate, {errors} erori din {total} comenzi{duration_str}")
return "\n".join(lines)
@@ -240,7 +252,7 @@ async def sync_run_text_log(run_id: str):
@router.get("/api/sync/run/{run_id}/orders")
async def sync_run_orders(run_id: str, status: str = "all", page: int = 1, per_page: int = 50,
- sort_by: str = "created_at", sort_dir: str = "asc"):
+ sort_by: str = "order_date", sort_dir: str = "desc"):
"""Get filtered, paginated orders for a sync run (R1)."""
return await sqlite_service.get_run_orders_filtered(run_id, status, page, per_page,
sort_by=sort_by, sort_dir=sort_dir)
@@ -327,23 +339,37 @@ async def dashboard_orders(page: int = 1, per_page: int = 50,
period_end=period_end if period_days == 0 else "",
)
- # Enrich imported orders with invoice data from Oracle
+ # Enrich orders with invoice data — prefer SQLite cache, fallback to Oracle
all_orders = result["orders"]
- imported_orders = [o for o in all_orders if o.get("id_comanda")]
- invoice_data = {}
- if imported_orders:
- id_comanda_list = [o["id_comanda"] for o in imported_orders]
- invoice_data = await asyncio.to_thread(
- invoice_service.check_invoices_for_orders, id_comanda_list
- )
-
for o in all_orders:
- idc = o.get("id_comanda")
- if idc and idc in invoice_data:
- o["invoice"] = invoice_data[idc]
+ if o.get("factura_numar"):
+ # Use cached invoice data from SQLite
+ o["invoice"] = {
+ "facturat": True,
+ "serie_act": o.get("factura_serie"),
+ "numar_act": o.get("factura_numar"),
+ "total_fara_tva": o.get("factura_total_fara_tva"),
+ "total_tva": o.get("factura_total_tva"),
+ "total_cu_tva": o.get("factura_total_cu_tva"),
+ }
else:
o["invoice"] = None
+ # For orders without cached invoice, check Oracle (only uncached imported orders)
+ uncached_orders = [o for o in all_orders if o.get("id_comanda") and not o.get("invoice")]
+ if uncached_orders:
+ try:
+ id_comanda_list = [o["id_comanda"] for o in uncached_orders]
+ invoice_data = await asyncio.to_thread(
+ invoice_service.check_invoices_for_orders, id_comanda_list
+ )
+ for o in uncached_orders:
+ idc = o.get("id_comanda")
+ if idc and idc in invoice_data:
+ o["invoice"] = invoice_data[idc]
+ except Exception:
+ pass
+
# Add shipping/billing name fields + is_different_person flag
s_name = o.get("shipping_name") or ""
b_name = o.get("billing_name") or ""
diff --git a/api/app/services/sync_service.py b/api/app/services/sync_service.py
index 72abc81..5bc52d7 100644
--- a/api/app/services/sync_service.py
+++ b/api/app/services/sync_service.py
@@ -2,10 +2,11 @@ import asyncio
import json
import logging
import uuid
-from datetime import datetime
+from datetime import datetime, timedelta
-from . import order_reader, validation_service, import_service, sqlite_service
+from . import order_reader, validation_service, import_service, sqlite_service, invoice_service
from ..config import settings
+from .. import database
logger = logging.getLogger(__name__)
@@ -43,7 +44,7 @@ def _update_progress(phase: str, phase_text: str, current: int = 0, total: int =
_current_sync["phase_text"] = phase_text
_current_sync["progress_current"] = current
_current_sync["progress_total"] = total
- _current_sync["counts"] = counts or {"imported": 0, "skipped": 0, "errors": 0}
+ _current_sync["counts"] = counts or {"imported": 0, "skipped": 0, "errors": 0, "already_imported": 0}
async def get_sync_status():
@@ -71,11 +72,25 @@ async def prepare_sync(id_pol: int = None, id_sectie: int = None) -> dict:
"phase_text": "Starting...",
"progress_current": 0,
"progress_total": 0,
- "counts": {"imported": 0, "skipped": 0, "errors": 0},
+ "counts": {"imported": 0, "skipped": 0, "errors": 0, "already_imported": 0},
}
return {"run_id": run_id, "status": "starting"}
+def _derive_customer_info(order):
+ """Extract shipping/billing names and customer from an order."""
+ shipping_name = ""
+ if order.shipping:
+ shipping_name = f"{getattr(order.shipping, 'firstname', '') or ''} {getattr(order.shipping, 'lastname', '') or ''}".strip()
+ billing_name = f"{getattr(order.billing, 'firstname', '') or ''} {getattr(order.billing, 'lastname', '') or ''}".strip()
+ if not shipping_name:
+ shipping_name = billing_name
+ customer = shipping_name or order.billing.company_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
+
+
async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None) -> dict:
"""Run a full sync cycle. Returns summary dict."""
global _current_sync
@@ -96,7 +111,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
"phase_text": "Reading JSON files...",
"progress_current": 0,
"progress_total": 0,
- "counts": {"imported": 0, "skipped": 0, "errors": 0},
+ "counts": {"imported": 0, "skipped": 0, "errors": 0, "already_imported": 0},
}
_update_progress("reading", "Reading JSON files...")
@@ -118,10 +133,13 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
_log_line(run_id, f"Gasite {len(orders)} comenzi in {json_count} fisiere")
# Populate web_products catalog from all orders (R4)
- for order in orders:
- for item in order.items:
- if item.sku and item.name:
- await sqlite_service.upsert_web_product(item.sku, item.name)
+ web_product_items = [
+ (item.sku, item.name)
+ for order in orders
+ for item in order.items
+ if item.sku and item.name
+ ]
+ await sqlite_service.upsert_web_products_batch(web_product_items)
if not orders:
_log_line(run_id, "Nicio comanda gasita.")
@@ -132,150 +150,176 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
_update_progress("validation", f"Validating {len(orders)} orders...", 0, len(orders))
- # Step 2a: Find new orders (not yet in Oracle)
- all_order_numbers = [o.number for o in orders]
- new_orders = await asyncio.to_thread(
- validation_service.find_new_orders, all_order_numbers
- )
+ # ── Single Oracle connection for entire validation phase ──
+ conn = await asyncio.to_thread(database.get_oracle_connection)
+ try:
+ # Step 2a: Find orders already in Oracle (date-range query)
+ order_dates = [o.date for o in orders if o.date]
+ if order_dates:
+ min_date_str = min(order_dates)
+ try:
+ min_date = datetime.strptime(min_date_str[:10], "%Y-%m-%d") - timedelta(days=1)
+ except (ValueError, TypeError):
+ min_date = datetime.now() - timedelta(days=90)
+ else:
+ min_date = datetime.now() - timedelta(days=90)
- # Step 2b: Validate SKUs (blocking Oracle call -> run in thread)
- all_skus = order_reader.get_all_skus(orders)
- validation = await asyncio.to_thread(validation_service.validate_skus, all_skus)
- importable, skipped = validation_service.classify_orders(orders, validation)
-
- _update_progress("validation", f"{len(importable)} importable, {len(skipped)} skipped (missing SKUs)",
- 0, len(importable))
- _log_line(run_id, f"Validare SKU-uri: {len(importable)} importabile, {len(skipped)} nemapate")
-
- # Step 2c: Build SKU context from skipped orders
- sku_context = {} # {sku: {"orders": [], "customers": []}}
- for order, missing_skus_list in skipped:
- customer = order.billing.company_name or \
- f"{order.billing.firstname} {order.billing.lastname}"
- for sku in missing_skus_list:
- if sku not in sku_context:
- sku_context[sku] = {"orders": [], "customers": []}
- if order.number not in sku_context[sku]["orders"]:
- sku_context[sku]["orders"].append(order.number)
- if customer not in sku_context[sku]["customers"]:
- sku_context[sku]["customers"].append(customer)
-
- # Track missing SKUs with context
- for sku in validation["missing"]:
- product_name = ""
- for order in orders:
- for item in order.items:
- if item.sku == sku:
- product_name = item.name
- break
- if product_name:
- break
- ctx = sku_context.get(sku, {})
- await sqlite_service.track_missing_sku(
- sku, product_name,
- order_count=len(ctx.get("orders", [])),
- order_numbers=json.dumps(ctx.get("orders", [])) if ctx.get("orders") else None,
- customers=json.dumps(ctx.get("customers", [])) if ctx.get("customers") else None,
+ existing_map = await asyncio.to_thread(
+ validation_service.check_orders_in_roa, min_date, conn
)
- # Step 2d: Pre-validate prices for importable articles
- id_pol = id_pol or settings.ID_POL
- id_sectie = id_sectie or settings.ID_SECTIE
- logger.info(f"Sync params: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
- _log_line(run_id, f"Parametri import: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
- if id_pol and importable:
- _update_progress("validation", "Validating prices...", 0, len(importable))
- _log_line(run_id, "Validare preturi...")
- # Gather all CODMATs from importable orders
- all_codmats = set()
+ # Step 2b: Validate SKUs (reuse same connection)
+ all_skus = order_reader.get_all_skus(orders)
+ validation = await asyncio.to_thread(validation_service.validate_skus, all_skus, conn)
+ importable, skipped = validation_service.classify_orders(orders, validation)
+
+ # ── Split importable into truly_importable vs already_in_roa ──
+ truly_importable = []
+ already_in_roa = []
for order in importable:
- for item in order.items:
- if item.sku in validation["mapped"]:
- # Mapped SKUs resolve to codmat via ARTICOLE_TERTI (handled by import)
- pass
- elif item.sku in validation["direct"]:
- all_codmats.add(item.sku)
- # For mapped SKUs, we'd need the ARTICOLE_TERTI lookup - direct SKUs = codmat
- if all_codmats:
- price_result = await asyncio.to_thread(
- validation_service.validate_prices, all_codmats, id_pol
+ if order.number in existing_map:
+ already_in_roa.append(order)
+ else:
+ truly_importable.append(order)
+
+ _update_progress("validation",
+ f"{len(truly_importable)} new, {len(already_in_roa)} already imported, {len(skipped)} skipped",
+ 0, len(truly_importable))
+ _log_line(run_id, f"Validare: {len(truly_importable)} noi, {len(already_in_roa)} deja importate, {len(skipped)} nemapate")
+
+ # Step 2c: Build SKU context from skipped orders
+ sku_context = {}
+ for order, missing_skus_list in skipped:
+ customer = order.billing.company_name or \
+ f"{order.billing.firstname} {order.billing.lastname}"
+ for sku in missing_skus_list:
+ if sku not in sku_context:
+ sku_context[sku] = {"orders": [], "customers": []}
+ if order.number not in sku_context[sku]["orders"]:
+ sku_context[sku]["orders"].append(order.number)
+ if customer not in sku_context[sku]["customers"]:
+ sku_context[sku]["customers"].append(customer)
+
+ # Track missing SKUs with context
+ for sku in validation["missing"]:
+ product_name = ""
+ for order in orders:
+ for item in order.items:
+ if item.sku == sku:
+ product_name = item.name
+ break
+ if product_name:
+ break
+ ctx = sku_context.get(sku, {})
+ await sqlite_service.track_missing_sku(
+ sku, product_name,
+ order_count=len(ctx.get("orders", [])),
+ order_numbers=json.dumps(ctx.get("orders", [])) if ctx.get("orders") else None,
+ customers=json.dumps(ctx.get("customers", [])) if ctx.get("customers") else None,
)
- if price_result["missing_price"]:
- logger.info(
- f"Auto-adding price 0 for {len(price_result['missing_price'])} "
- f"direct articles in policy {id_pol}"
- )
- await asyncio.to_thread(
- validation_service.ensure_prices,
- price_result["missing_price"], id_pol
- )
- # Step 3: Record skipped orders + store items
- skipped_count = 0
+ # Step 2d: Pre-validate prices for importable articles
+ id_pol = id_pol or settings.ID_POL
+ id_sectie = id_sectie or settings.ID_SECTIE
+ logger.info(f"Sync params: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
+ _log_line(run_id, f"Parametri import: ID_POL={id_pol}, ID_SECTIE={id_sectie}")
+ if id_pol and (truly_importable or already_in_roa):
+ _update_progress("validation", "Validating prices...", 0, len(truly_importable))
+ _log_line(run_id, "Validare preturi...")
+ all_codmats = set()
+ for order in (truly_importable + already_in_roa):
+ for item in order.items:
+ if item.sku in validation["mapped"]:
+ pass
+ elif item.sku in validation["direct"]:
+ all_codmats.add(item.sku)
+ if all_codmats:
+ price_result = await asyncio.to_thread(
+ validation_service.validate_prices, all_codmats, id_pol,
+ conn, validation.get("direct_id_map")
+ )
+ if price_result["missing_price"]:
+ logger.info(
+ f"Auto-adding price 0 for {len(price_result['missing_price'])} "
+ f"direct articles in policy {id_pol}"
+ )
+ await asyncio.to_thread(
+ validation_service.ensure_prices,
+ price_result["missing_price"], id_pol,
+ conn, validation.get("direct_id_map")
+ )
+ finally:
+ await asyncio.to_thread(database.pool.release, conn)
+
+ # Step 3a: Record already-imported orders (batch)
+ already_imported_count = len(already_in_roa)
+ already_batch = []
+ for order in already_in_roa:
+ shipping_name, billing_name, customer, payment_method, delivery_method = _derive_customer_info(order)
+ id_comanda_roa = existing_map.get(order.number)
+ order_items_data = [
+ {"sku": item.sku, "product_name": item.name,
+ "quantity": item.quantity, "price": item.price, "vat": item.vat,
+ "mapping_status": "mapped" if item.sku in validation["mapped"] else "direct",
+ "codmat": None, "id_articol": None, "cantitate_roa": None}
+ for item in order.items
+ ]
+ already_batch.append({
+ "sync_run_id": run_id, "order_number": order.number,
+ "order_date": order.date, "customer_name": customer,
+ "status": "ALREADY_IMPORTED", "status_at_run": "ALREADY_IMPORTED",
+ "id_comanda": id_comanda_roa, "id_partener": None,
+ "error_message": None, "missing_skus": None,
+ "items_count": len(order.items),
+ "shipping_name": shipping_name, "billing_name": billing_name,
+ "payment_method": payment_method, "delivery_method": delivery_method,
+ "items": order_items_data,
+ })
+ _log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → DEJA IMPORTAT (ID: {id_comanda_roa})")
+ await sqlite_service.save_orders_batch(already_batch)
+
+ # Step 3b: Record skipped orders + store items (batch)
+ skipped_count = len(skipped)
+ skipped_batch = []
for order, missing_skus in skipped:
- skipped_count += 1
- # Derive shipping / billing names
- shipping_name = ""
- if order.shipping:
- shipping_name = f"{getattr(order.shipping, 'firstname', '') or ''} {getattr(order.shipping, 'lastname', '') or ''}".strip()
- billing_name = f"{getattr(order.billing, 'firstname', '') or ''} {getattr(order.billing, 'lastname', '') or ''}".strip()
- if not shipping_name:
- shipping_name = billing_name
- customer = shipping_name or order.billing.company_name or billing_name
- payment_method = getattr(order, 'payment_name', None) or None
- delivery_method = getattr(order, 'delivery_name', None) or None
-
- await sqlite_service.upsert_order(
- sync_run_id=run_id,
- order_number=order.number,
- order_date=order.date,
- customer_name=customer,
- status="SKIPPED",
- missing_skus=missing_skus,
- items_count=len(order.items),
- shipping_name=shipping_name,
- billing_name=billing_name,
- payment_method=payment_method,
- delivery_method=delivery_method,
- )
- await sqlite_service.add_sync_run_order(run_id, order.number, "SKIPPED")
- # Store order items with mapping status (R9)
- order_items_data = []
- for item in order.items:
- ms = "missing" if item.sku in validation["missing"] else \
- "mapped" if item.sku in validation["mapped"] else "direct"
- order_items_data.append({
- "sku": item.sku, "product_name": item.name,
- "quantity": item.quantity, "price": item.price, "vat": item.vat,
- "mapping_status": ms, "codmat": None, "id_articol": None,
- "cantitate_roa": None
- })
- await sqlite_service.add_order_items(order.number, order_items_data)
+ shipping_name, billing_name, customer, payment_method, delivery_method = _derive_customer_info(order)
+ order_items_data = [
+ {"sku": item.sku, "product_name": item.name,
+ "quantity": item.quantity, "price": item.price, "vat": item.vat,
+ "mapping_status": "missing" if item.sku in validation["missing"] else
+ "mapped" if item.sku in validation["mapped"] else "direct",
+ "codmat": None, "id_articol": None, "cantitate_roa": None}
+ for item in order.items
+ ]
+ skipped_batch.append({
+ "sync_run_id": run_id, "order_number": order.number,
+ "order_date": order.date, "customer_name": customer,
+ "status": "SKIPPED", "status_at_run": "SKIPPED",
+ "id_comanda": None, "id_partener": None,
+ "error_message": None, "missing_skus": missing_skus,
+ "items_count": len(order.items),
+ "shipping_name": shipping_name, "billing_name": billing_name,
+ "payment_method": payment_method, "delivery_method": delivery_method,
+ "items": order_items_data,
+ })
_log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → OMIS (lipsa: {', '.join(missing_skus)})")
- _update_progress("skipped", f"Skipped {skipped_count}/{len(skipped)}: #{order.number} {customer}",
- 0, len(importable),
- {"imported": 0, "skipped": skipped_count, "errors": 0})
+ await sqlite_service.save_orders_batch(skipped_batch)
+ _update_progress("skipped", f"Skipped {skipped_count}",
+ 0, len(truly_importable),
+ {"imported": 0, "skipped": skipped_count, "errors": 0, "already_imported": already_imported_count})
- # Step 4: Import valid orders
+ # Step 4: Import only truly new orders
imported_count = 0
error_count = 0
- for i, order in enumerate(importable):
- # Derive shipping / billing names
- shipping_name = ""
- if order.shipping:
- shipping_name = f"{getattr(order.shipping, 'firstname', '') or ''} {getattr(order.shipping, 'lastname', '') or ''}".strip()
- billing_name = f"{getattr(order.billing, 'firstname', '') or ''} {getattr(order.billing, 'lastname', '') or ''}".strip()
- if not shipping_name:
- shipping_name = billing_name
- customer = shipping_name or order.billing.company_name or billing_name
- payment_method = getattr(order, 'payment_name', None) or None
- delivery_method = getattr(order, 'delivery_name', None) or None
+ for i, order in enumerate(truly_importable):
+ shipping_name, billing_name, customer, payment_method, delivery_method = _derive_customer_info(order)
_update_progress("import",
- f"Import {i+1}/{len(importable)}: #{order.number} {customer}",
- i + 1, len(importable),
- {"imported": imported_count, "skipped": len(skipped), "errors": error_count})
+ f"Import {i+1}/{len(truly_importable)}: #{order.number} {customer}",
+ i + 1, len(truly_importable),
+ {"imported": imported_count, "skipped": len(skipped), "errors": error_count,
+ "already_imported": already_imported_count})
result = await asyncio.to_thread(
import_service.import_single_order,
@@ -343,10 +387,41 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
logger.warning("Too many errors, stopping sync")
break
+ # Step 4b: Invoice check — update cached invoice data
+ _update_progress("invoices", "Checking invoices...", 0, 0)
+ invoices_updated = 0
+ try:
+ uninvoiced = await sqlite_service.get_uninvoiced_imported_orders()
+ if uninvoiced:
+ id_comanda_list = [o["id_comanda"] for o in uninvoiced]
+ invoice_data = await asyncio.to_thread(
+ invoice_service.check_invoices_for_orders, id_comanda_list
+ )
+ # Build reverse map: id_comanda → order_number
+ id_to_order = {o["id_comanda"]: o["order_number"] for o in uninvoiced}
+ for idc, inv in invoice_data.items():
+ order_num = id_to_order.get(idc)
+ if order_num and inv.get("facturat"):
+ await sqlite_service.update_order_invoice(
+ order_num,
+ serie=inv.get("serie_act"),
+ numar=str(inv.get("numar_act", "")),
+ total_fara_tva=inv.get("total_fara_tva"),
+ total_tva=inv.get("total_tva"),
+ total_cu_tva=inv.get("total_cu_tva"),
+ )
+ invoices_updated += 1
+ if invoices_updated:
+ _log_line(run_id, f"Facturi actualizate: {invoices_updated} comenzi facturate")
+ except Exception as e:
+ logger.warning(f"Invoice check failed: {e}")
+
# Step 5: Update sync run
+ total_imported = imported_count + already_imported_count # backward-compat
status = "completed" if error_count <= 10 else "failed"
await sqlite_service.update_sync_run(
- run_id, status, len(orders), imported_count, len(skipped), error_count
+ run_id, status, len(orders), total_imported, len(skipped), error_count,
+ already_imported=already_imported_count, new_imported=imported_count
)
summary = {
@@ -354,29 +429,36 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
"status": status,
"json_files": json_count,
"total_orders": len(orders),
- "new_orders": len(new_orders),
- "imported": imported_count,
+ "new_orders": len(truly_importable),
+ "imported": total_imported,
+ "new_imported": imported_count,
+ "already_imported": already_imported_count,
"skipped": len(skipped),
"errors": error_count,
- "missing_skus": len(validation["missing"])
+ "missing_skus": len(validation["missing"]),
+ "invoices_updated": invoices_updated,
}
_update_progress("completed",
- f"Completed: {imported_count} imported, {len(skipped)} skipped, {error_count} errors",
- len(importable), len(importable),
- {"imported": imported_count, "skipped": len(skipped), "errors": error_count})
+ f"Completed: {imported_count} new, {already_imported_count} already, {len(skipped)} skipped, {error_count} errors",
+ len(truly_importable), len(truly_importable),
+ {"imported": imported_count, "skipped": len(skipped), "errors": error_count,
+ "already_imported": already_imported_count})
if _current_sync:
_current_sync["status"] = status
_current_sync["finished_at"] = datetime.now().isoformat()
logger.info(
- f"Sync {run_id} completed: {imported_count} imported, "
+ f"Sync {run_id} completed: {imported_count} new, {already_imported_count} already imported, "
f"{len(skipped)} skipped, {error_count} errors"
)
duration = (datetime.now() - started_dt).total_seconds()
_log_line(run_id, "")
- _run_logs[run_id].append(f"Finalizat: {imported_count} importate, {len(skipped)} nemapate, {error_count} erori din {len(orders)} comenzi | Durata: {int(duration)}s")
+ _run_logs[run_id].append(
+ f"Finalizat: {imported_count} importate, {already_imported_count} deja importate, "
+ f"{len(skipped)} nemapate, {error_count} erori din {len(orders)} comenzi | Durata: {int(duration)}s"
+ )
return summary
@@ -405,6 +487,4 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None
def stop_sync():
"""Signal sync to stop. Currently sync runs to completion."""
- # For now, sync runs are not cancellable mid-flight.
- # Future: use an asyncio.Event for cooperative cancellation.
pass
diff --git a/api/app/services/validation_service.py b/api/app/services/validation_service.py
index e014e0b..8c2f866 100644
--- a/api/app/services/validation_service.py
+++ b/api/app/services/validation_service.py
@@ -1,23 +1,53 @@
import logging
+from datetime import datetime, timedelta
from .. import database
logger = logging.getLogger(__name__)
-def validate_skus(skus: set[str]) -> dict:
+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 validate_skus(skus: set[str], conn=None) -> dict:
"""Validate a set of SKUs against Oracle.
- Returns: {mapped: set, direct: set, missing: set}
+ 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()}
+ return {"mapped": set(), "direct": set(), "missing": set(), "direct_id_map": {}}
mapped = set()
direct = set()
+ direct_id_map = {}
sku_list = list(skus)
- conn = database.get_oracle_connection()
+ own_conn = conn is None
+ if own_conn:
+ conn = database.get_oracle_connection()
try:
with conn.cursor() as cur:
# Check in batches of 500
@@ -34,24 +64,26 @@ def validate_skus(skus: set[str]) -> dict:
for row in cur:
mapped.add(row[0])
- # Check NOM_ARTICOLE for remaining
+ # Check NOM_ARTICOLE for remaining — also fetch id_articol
remaining = [s for s in batch if s not in mapped]
if remaining:
placeholders2 = ",".join([f":n{j}" for j in range(len(remaining))])
params2 = {f"n{j}": sku for j, sku in enumerate(remaining)}
cur.execute(f"""
- SELECT DISTINCT codmat FROM NOM_ARTICOLE
+ SELECT codmat, id_articol FROM NOM_ARTICOLE
WHERE codmat IN ({placeholders2}) AND sters = 0 AND inactiv = 0
""", params2)
for row in cur:
direct.add(row[0])
+ direct_id_map[row[0]] = row[1]
finally:
- database.pool.release(conn)
+ 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}
+ 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.
@@ -73,39 +105,9 @@ def classify_orders(orders, validation_result):
return importable, skipped
-def find_new_orders(order_numbers: list[str]) -> set[str]:
- """Check which order numbers do NOT already exist in Oracle COMENZI.
- Returns: set of order numbers that are truly new (not yet imported).
- """
- if not order_numbers:
- return set()
-
- existing = set()
- num_list = list(order_numbers)
-
- conn = database.get_oracle_connection()
- try:
- with conn.cursor() as cur:
- for i in range(0, len(num_list), 500):
- batch = num_list[i:i+500]
- placeholders = ",".join([f":o{j}" for j in range(len(batch))])
- params = {f"o{j}": num for j, num in enumerate(batch)}
-
- cur.execute(f"""
- SELECT DISTINCT comanda_externa FROM COMENZI
- WHERE comanda_externa IN ({placeholders}) AND sters = 0
- """, params)
- for row in cur:
- existing.add(row[0])
- finally:
- database.pool.release(conn)
-
- new_orders = set(order_numbers) - existing
- logger.info(f"Order check: {len(new_orders)} new, {len(existing)} already exist out of {len(order_numbers)} total")
- return new_orders
-
-def validate_prices(codmats: set[str], id_pol: int) -> dict:
+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:
@@ -115,21 +117,31 @@ def validate_prices(codmats: set[str], id_pol: int) -> dict:
ids_with_price = set()
codmat_list = list(codmats)
- conn = database.get_oracle_connection()
+ # Pre-populate from direct_id_map if available
+ if direct_id_map:
+ for cm in codmat_list:
+ if cm in direct_id_map:
+ codmat_to_id[cm] = direct_id_map[cm]
+
+ own_conn = conn is None
+ if own_conn:
+ conn = database.get_oracle_connection()
try:
with conn.cursor() as cur:
- # Step 1: Get ID_ARTICOL for each CODMAT
- 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)}
+ # Step 1: Get ID_ARTICOL for CODMATs not already in direct_id_map
+ remaining = [cm for cm in codmat_list if cm not in codmat_to_id]
+ if remaining:
+ for i in range(0, len(remaining), 500):
+ batch = remaining[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)}
- cur.execute(f"""
- SELECT id_articol, codmat FROM NOM_ARTICOLE
- WHERE codmat IN ({placeholders})
- """, params)
- for row in cur:
- codmat_to_id[row[1]] = row[0]
+ cur.execute(f"""
+ SELECT id_articol, codmat FROM NOM_ARTICOLE
+ WHERE codmat IN ({placeholders})
+ """, params)
+ for row in cur:
+ codmat_to_id[row[1]] = row[0]
# Step 2: Check which ID_ARTICOLs have a price in the policy
id_list = list(codmat_to_id.values())
@@ -146,7 +158,8 @@ def validate_prices(codmats: set[str], id_pol: int) -> dict:
for row in cur:
ids_with_price.add(row[0])
finally:
- database.pool.release(conn)
+ 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}
@@ -155,12 +168,17 @@ def validate_prices(codmats: set[str], id_pol: int) -> dict:
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):
- """Insert price 0 entries for CODMATs missing from the given price policy."""
+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
- conn = database.get_oracle_connection()
+ 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
@@ -173,31 +191,53 @@ def ensure_prices(codmats: set[str], id_pol: int):
return
id_valuta = row[0]
+ # Build batch params using direct_id_map where available
+ batch_params = []
+ need_lookup = []
+ codmat_id_map = dict(direct_id_map) if direct_id_map else {}
+
for codmat in codmats:
- # Get ID_ARTICOL
- cur.execute("""
- SELECT id_articol FROM NOM_ARTICOLE WHERE codmat = :codmat
- """, {"codmat": codmat})
- row = cur.fetchone()
- if not row:
+ if codmat not in codmat_id_map:
+ need_lookup.append(codmat)
+
+ # Batch lookup remaining CODMATs
+ if need_lookup:
+ for i in range(0, len(need_lookup), 500):
+ batch = need_lookup[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)}
+ cur.execute(f"""
+ SELECT codmat, id_articol FROM NOM_ARTICOLE
+ WHERE codmat IN ({placeholders}) AND sters = 0 AND inactiv = 0
+ """, params)
+ for r in cur:
+ codmat_id_map[r[0]] = r[1]
+
+ 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
- id_articol = row[0]
+ batch_params.append({
+ "id_pol": id_pol,
+ "id_articol": id_articol,
+ "id_valuta": id_valuta
+ })
- cur.execute("""
+ if batch_params:
+ cur.executemany("""
INSERT INTO CRM_POLITICI_PRET_ART
- (ID_POL_ART, ID_POL, ID_ARTICOL, PRET, ID_VALUTA,
- ID_UTIL, DATAORA, PROC_TVAV,
- PRETFTVA, PRETCTVA)
+ (ID_POL, ID_ARTICOL, PRET, ID_VALUTA,
+ ID_UTIL, DATAORA, PROC_TVAV, PRETFTVA, PRETCTVA)
VALUES
- (SEQ_CRM_POLITICI_PRET_ART.NEXTVAL, :id_pol, :id_articol, 0, :id_valuta,
- -3, SYSDATE, 1.19,
- 0, 0)
- """, {"id_pol": id_pol, "id_articol": id_articol, "id_valuta": id_valuta})
- logger.info(f"Pret 0 adaugat pentru CODMAT {codmat} in politica {id_pol}")
+ (: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:
- database.pool.release(conn)
+ if own_conn:
+ database.pool.release(conn)
logger.info(f"Ensure prices done: {len(codmats)} CODMATs processed for policy {id_pol}")
diff --git a/api/app/static/js/dashboard.js b/api/app/static/js/dashboard.js
index 3ccc599..83e7e07 100644
--- a/api/app/static/js/dashboard.js
+++ b/api/app/static/js/dashboard.js
@@ -12,6 +12,7 @@ let qmAcTimeout = null;
let _pollInterval = null;
let _lastSyncStatus = null;
let _lastRunId = null;
+let _currentRunId = null;
// ── Init ──────────────────────────────────────────
@@ -66,6 +67,13 @@ function updateSyncPanel(data) {
if (txt) txt.textContent = statusLabels[data.status] || data.status || 'Inactiv';
if (startBtn) startBtn.disabled = data.status === 'running';
+ // Track current running sync run_id
+ if (data.status === 'running' && data.run_id) {
+ _currentRunId = data.run_id;
+ } else {
+ _currentRunId = null;
+ }
+
// Live progress area
if (progressArea) {
progressArea.style.display = data.status === 'running' ? 'flex' : 'none';
@@ -84,7 +92,16 @@ function updateSyncPanel(data) {
const st = document.getElementById('lastSyncStatus');
if (d) d.textContent = lr.started_at ? lr.started_at.replace('T', ' ').slice(0, 16) : '\u2014';
if (dur) dur.textContent = lr.duration_seconds ? Math.round(lr.duration_seconds) + 's' : '\u2014';
- if (cnt) cnt.textContent = '\u2191' + (lr.imported || 0) + ' \u2298' + (lr.skipped || 0) + ' \u2715' + (lr.errors || 0);
+ // Updated counts: ↑new =already ⊘skipped ✕errors
+ if (cnt) {
+ const newImp = lr.new_imported || 0;
+ const already = lr.already_imported || 0;
+ if (already > 0) {
+ cnt.textContent = '\u2191' + newImp + ' =' + already + ' \u2298' + (lr.skipped || 0) + ' \u2715' + (lr.errors || 0);
+ } else {
+ cnt.textContent = '\u2191' + (lr.imported || 0) + ' \u2298' + (lr.skipped || 0) + ' \u2715' + (lr.errors || 0);
+ }
+ }
if (st) {
st.textContent = lr.status === 'completed' ? '\u2713' : '\u2715';
st.style.color = lr.status === 'completed' ? '#10b981' : '#ef4444';
@@ -92,14 +109,16 @@ function updateSyncPanel(data) {
}
}
-// Wire last-sync-row click → journal
+// Wire last-sync-row click → journal (use current running sync if active)
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('lastSyncRow')?.addEventListener('click', () => {
- if (_lastRunId) window.location = '/logs?run=' + _lastRunId;
+ const targetId = _currentRunId || _lastRunId;
+ if (targetId) window.location = '/logs?run=' + targetId;
});
document.getElementById('lastSyncRow')?.addEventListener('keydown', (e) => {
- if ((e.key === 'Enter' || e.key === ' ') && _lastRunId) {
- window.location = '/logs?run=' + _lastRunId;
+ const targetId = _currentRunId || _lastRunId;
+ if ((e.key === 'Enter' || e.key === ' ') && targetId) {
+ window.location = '/logs?run=' + targetId;
}
});
});
@@ -391,10 +410,11 @@ function fmtDate(dateStr) {
function orderStatusBadge(status) {
switch ((status || '').toUpperCase()) {
- case 'IMPORTED': return 'Importat';
- case 'SKIPPED': return 'Omis';
- case 'ERROR': return 'Eroare';
- default: return `${esc(status)}`;
+ case 'IMPORTED': return 'Importat';
+ case 'ALREADY_IMPORTED': return 'Deja importat';
+ case 'SKIPPED': return 'Omis';
+ case 'ERROR': return 'Eroare';
+ default: return `${esc(status)}`;
}
}
diff --git a/api/app/static/js/logs.js b/api/app/static/js/logs.js
index 6bdac6a..4c0645b 100644
--- a/api/app/static/js/logs.js
+++ b/api/app/static/js/logs.js
@@ -7,8 +7,8 @@ let currentFilter = 'all';
let ordersPage = 1;
let currentQmSku = '';
let currentQmOrderNumber = '';
-let ordersSortColumn = 'created_at';
-let ordersSortDirection = 'asc';
+let ordersSortColumn = 'order_date';
+let ordersSortDirection = 'desc';
function esc(s) {
if (s == null) return '';
@@ -50,10 +50,11 @@ function runStatusBadge(status) {
function orderStatusBadge(status) {
switch ((status || '').toUpperCase()) {
- case 'IMPORTED': return 'Importat';
- case 'SKIPPED': return 'Omis';
- case 'ERROR': return 'Eroare';
- default: return `${esc(status)}`;
+ case 'IMPORTED': return 'Importat';
+ case 'ALREADY_IMPORTED': return 'Deja importat';
+ case 'SKIPPED': return 'Omis';
+ case 'ERROR': return 'Eroare';
+ default: return `${esc(status)}`;
}
}
@@ -76,10 +77,13 @@ async function loadRuns() {
const started = r.started_at ? new Date(r.started_at).toLocaleString('ro-RO', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}) : '?';
const st = (r.status || '').toUpperCase();
const statusEmoji = st === 'COMPLETED' ? '✓' : st === 'RUNNING' ? '⟳' : '✗';
+ const newImp = r.new_imported || 0;
+ const already = r.already_imported || 0;
const imp = r.imported || 0;
const skip = r.skipped || 0;
const err = r.errors || 0;
- const label = `${started} — ${statusEmoji} ${r.status} (${imp} imp, ${skip} skip, ${err} err)`;
+ const impLabel = already > 0 ? `${newImp} noi, ${already} deja` : `${imp} imp`;
+ const label = `${started} — ${statusEmoji} ${r.status} (${impLabel}, ${skip} skip, ${err} err)`;
const selected = r.run_id === currentRunId ? 'selected' : '';
return ``;
}).join('');
@@ -133,6 +137,7 @@ async function loadRunOrders(runId, statusFilter, page) {
document.querySelectorAll('#orderFilterBtns button').forEach(btn => {
btn.className = btn.className.replace(' btn-primary', ' btn-outline-primary')
.replace(' btn-success', ' btn-outline-success')
+ .replace(' btn-info', ' btn-outline-info')
.replace(' btn-warning', ' btn-outline-warning')
.replace(' btn-danger', ' btn-outline-danger');
});
@@ -147,13 +152,15 @@ async function loadRunOrders(runId, statusFilter, page) {
document.getElementById('countImported').textContent = counts.imported || 0;
document.getElementById('countSkipped').textContent = counts.skipped || 0;
document.getElementById('countError').textContent = counts.error || 0;
+ const alreadyEl = document.getElementById('countAlreadyImported');
+ if (alreadyEl) alreadyEl.textContent = counts.already_imported || 0;
// Highlight active filter
- const filterMap = { 'all': 0, 'IMPORTED': 1, 'SKIPPED': 2, 'ERROR': 3 };
+ const filterMap = { 'all': 0, 'IMPORTED': 1, 'ALREADY_IMPORTED': 2, 'SKIPPED': 3, 'ERROR': 4 };
const btns = document.querySelectorAll('#orderFilterBtns button');
- const idx = filterMap[currentFilter] || 0;
+ const idx = filterMap[currentFilter] ?? 0;
if (btns[idx]) {
- const colorMap = ['primary', 'success', 'warning', 'danger'];
+ const colorMap = ['primary', 'success', 'info', 'warning', 'danger'];
btns[idx].className = btns[idx].className.replace(`btn-outline-${colorMap[idx]}`, `btn-${colorMap[idx]}`);
}
diff --git a/api/app/templates/logs.html b/api/app/templates/logs.html
index 9876bf7..95e02ca 100644
--- a/api/app/templates/logs.html
+++ b/api/app/templates/logs.html
@@ -42,6 +42,9 @@
+
diff --git a/start.sh b/start.sh
index e8767f3..0751d1f 100644
--- a/start.sh
+++ b/start.sh
@@ -19,10 +19,10 @@ if [ api/requirements.txt -nt venv/.deps_installed ] || [ ! -f venv/.deps_instal
fi
# Stop any existing instance on port 5003
-EXISTING_PID=$(lsof -ti tcp:5003 2>/dev/null)
-if [ -n "$EXISTING_PID" ]; then
- echo "Stopping existing process on port 5003 (PID $EXISTING_PID)..."
- kill "$EXISTING_PID"
+EXISTING_PIDS=$(lsof -ti tcp:5003 2>/dev/null)
+if [ -n "$EXISTING_PIDS" ]; then
+ echo "Stopping existing process(es) on port 5003 (PID $EXISTING_PIDS)..."
+ echo "$EXISTING_PIDS" | xargs kill 2>/dev/null
sleep 2
fi
diff --git a/vfp/ApplicationSetup.prg b/vfp/ApplicationSetup.prg
deleted file mode 100644
index d634088..0000000
--- a/vfp/ApplicationSetup.prg
+++ /dev/null
@@ -1,320 +0,0 @@
-*-- ApplicationSetup.prg - Clasa pentru configurarea si setup-ul aplicatiei
-*-- Contine toate functiile pentru settings.ini si configurare
-*-- Autor: Claude AI
-*-- Data: 10 septembrie 2025
-
-DEFINE CLASS ApplicationSetup AS Custom
-
- *-- Proprietati publice
- cAppPath = ""
- cIniFile = ""
- oSettings = NULL
- lInitialized = .F.
-
- *-- Constructor
- PROCEDURE Init
- PARAMETERS tcAppPath
- IF !EMPTY(tcAppPath)
- THIS.cAppPath = ADDBS(tcAppPath)
- ELSE
- THIS.cAppPath = ADDBS(JUSTPATH(SYS(16,0)))
- ENDIF
-
- THIS.cIniFile = THIS.cAppPath + "settings.ini"
- THIS.lInitialized = .F.
- ENDPROC
-
- *-- Functie pentru incarcarea tuturor setarilor din fisierul INI
- PROCEDURE LoadSettings
- PARAMETERS tcIniFile
- LOCAL loSettings
-
- IF EMPTY(tcIniFile)
- tcIniFile = THIS.cIniFile
- ENDIF
-
- *-- Cream un obiect pentru toate setarile
- loSettings = CREATEOBJECT("Empty")
-
- *-- Sectiunea API
- ADDPROPERTY(loSettings, "ApiBaseUrl", ReadPini("API", "ApiBaseUrl", tcIniFile))
- ADDPROPERTY(loSettings, "OrderApiUrl", ReadPini("API", "OrderApiUrl", tcIniFile))
- ADDPROPERTY(loSettings, "ApiKey", ReadPini("API", "ApiKey", tcIniFile))
- ADDPROPERTY(loSettings, "ApiShop", ReadPini("API", "ApiShop", tcIniFile))
- ADDPROPERTY(loSettings, "UserAgent", ReadPini("API", "UserAgent", tcIniFile))
- ADDPROPERTY(loSettings, "ContentType", ReadPini("API", "ContentType", tcIniFile))
-
- *-- Sectiunea PAGINATION
- ADDPROPERTY(loSettings, "Limit", VAL(ReadPini("PAGINATION", "Limit", tcIniFile)))
-
- *-- Sectiunea OPTIONS
- ADDPROPERTY(loSettings, "GetProducts", ReadPini("OPTIONS", "GetProducts", tcIniFile) = "1")
- ADDPROPERTY(loSettings, "GetOrders", ReadPini("OPTIONS", "GetOrders", tcIniFile) = "1")
-
- *-- Sectiunea FILTERS
- ADDPROPERTY(loSettings, "OrderDaysBack", VAL(ReadPini("FILTERS", "OrderDaysBack", tcIniFile)))
-
- *-- Sectiunea ORACLE - pentru conexiunea la database
- ADDPROPERTY(loSettings, "OracleUser", ReadPini("ORACLE", "OracleUser", tcIniFile))
- ADDPROPERTY(loSettings, "OraclePassword", ReadPini("ORACLE", "OraclePassword", tcIniFile))
- ADDPROPERTY(loSettings, "OracleDSN", ReadPini("ORACLE", "OracleDSN", tcIniFile))
-
- *-- Sectiunea SYNC - pentru configurarea sincronizarii
- ADDPROPERTY(loSettings, "AdapterProgram", ReadPini("SYNC", "AdapterProgram", tcIniFile))
- ADDPROPERTY(loSettings, "JsonFilePattern", ReadPini("SYNC", "JsonFilePattern", tcIniFile))
- ADDPROPERTY(loSettings, "AutoRunAdapter", ReadPini("SYNC", "AutoRunAdapter", tcIniFile) = "1")
-
- *-- Sectiunea ROA - pentru configurarea sistemului ROA
- LOCAL lcRoaValue
-
- *-- IdPol - NULL sau valoare numerica
- lcRoaValue = UPPER(ALLTRIM(ReadPini("ROA", "IdPol", tcIniFile)))
- IF lcRoaValue = "NULL" OR EMPTY(lcRoaValue)
- ADDPROPERTY(loSettings, "IdPol", .NULL.)
- ELSE
- ADDPROPERTY(loSettings, "IdPol", VAL(lcRoaValue))
- ENDIF
-
- *-- IdGestiune - NULL sau valoare numerica
- lcRoaValue = UPPER(ALLTRIM(ReadPini("ROA", "IdGestiune", tcIniFile)))
- IF lcRoaValue = "NULL" OR EMPTY(lcRoaValue)
- ADDPROPERTY(loSettings, "IdGestiune", .NULL.)
- ELSE
- ADDPROPERTY(loSettings, "IdGestiune", VAL(lcRoaValue))
- ENDIF
-
- *-- IdSectie - NULL sau valoare numerica
- lcRoaValue = UPPER(ALLTRIM(ReadPini("ROA", "IdSectie", tcIniFile)))
- IF lcRoaValue = "NULL" OR EMPTY(lcRoaValue)
- ADDPROPERTY(loSettings, "IdSectie", .NULL.)
- ELSE
- ADDPROPERTY(loSettings, "IdSectie", VAL(lcRoaValue))
- ENDIF
-
- *-- Salvare in proprietatea clasei
- THIS.oSettings = loSettings
-
- RETURN loSettings
- ENDPROC
-
- *-- Functie pentru crearea unui fisier INI implicit cu setari de baza
- PROCEDURE CreateDefaultIni
- PARAMETERS tcIniFile
- LOCAL llSuccess
-
- IF EMPTY(tcIniFile)
- tcIniFile = THIS.cIniFile
- ENDIF
-
- llSuccess = .T.
-
- TRY
- *-- Sectiunea API
- WritePini("API", "ApiBaseUrl", "https://api.gomag.ro/api/v1/product/read/json?enabled=1", tcIniFile)
- WritePini("API", "OrderApiUrl", "https://api.gomag.ro/api/v1/order/read/json", tcIniFile)
- WritePini("API", "ApiKey", "YOUR_API_KEY_HERE", tcIniFile)
- WritePini("API", "ApiShop", "https://yourstore.gomag.ro", tcIniFile)
- WritePini("API", "UserAgent", "Mozilla/5.0", tcIniFile)
- WritePini("API", "ContentType", "application/json", tcIniFile)
-
- *-- Sectiunea PAGINATION
- WritePini("PAGINATION", "Limit", "100", tcIniFile)
-
- *-- Sectiunea OPTIONS
- WritePini("OPTIONS", "GetProducts", "1", tcIniFile)
- WritePini("OPTIONS", "GetOrders", "1", tcIniFile)
-
- *-- Sectiunea FILTERS
- WritePini("FILTERS", "OrderDaysBack", "7", tcIniFile)
-
- *-- Sectiunea ORACLE - conexiune database
- WritePini("ORACLE", "OracleUser", "MARIUSM_AUTO", tcIniFile)
- WritePini("ORACLE", "OraclePassword", "ROMFASTSOFT", tcIniFile)
- WritePini("ORACLE", "OracleDSN", "ROA_CENTRAL", tcIniFile)
-
- *-- Sectiunea SYNC - configurare sincronizare
- WritePini("SYNC", "AdapterProgram", "gomag-adapter.prg", tcIniFile)
- WritePini("SYNC", "JsonFilePattern", "gomag_orders*.json", tcIniFile)
- WritePini("SYNC", "AutoRunAdapter", "1", tcIniFile)
-
- *-- Sectiunea ROA - configurare sistem ROA
- WritePini("ROA", "IdPol", "NULL", tcIniFile)
- WritePini("ROA", "IdGestiune", "NULL", tcIniFile)
- WritePini("ROA", "IdSectie", "NULL", tcIniFile)
-
- CATCH
- llSuccess = .F.
- ENDTRY
-
- RETURN llSuccess
- ENDPROC
-
- *-- Functie pentru validarea setarilor obligatorii
- PROCEDURE ValidateSettings
- LPARAMETERS toSettings
- LOCAL llValid, lcErrors
-
- IF PCOUNT() = 0
- toSettings = THIS.oSettings
- ENDIF
-
- IF TYPE('toSettings') <> 'O' OR ISNULL(toSettings)
- RETURN .F.
- ENDIF
-
- llValid = .T.
- lcErrors = ""
-
- *-- Verificare setari API obligatorii
- IF EMPTY(toSettings.ApiKey) OR toSettings.ApiKey = "YOUR_API_KEY_HERE"
- llValid = .F.
- lcErrors = lcErrors + "ApiKey nu este setat corect in settings.ini" + CHR(13) + CHR(10)
- ENDIF
-
- IF EMPTY(toSettings.ApiShop) OR "yourstore.gomag.ro" $ toSettings.ApiShop
- llValid = .F.
- lcErrors = lcErrors + "ApiShop nu este setat corect in settings.ini" + CHR(13) + CHR(10)
- ENDIF
-
- *-- Verificare setari Oracle obligatorii (doar pentru sync)
- IF TYPE('toSettings.OracleUser') = 'C' AND EMPTY(toSettings.OracleUser)
- llValid = .F.
- lcErrors = lcErrors + "OracleUser nu este setat in settings.ini" + CHR(13) + CHR(10)
- ENDIF
-
- IF TYPE('toSettings.OracleDSN') = 'C' AND EMPTY(toSettings.OracleDSN)
- llValid = .F.
- lcErrors = lcErrors + "OracleDSN nu este setat in settings.ini" + CHR(13) + CHR(10)
- ENDIF
-
- *-- Log erorile daca exista
- IF !llValid AND TYPE('gcLogFile') = 'C'
- LogMessage("Erori validare settings.ini:", "ERROR", gcLogFile)
- LogMessage(lcErrors, "ERROR", gcLogFile)
- ENDIF
-
- RETURN llValid
- ENDPROC
-
- *-- Functie pentru configurarea initiala a aplicatiei
- PROCEDURE Setup
- LOCAL llSetupOk
-
- llSetupOk = .T.
-
- *-- Verificare existenta settings.ini
- IF !CheckIniFile(THIS.cIniFile)
- IF TYPE('gcLogFile') = 'C'
- LogMessage("ATENTIE: Fisierul settings.ini nu a fost gasit!", "WARN", gcLogFile)
- LogMessage("Cream un fisier settings.ini implicit...", "INFO", gcLogFile)
- ENDIF
-
- IF THIS.CreateDefaultIni()
- IF TYPE('gcLogFile') = 'C'
- LogMessage("Fisier settings.ini creat cu succes.", "INFO", gcLogFile)
- LogMessage("IMPORTANT: Modifica setarile din settings.ini (ApiKey, ApiShop) inainte de a rula scriptul din nou!", "INFO", gcLogFile)
- ENDIF
- llSetupOk = .F. && Opreste executia pentru a permite configurarea
- ELSE
- IF TYPE('gcLogFile') = 'C'
- LogMessage("EROARE: Nu s-a putut crea fisierul settings.ini!", "ERROR", gcLogFile)
- ENDIF
- llSetupOk = .F.
- ENDIF
- ENDIF
-
- *-- Incarca setarile daca setup-ul este OK
- IF llSetupOk
- THIS.LoadSettings()
- THIS.lInitialized = .T.
- ENDIF
-
- RETURN llSetupOk
- ENDPROC
-
- *-- Functie pentru afisarea informatiilor despre configuratie
- PROCEDURE DisplaySettingsInfo
- LPARAMETERS toSettings
- LOCAL lcInfo
-
- IF PCOUNT() = 0
- toSettings = THIS.oSettings
- ENDIF
-
- IF TYPE('toSettings') <> 'O' OR ISNULL(toSettings)
- RETURN .F.
- ENDIF
-
- IF TYPE('gcLogFile') != 'C'
- RETURN .F.
- ENDIF
-
- lcInfo = "=== CONFIGURATIE APLICATIE ==="
- LogMessage(lcInfo, "INFO", gcLogFile)
-
- *-- API Settings
- LogMessage("API: " + toSettings.ApiShop, "INFO", gcLogFile)
- LogMessage("Orders Days Back: " + TRANSFORM(toSettings.OrderDaysBack), "INFO", gcLogFile)
- LogMessage("Get Products: " + IIF(toSettings.GetProducts, "DA", "NU"), "INFO", gcLogFile)
- LogMessage("Get Orders: " + IIF(toSettings.GetOrders, "DA", "NU"), "INFO", gcLogFile)
-
- *-- Oracle Settings (doar daca exista)
- IF TYPE('toSettings.OracleUser') = 'C' AND !EMPTY(toSettings.OracleUser)
- LogMessage("Oracle User: " + toSettings.OracleUser, "INFO", gcLogFile)
- LogMessage("Oracle DSN: " + toSettings.OracleDSN, "INFO", gcLogFile)
- ENDIF
-
- *-- Sync Settings (doar daca exista)
- IF TYPE('toSettings.AdapterProgram') = 'C' AND !EMPTY(toSettings.AdapterProgram)
- LogMessage("Adapter Program: " + toSettings.AdapterProgram, "INFO", gcLogFile)
- LogMessage("JSON Pattern: " + toSettings.JsonFilePattern, "INFO", gcLogFile)
- LogMessage("Auto Run Adapter: " + IIF(toSettings.AutoRunAdapter, "DA", "NU"), "INFO", gcLogFile)
- ENDIF
-
- LogMessage("=== SFARSIT CONFIGURATIE ===", "INFO", gcLogFile)
-
- RETURN .T.
- ENDPROC
-
- *-- Metoda pentru setup complet cu validare
- PROCEDURE Initialize
- LOCAL llSuccess
-
- llSuccess = THIS.Setup()
-
- IF llSuccess
- llSuccess = THIS.ValidateSettings()
-
- IF llSuccess
- THIS.DisplaySettingsInfo()
- ENDIF
- ENDIF
-
- RETURN llSuccess
- ENDPROC
-
- *-- Functie pentru obtinerea setarilor
- PROCEDURE GetSettings
- RETURN THIS.oSettings
- ENDPROC
-
- *-- Functie pentru obtinerea path-ului aplicatiei
- PROCEDURE GetAppPath
- RETURN THIS.cAppPath
- ENDPROC
-
- *-- Functie pentru obtinerea path-ului fisierului INI
- PROCEDURE GetIniFile
- RETURN THIS.cIniFile
- ENDPROC
-
-ENDDEFINE
-
-*-- ApplicationSetup Class - Clasa pentru configurarea si setup-ul aplicatiei
-*-- Caracteristici:
-*-- - Gestionare completa a settings.ini cu toate sectiunile
-*-- - Creare fisier implicit cu valori default
-*-- - Validare setari obligatorii pentru functionare
-*-- - Setup si initializare completa cu o singura metoda
-*-- - Afisarea informatiilor despre configuratia curenta
-*-- - Proprietati pentru acces facil la configuratii si paths
\ No newline at end of file
diff --git a/vfp/gomag-adapter.prg b/vfp/gomag-adapter.prg
deleted file mode 100644
index cd3da5d..0000000
--- a/vfp/gomag-adapter.prg
+++ /dev/null
@@ -1,425 +0,0 @@
-*-- Script Visual FoxPro 9 pentru accesul la GoMag API cu paginare completa
-*-- Autor: Claude AI
-*-- Data: 26.08.2025
-
-SET SAFETY OFF
-SET CENTURY ON
-SET DATE DMY
-SET EXACT ON
-SET ANSI ON
-SET DELETED ON
-
-*-- Setari principale
-LOCAL lcApiBaseUrl, lcApiUrl, lcApiKey, lcUserAgent, lcContentType
-LOCAL loHttp, lcResponse, lcJsonResponse
-LOCAL laHeaders[10], lnHeaderCount
-Local lcApiShop, lcCsvFileName, lcErrorResponse, lcFileName, lcLogContent, lcLogFileName, lcPath
-Local lcStatusText, lnStatusCode, loError
-Local lnLimit, lnCurrentPage, llHasMorePages, loAllJsonData, lnTotalPages, lnTotalProducts
-Local lcOrderApiUrl, loAllOrderData, lcOrderCsvFileName, lcOrderJsonFileName
-Local ldStartDate, lcStartDateStr
-Local lcIniFile
-LOCAL llGetProducts, llGetOrders
-PRIVATE loJsonData, gcLogFile, gnStartTime, gnProductsProcessed, gnOrdersProcessed
-
-
-*-- Initializare logging si statistici
-gnStartTime = SECONDS()
-gnProductsProcessed = 0
-gnOrdersProcessed = 0
-gcLogFile = InitLog("gomag_sync")
-
-*-- Cream directorul output daca nu existe
-LOCAL lcOutputDir
-lcOutputDir = gcAppPath + "output"
-IF !DIRECTORY(lcOutputDir)
- MKDIR (lcOutputDir)
-ENDIF
-
-*-- Creare si initializare clasa setup aplicatie
-LOCAL loAppSetup
-loAppSetup = CREATEOBJECT("ApplicationSetup", gcAppPath)
-
-*-- Setup complet cu validare
-IF !loAppSetup.Initialize()
- LogMessage("EROARE: Setup-ul aplicatiei a esuat sau necesita configurare!", "ERROR", gcLogFile)
- RETURN .F.
-ENDIF
-
-*-- Configurare API din settings.ini
-lcApiBaseUrl = goSettings.ApiBaseUrl
-lcOrderApiUrl = goSettings.OrderApiUrl
-lcApiKey = goSettings.ApiKey
-lcApiShop = goSettings.ApiShop
-lcUserAgent = goSettings.UserAgent
-lcContentType = goSettings.ContentType
-lnLimit = goSettings.Limit
-llGetProducts = goSettings.GetProducts
-llGetOrders = goSettings.GetOrders
-lnCurrentPage = 1 && Pagina de start
-llHasMorePages = .T. && Flag pentru paginare
-loAllJsonData = NULL && Obiect pentru toate datele
-
-*-- Calculare data pentru ultimele X zile (din settings.ini)
-ldStartDate = DATE() - goSettings.OrderDaysBack
-lcStartDateStr = TRANSFORM(YEAR(ldStartDate)) + "-" + ;
- RIGHT("0" + TRANSFORM(MONTH(ldStartDate)), 2) + "-" + ;
- RIGHT("0" + TRANSFORM(DAY(ldStartDate)), 2)
-
-*******************************************
-*-- Sterg fisiere JSON comenzi anterioare
-lcDirJson = gcAppPath + "output\"
-lcJsonPattern = m.lcDirJson + goSettings.JsonFilePattern
-lnJsonFiles = ADIR(laJsonFiles, lcJsonPattern)
-FOR lnFile = 1 TO m.lnJsonFiles
- lcFile = m.lcDirJson + laJsonFiles[m.lnFile,1]
- IF FILE(m.lcFile)
- DELETE FILE (m.lcFile)
- ENDIF
-ENDFOR
-*******************************************
-
-*-- Verificare daca avem WinHttp disponibil
-TRY
- loHttp = CREATEOBJECT("WinHttp.WinHttpRequest.5.1")
-CATCH TO loError
- LogMessage("Eroare la crearea obiectului WinHttp: " + loError.Message, "ERROR", gcLogFile)
- RETURN .F.
-ENDTRY
-*-- SECTIUNEA PRODUSE - se executa doar daca llGetProducts = .T.
-IF llGetProducts
- LogMessage("[PRODUCTS] Starting product retrieval", "INFO", gcLogFile)
-
- *-- Bucla pentru preluarea tuturor produselor (paginare)
- loAllJsonData = CREATEOBJECT("Empty")
- ADDPROPERTY(loAllJsonData, "products", CREATEOBJECT("Empty"))
- ADDPROPERTY(loAllJsonData, "total", 0)
- ADDPROPERTY(loAllJsonData, "pages", 0)
- lnTotalProducts = 0
-
- DO WHILE llHasMorePages
- *-- Construire URL cu paginare
- lcApiUrl = lcApiBaseUrl + "&page=" + TRANSFORM(lnCurrentPage) + "&limit=" + TRANSFORM(lnLimit)
-
- LogMessage("[PRODUCTS] Page " + TRANSFORM(lnCurrentPage) + " fetching...", "INFO", gcLogFile)
-
- *-- Configurare request
- TRY
- *-- Initializare request GET
- loHttp.Open("GET", lcApiUrl, .F.)
-
- *-- Setare headers conform documentatiei GoMag
- loHttp.SetRequestHeader("User-Agent", lcUserAgent)
- loHttp.SetRequestHeader("Content-Type", lcContentType)
- loHttp.SetRequestHeader("Accept", "application/json")
- loHttp.SetRequestHeader("Apikey", lcApiKey) && Header pentru API Key
- loHttp.SetRequestHeader("ApiShop", lcApiShop) && Header pentru shop URL
-
- *-- Setari timeout
- loHttp.SetTimeouts(30000, 30000, 30000, 30000) && 30 secunde pentru fiecare
-
- *-- Trimitere request
- loHttp.Send()
-
- *-- Verificare status code
- lnStatusCode = loHttp.Status
- lcStatusText = loHttp.StatusText
-
- IF lnStatusCode = 200
- *-- Success - preluare raspuns
- lcResponse = loHttp.ResponseText
-
- *-- Parsare JSON cu nfjson
- SET PATH TO nfjson ADDITIVE
- loJsonData = nfJsonRead(lcResponse)
-
- IF !ISNULL(loJsonData)
- *-- Prima pagina - setam informatiile generale
- IF lnCurrentPage = 1
- LogMessage("[PRODUCTS] Analyzing JSON structure...", "INFO", gcLogFile)
- LOCAL ARRAY laJsonProps[1]
- lnPropCount = AMEMBERS(laJsonProps, loJsonData, 0)
- FOR lnDebugIndex = 1 TO MIN(lnPropCount, 10) && Primele 10 proprietati
- lcPropName = laJsonProps(lnDebugIndex)
- lcPropType = TYPE('loJsonData.' + lcPropName)
- LogMessage("[PRODUCTS] Property: " + lcPropName + " (Type: " + lcPropType + ")", "DEBUG", gcLogFile)
- ENDFOR
-
- IF TYPE('loJsonData.total') = 'C' OR TYPE('loJsonData.total') = 'N'
- loAllJsonData.total = VAL(TRANSFORM(loJsonData.total))
- ENDIF
- IF TYPE('loJsonData.pages') = 'C' OR TYPE('loJsonData.pages') = 'N'
- loAllJsonData.pages = VAL(TRANSFORM(loJsonData.pages))
- ENDIF
- LogMessage("[PRODUCTS] Total items: " + TRANSFORM(loAllJsonData.total) + " | Pages: " + TRANSFORM(loAllJsonData.pages), "INFO", gcLogFile)
- ENDIF
-
- *-- Adaugare produse din pagina curenta
- LOCAL llHasProducts, lnProductsFound
- llHasProducts = .F.
- lnProductsFound = 0
-
- IF TYPE('loJsonData.products') = 'O'
- *-- Numaram produsele din obiectul products
- lnProductsFound = AMEMBERS(laProductsPage, loJsonData.products, 0)
- IF lnProductsFound > 0
- DO MergeProducts WITH loAllJsonData, loJsonData
- llHasProducts = .T.
- LogMessage("[PRODUCTS] Found: " + TRANSFORM(lnProductsFound) + " products in page " + TRANSFORM(lnCurrentPage), "INFO", gcLogFile)
- gnProductsProcessed = gnProductsProcessed + lnProductsFound
- ENDIF
- ENDIF
-
- IF !llHasProducts
- LogMessage("[PRODUCTS] WARNING: No products found in JSON response for page " + TRANSFORM(lnCurrentPage), "WARN", gcLogFile)
- ENDIF
-
- *-- Verificare daca mai sunt pagini
- IF TYPE('loJsonData.pages') = 'C' OR TYPE('loJsonData.pages') = 'N'
- lnTotalPages = VAL(TRANSFORM(loJsonData.pages))
- IF lnCurrentPage >= lnTotalPages
- llHasMorePages = .F.
- ENDIF
- ELSE
- *-- Daca nu avem info despre pagini, verificam daca sunt produse
- IF TYPE('loJsonData.products') != 'O'
- llHasMorePages = .F.
- ENDIF
- ENDIF
-
- lnCurrentPage = lnCurrentPage + 1
-
- ELSE
- *-- Salvare raspuns JSON raw in caz de eroare de parsare
- lcFileName = "gomag_error_page" + TRANSFORM(lnCurrentPage) + "_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".json"
- STRTOFILE(lcResponse, lcFileName)
- llHasMorePages = .F.
- ENDIF
-
- ELSE
- *-- Eroare HTTP - salvare in fisier de log
- lcLogFileName = "gomag_error_page" + TRANSFORM(lnCurrentPage) + "_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".log"
- lcLogContent = "HTTP Error " + TRANSFORM(lnStatusCode) + ": " + lcStatusText + CHR(13) + CHR(10)
-
- *-- Incearca sa citesti raspunsul pentru detalii despre eroare
- TRY
- lcErrorResponse = loHttp.ResponseText
- IF !EMPTY(lcErrorResponse)
- lcLogContent = lcLogContent + "Error Details:" + CHR(13) + CHR(10) + lcErrorResponse
- ENDIF
- CATCH
- lcLogContent = lcLogContent + "Could not read error details"
- ENDTRY
-
- STRTOFILE(lcLogContent, lcLogFileName)
- llHasMorePages = .F.
- ENDIF
-
- CATCH TO loError
- *-- Salvare erori in fisier de log pentru pagina curenta
- lcLogFileName = "gomag_error_page" + TRANSFORM(lnCurrentPage) + "_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".log"
- lcLogContent = "Script Error on page " + TRANSFORM(lnCurrentPage) + ":" + CHR(13) + CHR(10) +;
- "Error Number: " + TRANSFORM(loError.ErrorNo) + CHR(13) + CHR(10) +;
- "Error Message: " + loError.Message + CHR(13) + CHR(10) +;
- "Error Line: " + TRANSFORM(loError.LineNo)
- STRTOFILE(lcLogContent, lcLogFileName)
- llHasMorePages = .F.
- ENDTRY
-
- IF llHasMorePages
- INKEY(1) && Pauza de 10 secunde pentru a evita "Limitele API depasite"
- ENDIF
-
-ENDDO
-
- *-- Salvare array JSON cu toate produsele
- IF !ISNULL(loAllJsonData) AND TYPE('loAllJsonData.products') = 'O'
- lcJsonFileName = lcOutputDir + "\gomag_all_products_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".json"
- DO SaveProductsArray WITH loAllJsonData, lcJsonFileName
- LogMessage("[PRODUCTS] JSON saved: " + lcJsonFileName, "INFO", gcLogFile)
- *-- Calculam numarul de produse procesate
- IF TYPE('loAllJsonData.products') = 'O'
- LOCAL ARRAY laProducts[1]
- lnPropCount = AMEMBERS(laProducts, loAllJsonData.products, 0)
- gnProductsProcessed = lnPropCount
- ENDIF
- ENDIF
-
-ELSE
- LogMessage("[PRODUCTS] Skipped product retrieval (llGetProducts = .F.)", "INFO", gcLogFile)
-ENDIF
-
-*-- SECTIUNEA COMENZI - se executa doar daca llGetOrders = .T.
-IF llGetOrders
- LogMessage("[ORDERS] =======================================", "INFO", gcLogFile)
- LogMessage("[ORDERS] RETRIEVING ORDERS FROM LAST " + TRANSFORM(goSettings.OrderDaysBack) + " DAYS", "INFO", gcLogFile)
- LogMessage("[ORDERS] Start date: " + lcStartDateStr, "INFO", gcLogFile)
- LogMessage("[ORDERS] =======================================", "INFO", gcLogFile)
-
- *-- Reinitializare pentru comenzi
- lnCurrentPage = 1
- llHasMorePages = .T.
- loAllOrderData = CREATEOBJECT("Empty")
- ADDPROPERTY(loAllOrderData, "orders", CREATEOBJECT("Empty"))
- ADDPROPERTY(loAllOrderData, "total", 0)
- ADDPROPERTY(loAllOrderData, "pages", 0)
-
-*-- Bucla pentru preluarea comenzilor
-DO WHILE llHasMorePages
- *-- Construire URL cu paginare si filtrare pe data (folosind startDate conform documentatiei GoMag)
- lcApiUrl = lcOrderApiUrl + "?startDate=" + lcStartDateStr + "&page=" + TRANSFORM(lnCurrentPage) + "&limit=" + TRANSFORM(lnLimit)
-
- LogMessage("[ORDERS] Page " + TRANSFORM(lnCurrentPage) + " fetching...", "INFO", gcLogFile)
-
- *-- Configurare request
- TRY
- *-- Initializare request GET
- loHttp.Open("GET", lcApiUrl, .F.)
-
- *-- Setare headers conform documentatiei GoMag
- loHttp.SetRequestHeader("User-Agent", lcUserAgent)
- loHttp.SetRequestHeader("Content-Type", lcContentType)
- loHttp.SetRequestHeader("Accept", "application/json")
- loHttp.SetRequestHeader("Apikey", lcApiKey) && Header pentru API Key
- loHttp.SetRequestHeader("ApiShop", lcApiShop) && Header pentru shop URL
-
- *-- Setari timeout
- loHttp.SetTimeouts(30000, 30000, 30000, 30000) && 30 secunde pentru fiecare
-
- *-- Trimitere request
- loHttp.Send()
-
- *-- Verificare status code
- lnStatusCode = loHttp.Status
- lcStatusText = loHttp.StatusText
-
- IF lnStatusCode = 200
- *-- Success - preluare raspuns
- lcResponse = loHttp.ResponseText
-
- *-- SALVARE DIRECTA: Salveaza raspunsul RAW exact cum vine din API, pe pagini
- lcOrderJsonFileName = lcOutputDir + "\gomag_orders_page" + TRANSFORM(lnCurrentPage) + "_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".json"
- STRTOFILE(lcResponse, lcOrderJsonFileName)
- LogMessage("[ORDERS] JSON RAW salvat: " + lcOrderJsonFileName, "INFO", gcLogFile)
-
- *-- Parsare JSON pentru obtinerea numarului de pagini
- SET PATH TO nfjson ADDITIVE
- loOrdersJsonData = nfJsonRead(lcResponse)
-
- IF !ISNULL(loOrdersJsonData)
- *-- Extragere informatii paginare din JSON procesat
- IF lnCurrentPage = 1
- IF TYPE('loOrdersJsonData.total') = 'C' OR TYPE('loOrdersJsonData.total') = 'N'
- LOCAL lnTotalOrders
- lnTotalOrders = VAL(TRANSFORM(loOrdersJsonData.total))
- LogMessage("[ORDERS] Total orders: " + TRANSFORM(lnTotalOrders), "INFO", gcLogFile)
- ENDIF
- ENDIF
-
- IF TYPE('loOrdersJsonData.pages') = 'C' OR TYPE('loOrdersJsonData.pages') = 'N'
- lnTotalPages = VAL(TRANSFORM(loOrdersJsonData.pages))
- IF lnCurrentPage = 1
- LogMessage("[ORDERS] Total pages: " + TRANSFORM(lnTotalPages), "INFO", gcLogFile)
- ENDIF
-
- IF lnCurrentPage >= lnTotalPages
- llHasMorePages = .F.
- LogMessage("[ORDERS] Reached last page (" + TRANSFORM(lnCurrentPage) + "/" + TRANSFORM(lnTotalPages) + ")", "INFO", gcLogFile)
- ENDIF
- ELSE
- *-- Fallback: verificare daca mai sunt comenzi in pagina
- IF TYPE('loOrdersJsonData.orders') != 'O'
- llHasMorePages = .F.
- LogMessage("[ORDERS] No orders found in response, stopping pagination", "INFO", gcLogFile)
- ENDIF
- ENDIF
-
- *-- Numarare comenzi din pagina curenta
- IF TYPE('loOrdersJsonData.orders') = 'O'
- LOCAL lnOrdersInPage
- lnOrdersInPage = AMEMBERS(laOrdersPage, loOrdersJsonData.orders, 0)
- gnOrdersProcessed = gnOrdersProcessed + lnOrdersInPage
- LogMessage("[ORDERS] Found " + TRANSFORM(lnOrdersInPage) + " orders in page " + TRANSFORM(lnCurrentPage), "INFO", gcLogFile)
- ENDIF
-
- ELSE
- *-- Eroare la parsarea JSON
- LogMessage("[ORDERS] ERROR: Could not parse JSON response for page " + TRANSFORM(lnCurrentPage), "ERROR", gcLogFile)
- llHasMorePages = .F.
- ENDIF
-
- lnCurrentPage = lnCurrentPage + 1
-
- ELSE
- *-- Eroare HTTP - salvare in fisier de log
- lcLogFileName = "gomag_order_error_page" + TRANSFORM(lnCurrentPage) + "_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".log"
- lcLogContent = "HTTP Error " + TRANSFORM(lnStatusCode) + ": " + lcStatusText + CHR(13) + CHR(10)
-
- *-- Incearca sa citesti raspunsul pentru detalii despre eroare
- TRY
- lcErrorResponse = loHttp.ResponseText
- IF !EMPTY(lcErrorResponse)
- lcLogContent = lcLogContent + "Error Details:" + CHR(13) + CHR(10) + lcErrorResponse
- ENDIF
- CATCH
- lcLogContent = lcLogContent + "Could not read error details"
- ENDTRY
-
- STRTOFILE(lcLogContent, lcLogFileName)
- llHasMorePages = .F.
- ENDIF
-
- CATCH TO loError
- *-- Salvare erori in fisier de log pentru pagina curenta
- lcLogFileName = "gomag_order_error_page" + TRANSFORM(lnCurrentPage) + "_" + DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "") + ".log"
- lcLogContent = "Script Error on page " + TRANSFORM(lnCurrentPage) + ":" + CHR(13) + CHR(10) +;
- "Error Number: " + TRANSFORM(loError.ErrorNo) + CHR(13) + CHR(10) +;
- "Error Message: " + loError.Message + CHR(13) + CHR(10) +;
- "Error Line: " + TRANSFORM(loError.LineNo)
- STRTOFILE(lcLogContent, lcLogFileName)
- llHasMorePages = .F.
- ENDTRY
-
- IF llHasMorePages
- INKEY(1) && Pauza de 10 secunde pentru a evita "Limitele API depasite"
- ENDIF
-
-ENDDO
-
- LogMessage("[ORDERS] JSON files salvate pe pagini separate in directorul output/", "INFO", gcLogFile)
- LogMessage("[ORDERS] Total orders processed: " + TRANSFORM(gnOrdersProcessed), "INFO", gcLogFile)
-
-ELSE
- LogMessage("[ORDERS] Skipped order retrieval (llGetOrders = .F.)", "INFO", gcLogFile)
-ENDIF
-
-*-- Curatare
-loHttp = NULL
-
-*-- Inchidere logging cu statistici finale
-CloseLog(gnStartTime, gnProductsProcessed, gnOrdersProcessed, gcLogFile)
-
-
-
-*-- Functiile utilitare au fost mutate in utils.prg
-
-*-- Scriptul cu paginare completa pentru preluarea tuturor produselor si comenzilor
-*-- Caracteristici principale:
-*-- - Paginare automata pentru toate produsele si comenzile
-*-- - Pauze intre cereri pentru respectarea rate limiting
-*-- - Salvare JSON array-uri pure (fara metadata de paginare)
-*-- - Utilizare nfjsoncreate pentru generare JSON corecta
-*-- - Logging separat pentru fiecare pagina in caz de eroare
-*-- - Afisare progres in timpul executiei
-
-*-- INSTRUCTIUNI DE UTILIZARE:
-*-- 1. Modifica settings.ini cu setarile tale:
-*-- - ApiKey: cheia ta API de la GoMag
-*-- - ApiShop: URL-ul magazinului tau
-*-- - GetProducts: 1 pentru a prelua produse, 0 pentru a sari peste
-*-- - GetOrders: 1 pentru a prelua comenzi, 0 pentru a sari peste
-*-- - OrderDaysBack: numarul de zile pentru preluarea comenzilor
-*-- 2. Ruleaza scriptul - va prelua doar ce ai selectat
-*-- 3. Verifica fisierele JSON generate cu array-uri pure
-
-*-- Script optimizat cu salvare JSON array-uri - verificati fisierele generate
\ No newline at end of file
diff --git a/vfp/gomag-comenzi.sql b/vfp/gomag-comenzi.sql
deleted file mode 100644
index 9001a99..0000000
--- a/vfp/gomag-comenzi.sql
+++ /dev/null
@@ -1,7 +0,0 @@
-alter table COMENZI_ELEMENTE add ptva number(5,2);
-comment on column COMENZI_ELEMENTE.ptva is 'PROCENT TVA (11,21)';
-
--- ARTICOLE_TERTI
--- PACK_COMENZI
--- PACK_IMPORT_PARTENERI
--- PACK_IMPORT_COMENZI
\ No newline at end of file
diff --git a/vfp/import-comanda.tst b/vfp/import-comanda.tst
deleted file mode 100644
index 4337f36..0000000
--- a/vfp/import-comanda.tst
+++ /dev/null
@@ -1,50 +0,0 @@
-PL/SQL Developer Test script 3.0
-12
-begin
- -- Call the procedure
- PACK_IMPORT_COMENZI.importa_comanda(p_nr_comanda_ext => '479317993',
- p_data_comanda => TO_DATE('03032026','DDMMYYYY'),
- p_id_partener => 1424,
- p_json_articole => '{"baseprice":"46","ean":"5941623003366","id":"137","name":"Coffee Creamer Doncafe Lapte Praf 1 Kg","price":"40.99","quantity":"1.00","sku":"5941623003366","type":"product","vat":"21"}',
- p_id_adresa_livrare => 1213,
- p_id_adresa_facturare => 1213,
- p_id_pol => 39,
- p_id_sectie => NULL,
- v_id_comanda => :v_id_comanda);
-end;
-9
-p_nr_comanda_ext
-1
-479317993
--5
-p_data_comanda
-1
-3/3/2026
--12
-p_id_partener
-1
-1424
--4
-p_json_articole
-1
-
--112
-p_id_adresa_livrare
-1
-1213
--4
-p_id_adresa_facturare
-1
-1213
--4
-p_id_pol
-1
-39
--4
-p_id_sectie
-0
--4
-v_id_comanda
-0
-4
-0
diff --git a/vfp/nfjson/nfjsoncreate.prg b/vfp/nfjson/nfjsoncreate.prg
deleted file mode 100644
index ac69ac6..0000000
--- a/vfp/nfjson/nfjsoncreate.prg
+++ /dev/null
@@ -1,381 +0,0 @@
-*-------------------------------------------------------------------
-* Created by Marco Plaza @vfp2Nofox
-* ver 1.100 - 24/02/2016
-* enabled collection processing
-* ver 1.101 - 24/02/2016
-* solved indentation on nested collections
-* ver 1.110 -11/03/2016
-* -added support for collections inside arrays
-* -user can pass aMemembersFlag value
-* ( since Json is intended for DTO creation default value is 'U' )
-* check amembers topic on vfp help file for usage
-* changed cr to crlf
-* Added Json validation ; throws error for invalid Json.
-* ver 1.120
-* encode control characters ( chr(0) ~ chr(31) )
-*-----------------------------------------------------------------------
-Parameters ovfp,FormattedOutput,nonullarrayitem,crootName,aMembersFlag
-
-#Define crlf Chr(13)+Chr(10)
-
-Private All
-
-aMembersFlag = Evl(m.aMembersFlag,'U')
-
-esarray = Type('oVfp',1) = 'A'
-esobjeto = Vartype(m.ovfp) = 'O'
-
-If !m.esarray And !m.esobjeto
- Error 'must supply a vfp object/array'
-Endif
-
-_nivel = Iif( Cast(m.formattedOutput As l ) , 1, -1)
-
-Do Case
- Case esarray
-
- ojson = Createobject('empty')
-
- AddProperty(ojson,'array(1)')
- Acopy(ovfp,ojson.Array)
- cjson = procobject(ojson,.F.,m.nonullarrayitem,m.aMembersFlag)
- cjson = Substr( m.cjson,At('[',m.cjson))
-
-
- Case Type('oVfp.BaseClass')='C' And ovfp.BaseClass = 'Collection'
- cjson = procobject(ovfp,.T.,m.nonullarrayitem,m.aMembersFlag)
-
- crootName = Evl(m.crootName,'collection')
- cjson = '{"'+m.crootName+collTagName(ovfp)+'": '+cjson+'}'+Iif(FormattedOutput,crlf,'')+'}'
-
- Otherwise
- cjson = '{'+procobject(ovfp,.F.,m.nonullarrayitem,m.aMembersFlag)+'}'
-
-Endcase
-
-
-Return Ltrim(cjson)
-
-*----------------------------------------
-Function collTagName(thiscoll)
- *----------------------------------------
- Return Iif( m.thiscoll.Count > 0 And !Empty( m.thiscoll.GetKey(1) ), '_kv_collection','_kl_collection' )
-
- *----------------------------------------------------------------------------------
-Function procobject(obt,iscollection,nonullarrayitem,aMembersFlag)
- *----------------------------------------------------------------------------------
-
- If Isnull(obt)
- Return 'null'
- Endif
-
- Private All Except _nivel
-
- este = ''
-
- xtabs = nivel(2)
-
- bc = Iif(Type('m.obt.class')='C',m.obt.Class,'?')
-
- iscollection = bc = 'Collection'
-
- If m.iscollection
-
-
- este = este+'{ '+xtabs
- xtabs = nivel(2)
- este = este+'"collectionitems": ['+xtabs
-
- procCollection(obt,m.nonullarrayitem,m.aMembersFlag)
-
- xtabs = nivel(-2)
- este = este+xtabs+']'
-
- Else
-
- Amembers(am,m.obt,0,m.aMembersFlag)
-
- If Vartype(m.am) = 'U'
- xtabs=m.nivel(-2)
- Return ''
- Endif
-
-
- nm = Alen(am)
-
- For x1 = 1 To m.nm
-
- Var = Lower(am(m.x1))
-
- este = m.este+Iif(m.x1>1,',','')+m.xtabs
-
- este = m.este+["]+Strtran(m.var,'_vfpsafe_','')+[":]
-
- esobjeto = Type('m.obt.&Var')='O'
-
- If Type('m.obt.&var') = 'U'
- este = m.este+["unable to evaluate expression"]
- Loop
- Endif
-
- esarray = Type('m.obt.&Var',1) = 'A'
-
- Do Case
-
- Case m.esarray
-
- procarray(obt,m.var,m.nonullarrayitem)
-
- Case m.esobjeto
-
- thiso=m.obt.&Var
-
- bc = Iif(Type('m.thiso.class')='C',m.thiso.Class,'?')
-
- If bc = 'Collection'
-
- este = Rtrim(m.este,1,'":')+ collTagName( m.thiso )+'":'
-
- este = m.este+procobject(m.obt.&Var,.T.,m.nonullarrayitem,m.aMembersFlag)+[}]
-
- Else
-
- este = m.este+[{]+procobject(m.obt.&Var,.F.,m.nonullarrayitem,m.aMembersFlag)+[}]
-
- Endif
-
- Otherwise
-
-
- este = este+concatval(m.obt.&Var)
-
- Endcase
-
- Endfor
-
-
- Endif
-
- xtabs = nivel(-2)
- este = este+m.xtabs
-
-
- Return m.este
-
-
- *----------------------------------------------------
-Procedure procarray(obt,arrayName,nonullarrayitem)
- *----------------------------------------------------
- nrows = Alen(m.obt.&arrayName,1)
- ncols = Alen(m.obt.&arrayName,2)
- bidim = m.ncols > 0
- ncols = Iif(m.ncols=0,m.nrows,m.ncols)
- titems = Alen(m.obt.&arrayName)
-
- xtabs=nivel(2)
-
- este = m.este+'['+m.xtabs
- nelem = 1
-
- Do While nelem <= m.titems
-
- este = este+Iif(m.nelem>1,','+m.xtabs,'')
-
- If m.bidim
- xtabs = nivel(2)
- este = m.este+'['+m.xtabs
- Endif
-
- For pn = m.nelem To m.nelem+m.ncols-1
-
- elem = m.obt.&arrayName( m.pn )
-
- este = m.este+Iif(m.pn>m.nelem,','+m.xtabs,'')
-
- If Vartype(m.elem) # 'O'
-
- If m.nelem+m.ncols-1 = 1 And Isnull(m.elem) And m.nonullarrayitem
-
- este = m.este+""
-
- Else
- este = m.este+concatval(m.elem)
-
- Endif
-
- Else
-
-
- bc = Iif(Type('m.elem.class')='C',m.elem.Class,'?')
-
- If bc = 'Collection'
-
- este = m.este+' { "collection'+ collTagName( m.elem )+'":'
-
- este = m.este+procobject(m.elem ,.T.,m.nonullarrayitem,m.aMembersFlag)
-
- este = este + '}'+m.xtabs+'}'
-
- Else
-
- este = m.este+[{]+procobject(m.elem ,.F.,m.nonullarrayitem,m.aMembersFlag)+[}]
-
- Endif
-
-
- Endif
-
- Endfor
-
- nelem = m.pn
-
- If m.bidim
- xtabs=nivel(-2)
- este = m.este+m.xtabs+']'
- Endif
-
- Enddo
-
-
- xtabs=nivel(-2)
-
- este = m.este+m.xtabs+']'
-
-
-
-
-
- *-----------------------------
-Function nivel(N)
- *-----------------------------
- If m._nivel = -1
- Return ''
- Else
- _nivel= m._nivel+m.n
- Return crlf+Replicate(' ',m._nivel)
- Endif
-
- *-----------------------------
-Function concatval(valor)
- *-----------------------------
-
- #Define specialChars ["\/]+Chr(127)+Chr(12)+Chr(10)+Chr(13)+Chr(9)+Chr(0)+Chr(1)+Chr(2)+Chr(3)+Chr(4)+Chr(5)+Chr(6)+Chr(7)+Chr(8)+Chr(9)+Chr(10)+Chr(11)+Chr(12)+Chr(13)+Chr(14)+Chr(15)+Chr(16)+Chr(17)+Chr(18)+Chr(19)+Chr(20)+Chr(21)+Chr(22)+Chr(23)+Chr(24)+Chr(25)+Chr(26)+Chr(27)+Chr(28)+Chr(29)+Chr(30)+Chr(31)
-
- If Isnull(m.valor)
-
- Return 'null'
-
- Else
-
-
- tvar = Vartype(m.valor)
- ** no cambiar el orden de ejecución!
- Do Case
- Case m.tvar $ 'FBYINQ'
- vc = Rtrim(Cast( m.valor As c(32)))
- Case m.tvar = 'L'
- vc = Iif(m.valor,'true','false')
- Case m.tvar $ 'DT'
- vc = ["]+Ttoc(m.valor,3)+["]
- Case mustEncode(m.valor)
- vc = ["]+escapeandencode(m.valor)+["]
- Case m.tvar $ 'CVM'
- vc = ["]+Rtrim(m.valor)+["]
- Case m.tvar $ 'GQW'
- vc = ["]+Strconv(m.valor,13)+["]
- Endcase
-
- Return m.vc
-
- Endif
-
- *-----------------------------------
-Function mustEncode(valor)
- *-----------------------------------
- Return Len(Chrtran(m.valor,specialChars,'')) <> Len(m.valor)
-
- *-------------------------------
-Function escapeandencode(valun)
- *-------------------------------
- valun = Strtran(m.valun,'\','\\')
- valun = Strtran(m.valun,'"','\"')
- *valun = Strtran(m.valun,'/','\/')
-
- If !mustEncode(m.valun)
- Return
- Endif
-
- valun = Strtran(m.valun,Chr(127),'\b')
- valun = Strtran(m.valun,Chr(12),'\f')
- valun = Strtran(m.valun,Chr(10),'\n')
- valun = Strtran(m.valun,Chr(13),'\r')
- valun = Strtran(m.valun,Chr(9),'\t')
-
- If !mustEncode(m.valun)
- Return
- Endif
-
- Local x
- For x = 0 To 31
- valun = Strtran(m.valun,Chr(m.x),'\u'+Right(Transform(m.x,'@0'),4))
- Endfor
-
- Return Rtrim(m.valun)
-
-
-
- *---------------------------------------------------------------
-Function procCollection(obt,nonullArrayItems,aMembersFlag )
- *---------------------------------------------------------------
-
- Local iscollection
-
- With obt
-
- nm = .Count
-
- conllave = .Count > 0 And !Empty(.GetKey(1))
-
- For x1 = 1 To .Count
-
- If conllave
- elem = Createobject('empty')
- AddProperty(elem,'Key', .GetKey(x1) )
- AddProperty(elem,'Value',.Item(x1))
- Else
- elem = .Item(x1)
- Endif
-
- este = este+Iif(x1>1,','+xtabs,'')
-
- If Vartype(elem) # 'O'
-
- este = este+concatval(m.elem)
-
- Else
-
- If Vartype( m.elem.BaseClass ) = 'C' And m.elem.BaseClass = 'Collection'
- iscollection = .T.
- este = m.este+'{ '+m.xtabs+'"collection'+collTagName(m.elem)+'" :'
- xtabs = nivel(2)
- Else
- iscollection = .F.
- m.este = m.este+'{'
- Endif
-
- este = este+procobject(m.elem, m.iscollection , m.nonullarrayitem, m.aMembersFlag )
-
- este = este+'}'
-
- If m.iscollection
- xtabs = nivel(-2)
- este = este+m.xtabs+'}'
- Endif
-
- Endif
-
- Endfor
-
- este = Rtrim(m.este,1,m.xtabs)
-
- Endwith
diff --git a/vfp/nfjson/nfjsonread.prg b/vfp/nfjson/nfjsonread.prg
deleted file mode 100644
index 671f3e6..0000000
--- a/vfp/nfjson/nfjsonread.prg
+++ /dev/null
@@ -1,775 +0,0 @@
-*-------------------------------------------------------------------
-* Created by Marco Plaza vfp2nofox@gmail.com / @vfp2Nofox
-* ver 2.000 - 26/03/2016
-* ver 2.090 - 22/07/2016 :
-* improved error management
-* nfjsonread will return .null. for invalid json
-*-------------------------------------------------------------------
-Lparameters cjsonstr,isFileName,reviveCollection
-
-#Define crlf Chr(13)+Chr(10)
-
-Private All
-
-stackLevels=Astackinfo(aerrs)
-
-If m.stackLevels > 1
- calledFrom = 'called From '+aerrs(m.stackLevels-1,4)+' line '+Transform(aerrs(m.stackLevels-1,5))
-Else
- calledFrom = ''
-Endif
-
-oJson = nfJsonCreate2(cjsonstr,isFileName,reviveCollection)
-
-Return Iif(Vartype(m.oJson)='O',m.oJson,.Null.)
-
-
-*-------------------------------------------------------------------------
-Function nfJsonCreate2(cjsonstr,isFileName,reviveCollection)
-*-------------------------------------------------------------------------
-* validate parameters:
-
-Do Case
-Case ;
- Vartype(m.cjsonstr) # 'C' Or;
- Vartype(m.reviveCollection) # 'L' Or ;
- Vartype(m.isFileName) # 'L'
-
- jERROR('invalid parameter type')
-
-Case m.isFileName And !File(m.cjsonstr)
-
- jERROR('File "'+Rtrim(Left(m.cjsonstr,255))+'" does not exist')
-
-
-Endcase
-
-* process json:
-
-If m.isFileName
- cjsonstr = Filetostr(m.cjsonstr)
-Endif
-
-
-cJson = Rtrim(Chrtran(m.cjsonstr,Chr(13)+Chr(9)+Chr(10),''))
-pChar = Left(Ltrim(m.cJson),1)
-
-
-nl = Alines(aj,m.cJson,20,'{','}','"',',',':','[',']')
-
-For xx = 1 To Alen(aj)
- If Left(Ltrim(aj(m.xx)),1) $ '{}",:[]' Or Left(Ltrim(m.aj(m.xx)),4) $ 'true/false/null'
- aj(m.xx) = Ltrim(aj(m.xx))
- Endif
-Endfor
-
-
-Try
-
- x = 1
- cError = ''
- oStack = Createobject('stack')
-
- oJson = Createobject('empty')
-
- Do Case
- Case aj(1)='{'
- x = 1
- oStack.pushObject()
- procstring(m.oJson)
-
- Case aj(1) = '['
- x = 0
- procstring(m.oJson,.T.)
-
- Otherwise
- Error 'Invalid Json: expecting [{ received '+m.pChar
-
- Endcase
-
-
- If m.reviveCollection
- oJson = reviveCollection(m.oJson)
- Endif
-
-
-Catch To oerr
-
- strp = ''
-
- For Y = 1 To m.x
- strp = m.strp+aj(m.y)
- Endfor
-
- Do Case
- Case oerr.ErrorNo = 1098
-
- cError = ' Invalid Json: '+ m.oerr.Message+crlf+' Parsing: '+Right(m.strp,80)
-
-*+' program line: '+Transform(oerr.Lineno)+' array item '+Transform(m.x)
-
- Case oerr.ErrorNo = 2034
-
- cError = ' INVALID DATE: '+crlf+' Parsing: '+Right(m.strp,80)
-
-
- Otherwise
-
- cError = 'program error # '+Transform(m.oerr.ErrorNo)+crlf+m.oerr.Message+' at: '+Transform(oerr.Lineno)+crlf+' Parsing ('+Transform(m.x)+') '
-
- Endcase
-
-Endtry
-
-If !Empty(m.cError)
- jERROR(m.cError)
-Endif
-
-Return m.oJson
-
-
-
-*------------------------------------------------
-Procedure jERROR( cMessage )
-*------------------------------------------------
-Error 'nfJson ('+m.calledFrom+'):'+crlf+m.cMessage
-Return To nfJsonRead
-
-
-
-*--------------------------------------------------------------------------------
-Procedure procstring(obj,eValue)
-*--------------------------------------------------------------------------------
-#Define cvalid 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_'
-#Define creem '_______________________________________________________________'
-
-Private rowpos,colpos,bidim,ncols,arrayName,expecting,arrayLevel,vari
-Private expectingPropertyName,expectingValue,objectOpen
-
-expectingPropertyName = !m.eValue
-expectingValue = m.eValue
-expecting = Iif(expectingPropertyName,'"}','')
-objectOpen = .T.
-bidim = .F.
-colpos = 0
-rowpos = 0
-arrayLevel = 0
-arrayName = ''
-vari = ''
-ncols = 0
-
-Do While m.objectOpen
-
- x = m.x+1
-
- Do Case
-
- Case m.x > m.nl
-
- m.x = m.nl
-
- If oStack.Count > 0
- Error 'expecting '+m.expecting
- Endif
-
- Return
-
- Case aj(m.x) = '}' And '}' $ m.expecting
- closeObject()
-
- Case aj(x) = ']' And ']' $ m.expecting
- closeArray()
-
- Case m.expecting = ':'
- If aj(m.x) = ':'
- expecting = ''
- Loop
- Else
- Error 'expecting : received '+aj(m.x)
- Endif
-
- Case ',' $ m.expecting
-
- Do Case
- Case aj(x) = ','
- expecting = Iif( '[' $ m.expecting , '[' , '' )
- Case Not aj(m.x) $ m.expecting
- Error 'expecting '+m.expecting+' received '+aj(m.x)
- Otherwise
- expecting = Strtran(m.expecting,',','')
- Endcase
-
-
- Case m.expectingPropertyName
-
- If aj(m.x) = '"'
- propertyName(m.obj)
- Else
- Error 'expecting "'+m.expecting+' received '+aj(m.x)
- Endif
-
-
- Case m.expectingValue
-
- If m.expecting == '[' And m.aj(m.x) # '['
- Error 'expecting [ received '+aj(m.x)
- Else
- procValue(m.obj)
- Endif
-
-
- Endcase
-
-
-Enddo
-
-
-*----------------------------------------------------------
-Function anuevoel(obj,arrayName,valasig,bidim,colpos,rowpos)
-*----------------------------------------------------------
-
-
-If m.bidim
-
- colpos = m.colpos+1
-
- If colpos > m.ncols
- ncols = m.colpos
- Endif
-
- Dimension obj.&arrayName(m.rowpos,m.ncols)
-
- obj.&arrayName(m.rowpos,m.colpos) = m.valasig
-
- If Vartype(m.valasig) = 'O'
- procstring(obj.&arrayName(m.rowpos,m.colpos))
- Endif
-
-Else
-
- rowpos = m.rowpos+1
- Dimension obj.&arrayName(m.rowpos)
-
- obj.&arrayName(m.rowpos) = m.valasig
-
- If Vartype(m.valasig) = 'O'
- procstring(obj.&arrayName(m.rowpos))
- Endif
-
-Endif
-
-
-*-----------------------------------------
-Function unescunicode( Value )
-*-----------------------------------------
-
-
-noc=1
-
-Do While .T.
-
- posunicode = At('\u',m.value,m.noc)
-
- If m.posunicode = 0
- Return
- Endif
-
- If Substr(m.value,m.posunicode-1,1) = '\' And Substr(m.value,m.posunicode-2,1) # '\'
- noc=m.noc+1
- Loop
- Endif
-
- nunic = Evaluate('0x'+ Substr(m.value,m.posunicode+2,4) )
-
- If Between(m.nunic,0,255)
- unicodec = Chr(m.nunic)
- Else
- unicodec = ''+Transform(m.nunic)+';'
- Endif
-
- Value = Stuff(m.value,m.posunicode,6,m.unicodec)
-
-
-Enddo
-
-*-----------------------------------
-Function unescapecontrolc( Value )
-*-----------------------------------
-
-If At('\', m.value) = 0
- Return
-Endif
-
-* unescape special characters:
-
-Private aa,elem,unesc
-
-
-Declare aa(1)
-=Alines(m.aa,m.value,18,'\\','\b','\f','\n','\r','\t','\"','\/')
-
-unesc =''
-
-#Define sustb 'bnrt/"'
-#Define sustr Chr(127)+Chr(10)+Chr(13)+Chr(9)+Chr(47)+Chr(34)
-
-For Each elem In m.aa
-
- If ! m.elem == '\\' And Right(m.elem,2) = '\'
- elem = Left(m.elem,Len(m.elem)-2)+Chrtran(Right(m.elem,1),sustb,sustr)
- Endif
-
- unesc = m.unesc+m.elem
-
-Endfor
-
-Value = m.unesc
-
-*--------------------------------------------
-Procedure propertyName(obj)
-*--------------------------------------------
-
-vari=''
-
-Do While ( Right(m.vari,1) # '"' Or ( Right(m.vari,2) = '\"' And Right(m.vari,3) # '\\"' ) ) And Alen(aj) > m.x
- x=m.x+1
- vari = m.vari+aj(m.x)
-Enddo
-
-If Right(m.vari,1) # '"'
- Error ' expecting " received '+ Right(Rtrim(m.vari),1)
-Endif
-
-vari = Left(m.vari,Len(m.vari)-1)
-vari = Iif(Isalpha(m.vari),'','_')+m.vari
-vari = Chrtran( vari, Chrtran( vari, cvalid,'' ) , creem )
-
-If vari = 'tabindex'
- vari = '_tabindex'
-Endif
-
-
-expecting = ':'
-expectingValue = .T.
-expectingPropertyName = .F.
-
-
-*-------------------------------------------------------------
-Procedure procValue(obj)
-*-------------------------------------------------------------
-
-Do Case
-Case aj(m.x) = '{'
-
- oStack.pushObject()
-
- If m.arrayLevel = 0
-
- AddProperty(obj,m.vari,Createobject('empty'))
-
- procstring(obj.&vari)
- expectingPropertyName = .T.
- expecting = ',}'
- expectingValue = .F.
-
- Else
-
- anuevoel(m.obj,m.arrayName,Createobject('empty'),m.bidim,@colpos,@rowpos)
- expectingPropertyName = .F.
- expecting = ',]'
- expectingValue = .T.
-
- Endif
-
-
-Case aj(x) = '['
-
- oStack.pushArray()
-
- Do Case
-
- Case m.arrayLevel = 0
-
- arrayName = Evl(m.vari,'array')
- rowpos = 0
- colpos = 0
- bidim = .F.
-
-#DEFINE EMPTYARRAYFLAG '_EMPTY_ARRAY_FLAG_'
-
- Try
- AddProperty(obj,(m.arrayName+'(1)'),EMPTYARRAYFLAG)
- Catch
- m.arrayName = m.arrayName+'_vfpSafe_'
- AddProperty(obj,(m.arrayName+'(1)'),EMPTYARRAYFLAG)
- Endtry
-
-
- Case m.arrayLevel = 1 And !m.bidim
-
- rowpos = 1
- colpos = 0
- ncols = 1
-
- Dime obj.&arrayName(1,2)
- bidim = .T.
-
- Endcase
-
- arrayLevel = m.arrayLevel+1
-
- vari=''
-
- expecting = Iif(!m.bidim,'[]{',']')
- expectingValue = .T.
- expectingPropertyName = .F.
-
-Otherwise
-
- isstring = aj(m.x)='"'
- x = m.x + Iif(m.isstring,1,0)
-
- Value = ''
-
- Do While .T.
-
- Value = m.value+m.aj(m.x)
-
- If m.isstring
- If Right(m.value,1) = '"' And ( Right(m.value,2) # '\"' Or Right(m.value,3) = '\\' )
- Exit
- Endif
- Else
- If Right(m.value,1) $ '}],' And ( Left(Right(m.value,2),1) # '\' Or Left(Right(Value,3),2) = '\\')
- Exit
- Endif
- Endif
-
- If m.x < Alen(aj)
- x = m.x+1
- Else
- Exit
- Endif
-
- Enddo
-
- closeChar = Right(m.value,1)
-
- Value = Rtrim(m.value,1,m.closeChar)
-
- If Empty(Value) And Not ( m.isstring And m.closeChar = '"' )
- Error 'Expecting value received '+m.closeChar
- Endif
-
- Do Case
-
- Case m.isstring
- If m.closeChar # '"'
- Error 'expecting " received '+m.closeChar
- Endif
-
- Case oStack.isObject() And Not m.closeChar $ ',}'
- Error 'expecting ,} received '+m.closeChar
-
- Case oStack.isArray() And Not m.closeChar $ ',]'
- Error 'expecting ,] received '+m.closeChar
-
- Endcase
-
-
-
- If m.isstring
-
-* don't change this lines sequence!:
- unescunicode(@Value) && 1
- unescapecontrolc(@Value) && 2
- Value = Strtran(m.value,'\\','\') && 3
-
-** check for Json Date:
- If isJsonDt( m.value )
- Value = jsonDateToDT( m.value )
- Endif
-
- Else
-
- Value = Alltrim(m.value)
-
- Do Case
- Case m.value == 'null'
- Value = .Null.
- Case m.value == 'true' Or m.value == 'false'
- Value = Value='true'
- Case Empty(Chrtran(m.value,'-1234567890.E','')) And Occurs('.',m.value) <= 1 And Occurs('-',m.value) <= 1 And Occurs('E',m.value)<=1
- If Not 'E' $ m.value
- Value = Cast( m.value As N( Len(m.value) , Iif(At('.',m.value)>0,Len(m.value)-At( '.',m.value) ,0) ))
- Endif
- Otherwise
- Error 'expecting "|number|null|true|false| received '+aj(m.x)
- Endcase
-
-
- Endif
-
-
- If m.arrayLevel = 0
-
-
- AddProperty(obj,m.vari,m.value)
-
- expecting = '}'
- expectingValue = .F.
- expectingPropertyName = .T.
-
- Else
-
- anuevoel(obj,m.arrayName,m.value,m.bidim,@colpos,@rowpos)
- expecting = ']'
- expectingValue = .T.
- expectingPropertyName = .F.
-
- Endif
-
- expecting = Iif(m.isstring,',','')+m.expecting
-
-
- Do Case
- Case m.closeChar = ']'
- closeArray()
- Case m.closeChar = '}'
- closeObject()
- Endcase
-
-Endcase
-
-
-*------------------------------
-Function closeArray()
-*------------------------------
-
-If oStack.Pop() # 'A'
- Error 'unexpected ] '
-Endif
-
-If m.arrayLevel = 0
- Error 'unexpected ] '
-Endif
-
-arrayLevel = m.arrayLevel-1
-
-If m.arrayLevel = 0
-
- arrayName = ''
- rowpos = 0
- colpos = 0
-
- expecting = Iif(oStack.isObject(),',}','')
- expectingPropertyName = .T.
- expectingValue = .F.
-
-Else
-
- If m.bidim
- rowpos = m.rowpos+1
- colpos = 0
- expecting = ',]['
- Else
- expecting = ',]'
- Endif
-
- expectingValue = .T.
- expectingPropertyName = .F.
-
-Endif
-
-
-
-*-------------------------------------
-Procedure closeObject
-*-------------------------------------
-
-If oStack.Pop() # 'O'
- Error 'unexpected }'
-Endif
-
-If m.arrayLevel = 0
- expecting = ',}'
- expectingValue = .F.
- expectingPropertyName = .T.
- objectOpen = .F.
-Else
- expecting = ',]'
- expectingValue = .T.
- expectingPropertyName = .F.
-Endif
-
-
-*----------------------------------------------
-Function reviveCollection( o )
-*----------------------------------------------
-
-Private All
-
-oConv = Createobject('empty')
-
-nProp = Amembers(elem,m.o,0,'U')
-
-For x = 1 To m.nProp
-
- estaVar = m.elem(x)
-
- esArray = .F.
- esColeccion = Type('m.o.'+m.estaVar) = 'O' And Right( m.estaVar , 14 ) $ '_KV_COLLECTION,_KL_COLLECTION' And Type( 'm.o.'+m.estaVar+'.collectionitems',1) = 'A'
-
- Do Case
- Case m.esColeccion
-
- estaProp = Createobject('collection')
-
- tv = m.o.&estaVar
-
- m.keyValColl = Right( m.estaVar , 14 ) = '_KV_COLLECTION'
-
- For T = 1 To Alen(m.tv.collectionItems)
-
- If m.keyValColl
- esteval = m.tv.collectionItems(m.T).Value
- Else
- esteval = m.tv.collectionItems(m.T)
- ENDIF
-
- IF VARTYPE(m.esteval) = 'C' AND m.esteval = emptyarrayflag
- loop
- ENDIF
-
- If Vartype(m.esteval) = 'O' Or Type('esteVal',1) = 'A'
- esteval = reviveCollection(m.esteval)
- Endif
-
- If m.keyValColl
- estaProp.Add(esteval,m.tv.collectionItems(m.T).Key)
- Else
- estaProp.Add(m.esteval)
- Endif
-
- Endfor
-
- Case Type('m.o.'+m.estaVar,1) = 'A'
-
- esArray = .T.
-
- For T = 1 To Alen(m.o.&estaVar)
-
- Dimension &estaVar(m.T)
-
- If Type('m.o.&estaVar(m.T)') = 'O'
- &estaVar(m.T) = reviveCollection(m.o.&estaVar(m.T))
- Else
- &estaVar(m.T) = m.o.&estaVar(m.T)
- Endif
-
- Endfor
-
- Case Type('m.o.'+estaVar) = 'O'
- estaProp = reviveCollection(m.o.&estaVar)
-
- Otherwise
- estaProp = m.o.&estaVar
-
- Endcase
-
-
- estaVar = Strtran( m.estaVar,'_KV_COLLECTION', '' )
- estaVar = Strtran( m.estaVar, '_KL_COLLECTION', '' )
-
- Do Case
- Case m.esColeccion
- AddProperty(m.oConv,m.estaVar,m.estaProp)
- Case m.esArray
- AddProperty(m.oConv,m.estaVar+'(1)')
- Acopy(&estaVar,m.oConv.&estaVar)
- Otherwise
- AddProperty(m.oConv,m.estaVar,m.estaProp)
- Endcase
-
-Endfor
-
-Try
- retCollection = m.oConv.Collection.BaseClass = 'Collection'
-Catch
- retCollection = .F.
-Endtry
-
-If m.retCollection
- Return m.oConv.Collection
-Else
- Return m.oConv
-Endif
-
-
-*----------------------------------
-Function isJsonDt( cstr )
-*----------------------------------
-Return Iif( Len(m.cstr) = 19 ;
- AND Len(Chrtran(m.cstr,'01234567890:T-','')) = 0 ;
- and Substr(m.cstr,5,1) = '-' ;
- and Substr(m.cstr,8,1) = '-' ;
- and Substr(m.cstr,11,1) = 'T' ;
- and Substr(m.cstr,14,1) = ':' ;
- and Substr(m.cstr,17,1) = ':' ;
- and Occurs('T',m.cstr) = 1 ;
- and Occurs('-',m.cstr) = 2 ;
- and Occurs(':',m.cstr) = 2 ,.T.,.F. )
-
-
-*-----------------------------------
-Procedure jsonDateToDT( cJsonDate )
-*-----------------------------------
-Return Eval("{^"+m.cJsonDate+"}")
-
-
-
-******************************************
-Define Class Stack As Collection
-******************************************
-
-*---------------------------
- Function pushObject()
-*---------------------------
- This.Add('O')
-
-*---------------------------
- Function pushArray()
-*---------------------------
- This.Add('A')
-
-*--------------------------------------
- Function isObject()
-*--------------------------------------
- If This.Count > 0
- Return This.Item( This.Count ) = 'O'
- Else
- Return .F.
- Endif
-
-
-*--------------------------------------
- Function isArray()
-*--------------------------------------
- If This.Count > 0
- Return This.Item( This.Count ) = 'A'
- Else
- Return .F.
- Endif
-
-*----------------------------
- Function Pop()
-*----------------------------
- cret = This.Item( This.Count )
- This.Remove( This.Count )
- Return m.cret
-
-******************************************
-Enddefine
-******************************************
-
-
diff --git a/vfp/regex.prg b/vfp/regex.prg
deleted file mode 100644
index 272ca2e..0000000
--- a/vfp/regex.prg
+++ /dev/null
@@ -1,268 +0,0 @@
-*!* CLEAR
-*!* ?strtranx([ana are 1234567890.1234 lei], [\s\d+\.\d\s], [=TRANSFORM($1, "999 999 999 999.99")])
-*?strtranx([ana are <<1234567890.1234>> lei], [<<], [=TRANSFORM($1, "AA")])
-*!* RETURN
-CLEAR
-
-
-*-- http://www.cornerstonenw.com/article_id_parsing3.htm
-SET STEP ON
-
-lcSourceString = [ana are mere 123,345 678 ad]
-LOCAL laItems[10]
-
-lnResults = GetRegExpAll(lcSourceString, '\d+', @laItems)
-
-SET STEP ON
-RETURN
-strTest = [ab cd2""$$£]
-?strTest
-?StripNonAscii(strTest)
-
-*-- replace non a-z09 with "" case-insensitive
-? strtranx([Ab ra /ca\d&abr'a],"[^a-z0-9]",[],1,,1)
-RETURN
-
-*-- count words
-? OccursRegExp("\b(\w+)\b", [the then quick quick brown fox fox])
-&& prints 7
-
-*-- count repeatedwords
-? OccursRegExp("\b(\w+)\s\1\b", [the then quick quick brown fox fox])
-&& prints 2
-
-
-*-- replace first and second lower-case "a"
-? strtranx([Abracadabra],[a],[*],1,2)
-&& prints Abr*c*dabra
-
-*-- replace first and second "a" case-insensitive
-? strtranx([Abracadabra],[a],[*],1,2,1)
-&& prints *br*cadabra
-
-*-- locate the replacement targets
-? strtranx([Abracadabra],[^a|a$],[*],1,2,0)
-&& Abracadabr*
-? strtranx([Abracadabra],[^a|a$],[*],1,2,1)
-&& *bracadabr*
-
-
-lcText = "The cost, is $123,345.75. "
-*-- convert the commas
-lcText = strtranx( m.lcText, "(\d{1,3})\,(\d{1,}) ","$1 $2" )
-
-*-- convert the decimals
-? strtranx( m.lcText, "(\d{1,3})\.(\d{1,})", "$1,$2" )
-
-** prints "The cost, is $123 345,75."
-
-*-- add 1 to all digits
-? strtranx( [ABC123], "(\d)", [=TRANSFORM(VAL($1)+1)] )
-** prints "ABC234"
-
-*-- convert all dates to long format
-? strtranx( [the date is: 7/18/2004 ] , [(\d{1,2}/\d{1,2}/\d{4})], [=TRANSFORM(CTOD($1),"@YL")])
-** prints "the date is: Sunday, July 18, 2004"
-
-
-*----------------------------------------------------------
-FUNCTION StrtranRegExp( tcSourceString, tcPattern, tcReplace )
- LOCAL loRE
- loRE = CREATEOBJECT("vbscript.regexp")
- WITH loRE
- .PATTERN = tcPattern
- .GLOBAL = .T.
- .multiline = .T.
- RETURN .REPLACE( tcSourceString , tcReplace )
- ENDWITH
-ENDFUNC
-
-*----------------------------------------------------------
-FUNCTION OccursRegExp(tcPattern, tcText)
- LOCAL loRE, loMatches, lnResult
- loRE = CREATEOBJECT("vbscript.regexp")
- WITH loRE
- .PATTERN = m.tcPattern
- .GLOBAL = .T.
- .multiline = .T.
- loMatches = loRE.Execute( m.tcText )
- lnResult = loMatches.COUNT
- loMatches = NULL
- ENDWITH
- RETURN m.lnResult
-ENDFUNC
-
-
-
-*----------------------------------------------------------
-FUNCTION strtranx(tcSearched, ;
- tcSearchFor, ;
- tcReplacement, ;
- tnStart, tnNumber, ;
- tnFlag )
-
- *-- the final version of the UDF
- LOCAL loRE, lcText, lnShift, lcCommand,;
- loMatch, loMatches, lnI, lnK, lcSubMatch,;
- llevaluate, lcMatchDelim, lcReplaceText, lcReplacement,;
- lnStart, lnNumber, loCol, lcKey
-
- IF EMPTY(NVL(tcSearched, ''))
- RETURN NVL(tcSearched, '')
- ENDIF
-
- loRE = CREATEOBJECT("vbscript.regexp")
-
- WITH loRE
- .PATTERN = m.tcSearchFor
- .GLOBAL = .T.
- .multiline = .T.
- .ignorecase = IIF(VARTYPE(m.tnFlag)=[N],m.tnFlag = 1,.F.)
- ENDWITH
-
- lcReplacement = m.tcReplacement
-
- *--- are we evaluating?
- IF m.lcReplacement = [=]
- llevaluate = .T.
- lcReplacement = SUBSTR( m.lcReplacement, 2 )
- ENDIF
-
- IF VARTYPE( m.tnStart )=[N]
- lnStart = m.tnStart
- ELSE
- lnStart = 1
- ENDIF
-
- IF VARTYPE( m.tnNumber) =[N]
- lnNumber = m.tnNumber
- ELSE
- lnNumber = -1
- ENDIF
-
- IF m.lnStart>1 OR m.lnNumber#-1 OR m.llevaluate
-
- lcText = m.tcSearched
- lnShift = 1
- loMatches = loRE.execute( m.lcText )
- loCol = CREATEOBJECT([collection])
- lnNumber = IIF( lnNumber=-1,loMatches.COUNT,MIN(lnNumber,loMatches.COUNT))
-
- FOR lnK = m.lnStart TO m.lnNumber
- loMatch = loMatches.ITEM(m.lnK-1) && zero based
- lcCommand = m.lcReplacement
- FOR lnI= 1 TO loMatch.submatches.COUNT
- lcSubMatch = loMatch.submatches(m.lnI-1) && zero based
- IF m.llevaluate
- * "escape" the string we are about to use in an evaluation.
- * it is important to escape due to possible delim chars (like ", ' etc)
- * malicious content, or VFP line-length violations.
- lcKey = ALLTRIM(TRANSFORM(m.lnK)+[_]+TRANSFORM(m.lnI))
- loCol.ADD( m.lcSubMatch, m.lcKey )
- lcSubMatch = [loCol.item(']+m.lcKey+[')]
- ENDIF
- lcCommand = STRTRAN( m.lcCommand, "$" + ALLTRIM( STR( m.lnI ) ) , m.lcSubMatch)
- ENDFOR
-
- IF m.llevaluate
- TRY
- lcReplaceText = EVALUATE( m.lcCommand )
- CATCH TO loErr
- lcReplaceText="[[ERROR #"+TRANSFORM(loErr.ERRORNO)+[ ]+loErr.MESSAGE+"]]"
- ENDTRY
- ELSE
- lcReplaceText = m.lcCommand
- ENDIF
- lcText = STUFF( m.lcText, loMatch.FirstIndex + m.lnShift, m.loMatch.LENGTH, m.lcReplaceText )
- lnShift = m.lnShift + LEN( m.lcReplaceText ) - m.loMatch.LENGTH
- ENDFOR
- ELSE
- lcText = loRE.REPLACE( m.tcSearched, m.tcReplacement )
- ENDIF
- RETURN m.lcText
-ENDFUNC
-
-*=====================
-FUNCTION StripNonAscii
- LPARAMETERS tcSourceString, tcReplaceString
-
-TEXT TO lcPattern NOSHOW
-[^A-Za-z 0-9 \.,\?'""!@#\$%\^&\*\(\)-_=\+;:<>\/\\\|\}\{\[\]`~]
-ENDTEXT
- lcReplace = IIF(TYPE('tcReplaceString') <> 'C', "", tcReplaceString)
- lcReturn = strtranx( m.tcSourceString, m.lcPattern, m.lcReplace,1,,1)
-
- RETURN m.lcReturn
-ENDFUNC && StripNonAscii
-
-*=====================
-* Intoarce un text care se potriveste cu pattern-ul
-* Ex. Localitatea din textul: STRADA NR LOCALITATE
-*=====================
-FUNCTION GetRegExp
- LPARAMETERS tcSourceString, tcPattern, tnOccurence
-
- * tcSourceString: Bld. Stefan cel Mare 14 Tirgu Neamt
- * tcPattern: [A-Za-z\s]+$ = (caracter sau spatiu) de cel putin o data la sfarsitul liniei = Tirgu Neamt
- * tcPattern: \d+[A-Za-z\s]+$ = oricate cifre (caracter sau spatiu) de cel putin o data la sfarsitul liniei = 14 Tirgu Neamt
-
- LOCAL loRE, loMatches, lcResult, lnOccurence
- lcResult = ''
- lnOccurence = IIF(!EMPTY(m.tnOccurence) and TYPE('tnOccurence') = 'N', m.tnOccurence, 1)
-
- loRE = CREATEOBJECT("vbscript.regexp")
- WITH loRE
- .PATTERN = m.tcPattern
- .GLOBAL = .T.
- .multiline = .T.
- loMatches = loRE.Execute( m.tcSourceString)
- IF loMatches.COUNT >= m.lnOccurence
- lcResult = loMatches.Item(m.lnOccurence - 1).Value
- ENDIF
- loMatches = NULL
- ENDWITH
-
- RETURN m.lcResult
-ENDFUNC && GetRegExp
-
-*=====================
-* Intoarce numarul potrivirilor si un parametru OUT array sau lista de numere facturi separate prin ","
-* Ex. Toate numerele dintr-un text lnMatches = GetRegExpAll(lcSourceString, '\d+', @loMatches)
-*=====================
-FUNCTION GetRegExpAll
- LPARAMETERS tcSourceString, tcPattern, taItems
-
- * tcSourceString: Bld. Stefan cel Mare 14 Tirgu Neamt
- * tcPattern: [A-Za-z\s]+$ = (caracter sau spatiu) de cel putin o data la sfarsitul liniei = Tirgu Neamt
- * tcPattern: \d+[A-Za-z\s]+$ = oricate cifre (caracter sau spatiu) de cel putin o data la sfarsitul liniei = 14 Tirgu Neamt
- * taItems "A">taItems : array cu rezultatele (OUT) taItems[1..Result] sau taItems "C" lista facturi separate prin virgula
- LOCAL loRE, loMatches, lnResults, lnItem
- IF TYPE('taItems') = "A"
- EXTERNAL ARRAY taItems
- ELSE
- taItems = ""
- ENDIF
- lnResult = 0
-
- loRE = CREATEOBJECT("vbscript.regexp")
- WITH loRE
- .PATTERN = m.tcPattern
- .GLOBAL = .T.
- .multiline = .T.
- loMatches = loRE.Execute( m.tcSourceString)
- lnResults = loMatches.COUNT
- IF TYPE('taItems') = "A"
- DIMENSION taItems[m.lnResult]
- FOR lnItem = 1 TO m.lnResult
- taItems[m.lnItem] = loMatches.Item(m.lnItem-1).Value
- ENDFOR
- ELSE
- FOR lnItem = 1 TO m.lnResults
- taItems = taItems + IIF(m.lnItem > 1, ",", "") + loMatches.Item(m.lnItem-1).Value
- ENDFOR
- ENDIF
- loMatches = NULL
- ENDWITH
-
- RETURN m.lnResults
-ENDFUNC && GetRegExp
\ No newline at end of file
diff --git a/vfp/roawebcomenzi.PJT b/vfp/roawebcomenzi.PJT
deleted file mode 100644
index b735713..0000000
Binary files a/vfp/roawebcomenzi.PJT and /dev/null differ
diff --git a/vfp/roawebcomenzi.pjx b/vfp/roawebcomenzi.pjx
deleted file mode 100644
index 3bd90bc..0000000
Binary files a/vfp/roawebcomenzi.pjx and /dev/null differ
diff --git a/vfp/run-gomag.bat b/vfp/run-gomag.bat
deleted file mode 100644
index aab0b56..0000000
--- a/vfp/run-gomag.bat
+++ /dev/null
@@ -1,4 +0,0 @@
-@echo off
-cd /d "%~dp0"
-"C:\Program Files (x86)\Microsoft Visual FoxPro 9\vfp9.exe" -T "%~dp0sync-comenzi-web.prg"
-pause
\ No newline at end of file
diff --git a/vfp/settings.ini.example b/vfp/settings.ini.example
deleted file mode 100644
index 0380db3..0000000
--- a/vfp/settings.ini.example
+++ /dev/null
@@ -1,70 +0,0 @@
-[API]
-ApiBaseUrl=https://api.gomag.ro/api/v1/product/read/json?enabled=1
-OrderApiUrl=https://api.gomag.ro/api/v1/order/read/json
-ApiKey=YOUR_API_KEY_HERE
-ApiShop=https://yourstore.gomag.ro
-UserAgent=Mozilla/5.0
-ContentType=application/json
-
-[PAGINATION]
-Limit=100
-
-[OPTIONS]
-GetProducts=1
-GetOrders=1
-
-[FILTERS]
-OrderDaysBack=7
-
-[ORACLE]
-OracleUser=MARIUSM_AUTO
-OraclePassword=ROMFASTSOFT
-OracleDSN=ROA_CENTRAL
-
-[SYNC]
-AdapterProgram=gomag-adapter.prg
-JsonFilePattern=gomag_orders*.json
-AutoRunAdapter=1
-
-[ROA]
-IdPol=39
-IdGestiune=NULL
-IdSectie=NULL
-
-# ===============================================
-# CONFIGURATIE SYNC COMENZI WEB → ORACLE ROA
-# ===============================================
-#
-# [API] - Configurari pentru GoMag API
-# - ApiKey: Cheia API de la GoMag (OBLIGATORIU)
-# - ApiShop: URL-ul magazinului GoMag (OBLIGATORIU)
-#
-# [OPTIONS]
-# - GetProducts: 1=descarca produse, 0=skip
-# - GetOrders: 1=descarca comenzi, 0=skip
-#
-# [ORACLE] - Conexiune la database ROA
-# - OracleUser: Utilizatorul Oracle (OBLIGATORIU)
-# - OraclePassword: Parola Oracle (OBLIGATORIU)
-# - OracleDSN: Data Source Name (OBLIGATORIU)
-#
-# [SYNC] - Configurari sincronizare
-# - AdapterProgram: Numele programului adapter (ex: gomag-adapter.prg)
-# - JsonFilePattern: Pattern pentru fisiere JSON (ex: gomag_orders*.json)
-# - AutoRunAdapter: 1=ruleaza automat adapter, 0=foloseste doar JSON existent
-#
-# [ROA] - Configurari sistem ROA
-# - IdPol: ID politica de preturi (NULL=fara politica, numar=ID specific)
-# - IdGestiune: ID gestiune pentru comenzi (NULL=automat, numar=ID specific)
-# - IdSectie: ID sectie pentru comenzi (NULL=automat, numar=ID specific)
-#
-# Pentru utilizare:
-# 1. Copiaza settings.ini.example → settings.ini
-# 2. Configureaza ApiKey si ApiShop pentru GoMag
-# 3. Verifica datele Oracle (default: schema MARIUSM_AUTO)
-# 4. Ruleaza sync-comenzi-web.prg
-#
-# Pentru scheduled task Windows:
-# - Creeaza task care ruleaza sync-comenzi-web.prg la interval
-# - Nu mai este nevoie de auto-sync-timer.prg
-# - sync-comenzi-web.prg va apela automat gomag-adapter.prg
\ No newline at end of file
diff --git a/vfp/sync-comenzi-web.prg b/vfp/sync-comenzi-web.prg
deleted file mode 100644
index 054876e..0000000
--- a/vfp/sync-comenzi-web.prg
+++ /dev/null
@@ -1,682 +0,0 @@
-*-- sync-comenzi-web.prg - Orchestrator pentru sincronizarea comenzilor web cu Oracle ROA
-*-- Autor: Claude AI
-*-- Data: 10 septembrie 2025
-*-- Dependency: gomag-vending.prg trebuie rulat mai intai pentru generarea JSON-urilor
-
-Set Safety Off
-Set Century On
-Set Date Dmy
-Set Exact On
-Set Ansi On
-Set Deleted On
-
-*-- Variabile globale
-Private gcAppPath, gcLogFile, gnStartTime, gnOrdersProcessed, gnOrdersSuccess, gnOrdersErrors, gcFailedSKUs
-Private goConnectie, goSettings, goAppSetup, gcStepError
-Local lcJsonPattern, laJsonFiles[1], lnJsonFiles, lnIndex, lcJsonFile
-Local loJsonData, lcJsonContent, lnOrderCount, lnOrderIndex
-Local loOrder, lcResult, llProcessSuccess, lcPath
-
-goConnectie = Null
-gcStepError = ""
-
-*-- Initializare
-gcAppPath = Addbs(Justpath(Sys(16,0)))
-Set Default To (m.gcAppPath)
-
-lcPath = gcAppPath + 'nfjson;'
-Set Path To &lcPath Additive
-
-Set Procedure To utils.prg Additive
-Set Procedure To ApplicationSetup.prg Additive
-Set Procedure To nfjsonread.prg Additive
-Set Procedure To nfjsoncreate.prg Additive
-Set Procedure To regex.prg Additive
-
-*-- Statistici
-gnStartTime = Seconds()
-gnOrdersProcessed = 0
-gnOrdersSuccess = 0
-gnOrdersErrors = 0
-gcFailedSKUs = ""
-
-*-- Initializare logging
-gcLogFile = InitLog("sync_comenzi")
-
-*-- Creare si initializare clasa setup aplicatie
-goAppSetup = Createobject("ApplicationSetup", gcAppPath)
-
-If !goAppSetup.Initialize()
- LogMessage("EROARE: Setup-ul aplicatiei a esuat sau necesita configurare!", "ERROR", gcLogFile)
- Return .F.
-Endif
-
-goSettings = goAppSetup.GetSettings()
-
-*-- Verificare directoare necesare
-If !Directory(gcAppPath + "output")
- LogMessage("EROARE: Directorul output/ nu exista!", "ERROR", gcLogFile)
- Return .F.
-Endif
-
-*-- Rulare automata adapter pentru obtinere comenzi
-If goSettings.AutoRunAdapter
- If !ExecuteAdapter()
- LogMessage("EROARE adapter, continuez cu JSON existente", "WARN", gcLogFile)
- Endif
-Endif
-
-*-- Gasire fisiere JSON comenzi
-lcJsonPattern = gcAppPath + "output\" + goSettings.JsonFilePattern
-lnJsonFiles = Adir(laJsonFiles, lcJsonPattern)
-
-If lnJsonFiles = 0
- LogMessage("Nu au fost gasite fisiere JSON cu comenzi web", "WARN", gcLogFile)
- Return .T.
-Endif
-
-*-- Conectare Oracle
-If !ConnectToOracle()
- LogMessage("EROARE: Nu s-a putut conecta la Oracle ROA", "ERROR", gcLogFile)
- Return .F.
-Endif
-
-*-- Header compact
-LogMessage("SYNC START | " + goSettings.OracleDSN + " " + goSettings.OracleUser + " | " + Transform(lnJsonFiles) + " JSON files", "INFO", gcLogFile)
-
-*-- Procesare fiecare fisier JSON gasit
-For lnIndex = 1 To lnJsonFiles
- lcJsonFile = gcAppPath + "output\" + laJsonFiles[lnIndex, 1]
-
- Try
- lcJsonContent = Filetostr(lcJsonFile)
- If Empty(lcJsonContent)
- Loop
- Endif
-
- loJsonData = nfjsonread(lcJsonContent)
- If Isnull(loJsonData) Or Type('loJsonData') != 'O' Or Type('loJsonData.orders') != 'O'
- LogMessage("EROARE JSON: " + laJsonFiles[lnIndex, 1], "ERROR", gcLogFile)
- Loop
- Endif
-
- Local Array laOrderProps[1]
- lnOrderCount = Amembers(laOrderProps, loJsonData.orders, 0)
-
- *-- Procesare fiecare comanda
- For lnOrderIndex = 1 To lnOrderCount
- Local lcOrderId, loOrder
- lcOrderId = laOrderProps[lnOrderIndex]
- loOrder = Evaluate('loJsonData.orders.' + lcOrderId)
-
- If Type('loOrder') = 'O'
- gnOrdersProcessed = gnOrdersProcessed + 1
-
- llProcessSuccess = ProcessWebOrder(loOrder, lnOrderIndex, lnOrderCount)
-
- If llProcessSuccess
- gnOrdersSuccess = gnOrdersSuccess + 1
- Else
- gnOrdersErrors = gnOrdersErrors + 1
- Endif
- Endif
-
- If m.gnOrdersErrors > 10
- Exit
- Endif
- Endfor
-
- Catch To loError
- LogMessage("EROARE fisier " + laJsonFiles[lnIndex, 1] + ": " + loError.Message, "ERROR", gcLogFile)
- gnOrdersErrors = gnOrdersErrors + 1
- Endtry
-
- If m.gnOrdersErrors > 10
- LogMessage("Peste 10 erori, stop import", "ERROR", gcLogFile)
- Exit
- Endif
-Endfor
-
-*-- Inchidere conexiune Oracle
-DisconnectFromOracle()
-
-*-- Sumar SKU-uri lipsa
-If !Empty(gcFailedSKUs)
- LogMessage("=== SKU-URI LIPSA ===", "INFO", gcLogFile)
- Local lnSkuCount, lnSkuIdx
- Local Array laSkus[1]
- lnSkuCount = Alines(laSkus, gcFailedSKUs, .T., CHR(10))
- For lnSkuIdx = 1 To lnSkuCount
- If !Empty(laSkus[lnSkuIdx])
- LogMessage(Alltrim(laSkus[lnSkuIdx]), "INFO", gcLogFile)
- Endif
- Endfor
- LogMessage("=== SFARSIT SKU-URI LIPSA ===", "INFO", gcLogFile)
-Endif
-
-*-- Footer compact
-Local lcStopped, lnSkuTotal, lnDuration
-lnDuration = Int(Seconds() - gnStartTime)
-lnSkuTotal = Iif(Empty(gcFailedSKUs), 0, Occurs(CHR(10), gcFailedSKUs) + 1)
-lcStopped = Iif(gnOrdersErrors > 10, " (stopped early)", "")
-LogMessage("SYNC END | " + Transform(gnOrdersProcessed) + " processed: " + Transform(gnOrdersSuccess) + " ok, " + Transform(gnOrdersErrors) + " err" + lcStopped + " | " + Transform(lnSkuTotal) + " SKUs lipsa | " + Transform(lnDuration) + "s", "INFO", gcLogFile)
-CloseLog(gnStartTime, 0, gnOrdersProcessed, gcLogFile)
-
-Return .T.
-
-*-- ===================================================================
-*-- HELPER FUNCTIONS
-*-- ===================================================================
-
-*-- Conectare la Oracle
-Function ConnectToOracle
- Local llSuccess, lnHandle
-
- llSuccess = .F.
-
- Try
- lnHandle = SQLConnect(goSettings.OracleDSN, goSettings.OracleUser, goSettings.OraclePassword)
-
- If lnHandle > 0
- goConnectie = lnHandle
- llSuccess = .T.
- Else
- LogMessage("EROARE conectare Oracle: Handle=" + Transform(lnHandle), "ERROR", gcLogFile)
- Endif
-
- Catch To loError
- LogMessage("EROARE conectare Oracle: " + loError.Message, "ERROR", gcLogFile)
- Endtry
-
- Return llSuccess
-Endfunc
-
-*-- Deconectare de la Oracle
-Function DisconnectFromOracle
- If Type('goConnectie') = 'N' And goConnectie > 0
- SQLDisconnect(goConnectie)
- Endif
- Return .T.
-Endfunc
-
-*-- Procesare comanda web - logeaza O SINGURA LINIE per comanda
-*-- Format: [N/Total] OrderNumber P:PartnerID A:AddrFact/AddrLivr -> OK/ERR details
-*-- NOTA: VFP nu permite RETURN in TRY/CATCH, se foloseste flag llContinue
-Function ProcessWebOrder
- Lparameters loOrder, tnIndex, tnTotal
- Local llSuccess, lcOrderNumber, lcOrderDate, lnPartnerID, lcArticlesJSON
- Local lcSQL, lnResult, lcErrorDetails, lnIdComanda
- Local ldOrderDate, loError
- Local lnIdAdresaFacturare, lnIdAdresaLivrare
- Local lcPrefix, lcSummary, lcErrDetail, llContinue
-
- lnIdAdresaLivrare = NULL
- lnIdAdresaFacturare = NULL
- lnIdComanda = 0
- llSuccess = .F.
- llContinue = .T.
- lnPartnerID = 0
- lcOrderNumber = "?"
-
- *-- Prefix: [N/Total] OrderNumber
- lcPrefix = "[" + Transform(tnIndex) + "/" + Transform(tnTotal) + "]"
-
- Try
- *-- Validare comanda
- If !ValidateWebOrder(loOrder)
- LogMessage(lcPrefix + " ? -> ERR VALIDARE: date obligatorii lipsa", "ERROR", gcLogFile)
- llContinue = .F.
- Endif
-
- *-- Extragere date comanda
- If llContinue
- lcOrderNumber = CleanWebText(Transform(loOrder.Number))
- lcOrderDate = ConvertWebDate(loOrder.Date)
- ldOrderDate = String2Date(m.lcOrderDate, 'yyyymmdd')
- lcPrefix = lcPrefix + " " + lcOrderNumber
-
- *-- Procesare partener
- gcStepError = ""
- lnPartnerID = ProcessPartner(loOrder.billing)
- If lnPartnerID <= 0
- LogMessage(lcPrefix + " -> ERR PARTENER: " + Iif(Empty(gcStepError), "nu s-a putut procesa", gcStepError), "ERROR", gcLogFile)
- llContinue = .F.
- Endif
- Endif
-
- *-- Adrese + JSON articole
- If llContinue
- lnIdAdresaFacturare = ProcessAddress(m.lnPartnerID, loOrder.billing)
- If Type('loOrder.shipping') = 'O'
- lnIdAdresaLivrare = ProcessAddress(m.lnPartnerID, loOrder.shipping)
- Endif
-
- lcArticlesJSON = BuildArticlesJSON(loOrder.items)
- If Empty(m.lcArticlesJSON)
- LogMessage(lcPrefix + " P:" + Transform(lnPartnerID) + " -> ERR JSON_ARTICOLE", "ERROR", gcLogFile)
- llContinue = .F.
- Endif
- Endif
-
- *-- Import comanda in Oracle
- If llContinue
- lcSQL = "BEGIN PACK_IMPORT_COMENZI.importa_comanda(?lcOrderNumber, ?ldOrderDate, ?lnPartnerID, ?lcArticlesJSON, ?lnIdAdresaLivrare, ?lnIdAdresaFacturare, ?goSettings.IdPol, ?goSettings.IdSectie, ?@lnIdComanda); END;"
- lnResult = SQLExec(goConnectie, lcSQL)
-
- lcSummary = lcPrefix + " P:" + Transform(lnPartnerID) + ;
- " A:" + Transform(Nvl(lnIdAdresaFacturare, 0)) + "/" + Transform(Nvl(lnIdAdresaLivrare, 0))
-
- If lnResult > 0 And Nvl(m.lnIdComanda, 0) > 0
- LogMessage(lcSummary + " -> OK ID:" + Transform(m.lnIdComanda), "INFO", gcLogFile)
- llSuccess = .T.
- Else
- lcErrorDetails = GetOracleErrorDetails()
- lcErrDetail = ClassifyImportError(lcErrorDetails)
- CollectFailedSKUs(lcErrorDetails)
- LogMessage(lcSummary + " -> ERR " + lcErrDetail, "ERROR", gcLogFile)
- Endif
- Endif
-
- Catch To loError
- LogMessage(lcPrefix + " -> ERR EXCEPTIE: " + loError.Message, "ERROR", gcLogFile)
- Endtry
-
- Return llSuccess
-Endfunc
-
-*-- Validare comanda web
-Function ValidateWebOrder
- Parameters loOrder
- Local llValid
-
- llValid = .T.
-
- If Type('loOrder.number') != 'C' Or Empty(loOrder.Number)
- llValid = .F.
- Endif
-
- If Type('loOrder.date') != 'C' Or Empty(loOrder.Date)
- llValid = .F.
- Endif
-
- If Type('loOrder.billing') != 'O'
- llValid = .F.
- Endif
-
- If Type('loOrder.items') != 'O'
- llValid = .F.
- Endif
-
- Return llValid
-Endfunc
-
-*-- Procesare partener (fara logging, seteaza gcStepError la eroare)
-Function ProcessPartner
- Lparameters toBilling
- Local lcDenumire, lcCodFiscal, lcRegistru, lcAdresa, lcTelefon, lcEmail, lcRegistru
- Local lcSQL, lnResult
- Local lnIdPart, lnIdAdresa, lnIsPersoanaJuridica
- lnIdPart = 0
- lnIdAdresa = 0
- lcDenumire = ''
- lcCodFiscal = ''
- lcRegistru = ''
- lnIsPersoanaJuridica = 0
- lcCodFiscal = Null
-
- Try
- If Type('toBilling.company') = 'O' And !Empty(toBilling.company.Name)
- loCompany = toBilling.company
- lcDenumire = CleanWebText(loCompany.Name)
- lcCodFiscal = Iif(Type('loCompany.code') = 'C', loCompany.Code, Null)
- lcCodFiscal = CleanWebText(m.lcCodFiscal)
- lcRegistru = Iif(Type('loCompany.registrationNo') = 'C', loCompany.registrationNo, Null)
- lcRegistru = CleanWebText(m.lcRegistru)
- lnIsPersoanaJuridica = 1
- Else
- If Type('toBilling.firstname') = 'C'
- lcDenumire = CleanWebText(Alltrim(toBilling.firstname) + " " + Alltrim(toBilling.lastname))
- lnIsPersoanaJuridica = 0
- Endif
- Endif
-
- lcSQL = "BEGIN PACK_IMPORT_PARTENERI.cauta_sau_creeaza_partener(?lcCodFiscal, ?lcDenumire, ?lcRegistru, ?lnIsPersoanaJuridica, ?@lnIdPart); END;"
- lnResult = SQLExec(goConnectie, lcSQL)
-
- If lnResult <= 0
- gcStepError = lcDenumire + " | " + GetOracleErrorDetails()
- Endif
-
- Catch To loError
- gcStepError = loError.Message
- Endtry
-
- Return m.lnIdPart
-Endfunc
-
-*-- Procesare adresa (fara logging)
-Function ProcessAddress
- Lparameters tnIdPart, toAdresa
-
- Local lcAdresa, lcTelefon, lcEmail, lcSQL, lnResult, lnIdAdresa
- lnIdAdresa = 0
-
- Try
- If !Empty(Nvl(m.tnIdPart, 0))
- lcAdresa = FormatAddressForOracle(toAdresa)
- lcTelefon = Iif(Type('toAdresa.phone') = 'C', toAdresa.phone, "")
- lcEmail = Iif(Type('toAdresa.email') = 'C', toAdresa.email, "")
-
- lcSQL = "BEGIN PACK_IMPORT_PARTENERI.cauta_sau_creeaza_adresa(?tnIdPart, ?lcAdresa, ?lcTelefon, ?lcEmail, ?@lnIdAdresa); END;"
- lnResult = SQLExec(goConnectie, lcSQL)
- Endif
-
- Catch To loError
- Endtry
-
- Return m.lnIdAdresa
-Endfunc
-
-
-*-- Construire JSON articole
-Function BuildArticlesJSON
- Lparameters loItems
-
- Local lcJSON, loError
-
- lcJSON = ""
- Try
- lcJSON = nfjsoncreate(loItems)
- Catch To loError
- lcJSON = ""
- Endtry
-
- Return lcJSON
-Endfunc
-
-*-- Curatare text web (HTML entities -> ASCII simplu)
-Function CleanWebText
- Parameters tcText
- Local lcResult
-
- If Empty(tcText) Or Type('tcText') != 'C'
- Return ""
- Endif
-
- lcResult = tcText
-
- lcResult = Strtran(lcResult, 'ă', 'a')
- lcResult = Strtran(lcResult, 'ș', 's')
- lcResult = Strtran(lcResult, 'ț', 't')
- lcResult = Strtran(lcResult, 'î', 'i')
- lcResult = Strtran(lcResult, 'â', 'a')
- lcResult = Strtran(lcResult, '&', '&')
- lcResult = Strtran(lcResult, '<', '<')
- lcResult = Strtran(lcResult, '>', '>')
- lcResult = Strtran(lcResult, '"', '"')
-
- lcResult = Strtran(lcResult, '
', ' ')
- lcResult = Strtran(lcResult, '
', ' ')
- lcResult = Strtran(lcResult, '
', ' ')
-
- lcResult = Strtran(lcResult, '\/', '/')
-
- Return Alltrim(lcResult)
-Endfunc
-
-*-- Conversie data web in format YYYYMMDD
-Function ConvertWebDate
- Parameters tcWebDate
- Local lcResult
-
- If Empty(tcWebDate) Or Type('tcWebDate') != 'C'
- Return Dtos(Date())
- Endif
-
- lcResult = Strtran(Left(tcWebDate, 10), "-", "",1,10,1)
-
- If Len(lcResult) = 8
- Return lcResult
- Else
- Return Dtos(Date())
- Endif
-Endfunc
-
-*-- Conversie string in Date
-Function String2Date
- Lparameters tcDate, tcFormat
-
- Local lcAn, lcDate, lcFormat, lcLuna, lcZi, ldData, lnAn, lnLuna, lnZi, loEx
- ldData = {}
-
- lcDate = m.tcDate
- lcFormat = Iif(!Empty(m.tcFormat), Alltrim(Lower(m.tcFormat)), 'yyyymmdd')
-
- lcDate = Chrtran(m.lcDate, '-/\','...')
- lcDate = Strtran(m.lcDate, '.', '', 1, 2, 1)
- lcFormat = Chrtran(m.lcFormat, '.-/\','...')
- lcFormat = Strtran(m.lcFormat, '.', '', 1, 2, 1)
-
- Do Case
- Case m.lcFormat = 'ddmmyyyy'
- lcAn = Substr(m.tcDate, 5, 4)
- lcLuna = Substr(m.tcDate, 3, 2)
- lcZi = Substr(m.tcDate, 1, 2)
- Otherwise
- lcAn = Substr(m.tcDate, 1, 4)
- lcLuna = Substr(m.tcDate, 5, 2)
- lcZi = Substr(m.tcDate, 7, 2)
- Endcase
- lnAn = Int(Val(m.lcAn))
- lnLuna = Int(Val(m.lcLuna))
- lnZi = Int(Val(m.lcZi))
-
- Try
- ldData = Date(m.lnAn, m.lnLuna, m.lnZi)
- Catch To loEx
- ldData = {}
- Endtry
-
- Return m.ldData
-Endfunc
-
-*-- Formatare adresa in format semicolon pentru Oracle
-Function FormatAddressForOracle
- Parameters loBilling
- Local lcAdresa, lcJudet, lcOras, lcStrada
-
- lcJudet = Iif(Type('loBilling.region') = 'C', CleanWebText(loBilling.Region), "")
- lcOras = Iif(Type('loBilling.city') = 'C', CleanWebText(loBilling.city), "")
- lcStrada = Iif(Type('loBilling.address') = 'C', CleanWebText(loBilling.address), "")
-
- lcAdresa = "JUD:" + lcJudet + ";" + lcOras + ";" + lcStrada
-
- Return lcAdresa
-Endfunc
-
-*-- Construire observatii comanda
-Function BuildOrderObservations
- Parameters loOrder
- Local lcObservatii
-
- lcObservatii = ""
-
- If Type('loOrder.payment') = 'O' And Type('loOrder.payment.name') = 'C'
- lcObservatii = lcObservatii + "Payment: " + CleanWebText(loOrder.Payment.Name) + "; "
- Endif
-
- If Type('loOrder.delivery') = 'O' And Type('loOrder.delivery.name') = 'C'
- lcObservatii = lcObservatii + "Delivery: " + CleanWebText(loOrder.delivery.Name) + "; "
- Endif
-
- If Type('loOrder.status') = 'C'
- lcObservatii = lcObservatii + "Status: " + CleanWebText(loOrder.Status) + "; "
- Endif
-
- If Type('loOrder.source') = 'C'
- lcObservatii = lcObservatii + "Source: " + CleanWebText(loOrder.Source)
-
- If Type('loOrder.sales_channel') = 'C'
- lcObservatii = lcObservatii + " " + CleanWebText(loOrder.sales_channel)
- Endif
- lcObservatii = lcObservatii + "; "
- Endif
-
- If Type('loOrder.shipping') = 'O' And Type('loOrder.billing') = 'O'
- If Type('loOrder.shipping.address') = 'C' And Type('loOrder.billing.address') = 'C'
- If !Alltrim(loOrder.shipping.address) == Alltrim(loOrder.billing.address)
- lcObservatii = lcObservatii + "Shipping: " + CleanWebText(loOrder.shipping.address)
-
- If Type('loOrder.shipping.city') = 'C'
- lcObservatii = lcObservatii + ", " + CleanWebText(loOrder.shipping.city)
- Endif
- lcObservatii = lcObservatii + "; "
- Endif
- Endif
- Endif
-
- If Len(lcObservatii) > 500
- lcObservatii = Left(lcObservatii, 497) + "..."
- Endif
-
- Return lcObservatii
-Endfunc
-
-*-- Obtinere detalii eroare Oracle (single-line, fara SQL)
-Function GetOracleErrorDetails
- Local lcError, laError[1], lnErrorLines, lnIndex
-
- lcError = ""
-
- lnErrorLines = Aerror(laError)
- If lnErrorLines > 0
- For lnIndex = 1 To lnErrorLines
- If lnIndex > 1
- lcError = lcError + " | "
- Endif
- lcError = lcError + Alltrim(Str(laError[lnIndex, 1])) + ": " + laError[lnIndex, 2]
- Endfor
- Endif
-
- If Empty(lcError)
- lcError = "Eroare Oracle nedefinita"
- Endif
-
- *-- Compact: inlocuieste newlines cu spatii
- lcError = Strtran(lcError, CHR(13) + CHR(10), " ")
- lcError = Strtran(lcError, CHR(10), " ")
- lcError = Strtran(lcError, CHR(13), " ")
-
- Return lcError
-Endfunc
-
-*-- Clasifica eroarea Oracle intr-un format compact
-*-- Returneaza: "SKU_NOT_FOUND: sku" / "PRICE_POLICY: sku" / eroarea bruta
-Function ClassifyImportError
- Lparameters tcErrorDetails
- Local lcText, lcSku, lnPos, lcSearch
-
- lcText = Iif(Empty(tcErrorDetails), "", tcErrorDetails)
-
- *-- SKU negasit
- lcSearch = "NOM_ARTICOLE: "
- lnPos = Atc(lcSearch, lcText)
- If lnPos > 0
- lcSku = Alltrim(Getwordnum(Substr(lcText, lnPos + Len(lcSearch)), 1))
- Return "SKU_NOT_FOUND: " + lcSku
- Endif
-
- *-- Eroare adaugare articol (include pretul)
- lcSearch = "Eroare adaugare articol "
- lnPos = Atc(lcSearch, lcText)
- If lnPos > 0
- lcSku = Alltrim(Getwordnum(Substr(lcText, lnPos + Len(lcSearch)), 1))
- Return "PRICE_POLICY: " + lcSku
- Endif
-
- *-- Eroare pret fara SKU (inainte de fix-ul Oracle)
- If Atc("Pretul pentru acest articol", lcText) > 0
- Return "PRICE_POLICY: (SKU necunoscut)"
- Endif
-
- *-- Eroare generica - primele 100 caractere
- Return Left(lcText, 100)
-Endfunc
-
-*-- Colectare SKU-uri lipsa din mesajele de eroare Oracle
-Function CollectFailedSKUs
- Lparameters tcErrorDetails
- Local lcSku, lnPos, lcSearch, lcText
-
- If Empty(tcErrorDetails)
- Return
- Endif
-
- lcText = tcErrorDetails
-
- *-- Pattern 1: "SKU negasit in ARTICOLE_TERTI si NOM_ARTICOLE: XXXXX"
- lcSearch = "NOM_ARTICOLE: "
- lnPos = Atc(lcSearch, lcText)
- If lnPos > 0
- lcSku = Alltrim(Getwordnum(Substr(lcText, lnPos + Len(lcSearch)), 1))
- If !Empty(lcSku)
- AddUniqueSKU(lcSku)
- Endif
- Endif
-
- *-- Pattern 2: "Eroare adaugare articol XXXXX (CODMAT:" sau "Eroare adaugare articol XXXXX:"
- lcSearch = "Eroare adaugare articol "
- lnPos = Atc(lcSearch, lcText)
- If lnPos > 0
- lcSku = Alltrim(Getwordnum(Substr(lcText, lnPos + Len(lcSearch)), 1))
- If !Empty(lcSku)
- AddUniqueSKU(lcSku)
- Endif
- Endif
-
- Return
-Endfunc
-
-*-- Adauga un SKU in gcFailedSKUs daca nu exista deja
-Function AddUniqueSKU
- Lparameters tcSku
- Local lcSku
- lcSku = Alltrim(tcSku)
-
- If Empty(lcSku)
- Return
- Endif
-
- If Empty(gcFailedSKUs)
- gcFailedSKUs = lcSku
- Else
- If !(CHR(10) + lcSku + CHR(10)) $ (CHR(10) + gcFailedSKUs + CHR(10))
- gcFailedSKUs = gcFailedSKUs + CHR(10) + lcSku
- Endif
- Endif
-
- Return
-Endfunc
-
-*-- Executie adapter configurat
-Function ExecuteAdapter
- Local llSuccess, lcAdapterPath
-
- llSuccess = .F.
-
- Try
- lcAdapterPath = gcAppPath + goSettings.AdapterProgram
-
- If File(lcAdapterPath)
- Do (lcAdapterPath)
- llSuccess = .T.
- Else
- LogMessage("EROARE: Adapter negasit: " + lcAdapterPath, "ERROR", gcLogFile)
- Endif
-
- Catch To loError
- LogMessage("EROARE adapter: " + loError.Message, "ERROR", gcLogFile)
- Endtry
-
- Return llSuccess
-Endfunc
diff --git a/vfp/utils.prg b/vfp/utils.prg
deleted file mode 100644
index 42657f5..0000000
--- a/vfp/utils.prg
+++ /dev/null
@@ -1,203 +0,0 @@
-*-- utils.prg - Functii utilitare generale
-*-- Contine doar functii utilitare reutilizabile (INI, HTTP, logging, encoding)
-*-- Autor: Claude AI
-*-- Data: 10 septembrie 2025
-
-*-- Functie pentru citirea fisierelor INI private
-*-- Returneaza valoarea din sectiunea si intrarea specificata sau blank daca nu e gasita
-FUNCTION ReadPini
-PARAMETERS cSection, cEntry, cINIFile
-LOCAL cDefault, cRetVal, nRetLen
-
-cDefault = ""
-cRetVal = SPACE(255)
-nRetLen = LEN(cRetVal)
-
-DECLARE INTEGER GetPrivateProfileString IN WIN32API ;
- STRING cSection, ;
- STRING cEntry, ;
- STRING cDefault, ;
- STRING @cRetVal, ;
- INTEGER nRetLen, ;
- STRING cINIFile
-
-nRetLen = GetPrivateProfileString(cSection, ;
- cEntry, ;
- cDefault, ;
- @cRetVal, ;
- nRetLen, ;
- cINIFile)
-
-RETURN LEFT(cRetVal, nRetLen)
-ENDFUNC
-
-*-- Functie pentru scrierea in fisierele INI private
-*-- Returneaza .T. daca e successful, .F. daca nu
-FUNCTION WritePini
-PARAMETERS cSection, cEntry, cValue, cINIFile
-LOCAL nRetVal
-
-DECLARE INTEGER WritePrivateProfileString IN WIN32API ;
- STRING cSection, ;
- STRING cEntry, ;
- STRING cValue, ;
- STRING cINIFile
-
-nRetVal = WritePrivateProfileString(cSection, ;
- cEntry, ;
- cValue, ;
- cINIFile)
-
-RETURN nRetVal = 1
-ENDFUNC
-
-
-*-- Test conectivitate internet
-FUNCTION TestConnectivity
-LOCAL loHttp, llResult
-
-llResult = .T.
-
-TRY
- loHttp = CREATEOBJECT("WinHttp.WinHttpRequest.5.1")
- loHttp.Open("GET", "https://www.google.com", .F.)
- loHttp.SetTimeouts(5000, 5000, 5000, 5000)
- loHttp.Send()
-
- IF loHttp.Status != 200
- llResult = .F.
- ENDIF
-
-CATCH
- llResult = .F.
-ENDTRY
-
-loHttp = NULL
-RETURN llResult
-ENDFUNC
-
-*-- Functie pentru codificare URL
-FUNCTION UrlEncode
-PARAMETERS tcString
-
-LOCAL lcResult, lcChar, lnI
-
-lcResult = ""
-
-FOR lnI = 1 TO LEN(tcString)
- lcChar = SUBSTR(tcString, lnI, 1)
-
- DO CASE
- CASE ISALPHA(lcChar) OR ISDIGIT(lcChar) OR INLIST(lcChar, "-", "_", ".", "~")
- lcResult = lcResult + lcChar
- OTHERWISE
- lcResult = lcResult + "%" + RIGHT("0" + TRANSFORM(ASC(lcChar), "@0"), 2)
- ENDCASE
-ENDFOR
-
-RETURN lcResult
-ENDFUNC
-
-*-- Functie pentru verificarea existentei fisierului INI
-FUNCTION CheckIniFile
-PARAMETERS cINIFile
-LOCAL llExists
-
-TRY
- llExists = FILE(cINIFile)
-CATCH
- llExists = .F.
-ENDTRY
-
-RETURN llExists
-ENDFUNC
-
-*-- Functie pentru initializarea logging-ului
-FUNCTION InitLog
-PARAMETERS cBaseName
-LOCAL lcLogFile, lcStartTime, lcLogHeader, lcLogDir
-
-*-- Cream directorul log daca nu existe
-lcLogDir = gcAppPath + "log"
-IF !DIRECTORY(lcLogDir)
- MKDIR (lcLogDir)
-ENDIF
-
-*-- Generam numele fisierului log cu timestamp in directorul log
-lcStartTime = DTOS(DATE()) + "_" + STRTRAN(TIME(), ":", "")
-lcLogFile = lcLogDir + "\" + cBaseName + "_" + lcStartTime + ".log"
-
-*-- Header pentru log
-lcLogHeader = "[" + TIME() + "] [START] === GoMag Sync Started ===" + CHR(13) + CHR(10)
-lcLogHeader = lcLogHeader + "[" + TIME() + "] [INFO ] Date: " + DTOC(DATE()) + " | VFP: " + VERSION() + CHR(13) + CHR(10)
-
-*-- Cream fisierul log
-STRTOFILE(lcLogHeader, lcLogFile)
-
-RETURN lcLogFile
-ENDFUNC
-
-*-- Functie pentru logging cu nivel si timestamp
-FUNCTION LogMessage
-PARAMETERS cMessage, cLevel, cLogFile
-LOCAL lcTimeStamp, lcLogEntry, lcExistingContent
-
-*-- Setam nivel implicit daca nu e specificat
-IF EMPTY(cLevel)
- cLevel = "INFO "
-ELSE
- *-- Formatam nivelul pentru a avea 5 caractere
- cLevel = LEFT(cLevel + " ", 5)
-ENDIF
-
-*-- Cream timestamp-ul
-lcTimeStamp = TIME()
-
-*-- Formatam mesajul pentru log
-lcLogEntry = "[" + lcTimeStamp + "] [" + cLevel + "] " + cMessage + CHR(13) + CHR(10)
-
-*-- Adaugam la fisierul existent
-lcExistingContent = ""
-IF FILE(cLogFile)
- lcExistingContent = FILETOSTR(cLogFile)
-ENDIF
-
-STRTOFILE(lcExistingContent + lcLogEntry, cLogFile)
-
-RETURN .T.
-ENDFUNC
-
-*-- Functie pentru inchiderea logging-ului cu statistici
-FUNCTION CloseLog
-PARAMETERS nStartTime, nProductsCount, nOrdersCount, cLogFile
-LOCAL lcEndTime, lcDuration, lcStatsEntry
-
-lcEndTime = TIME()
-*-- Calculam durata in secunde
-nDuration = SECONDS() - nStartTime
-
-*-- Formatam statisticile finale
-lcStatsEntry = "[" + lcEndTime + "] [END ] === GoMag Sync Completed ===" + CHR(13) + CHR(10)
-lcStatsEntry = lcStatsEntry + "[" + lcEndTime + "] [STATS] Duration: " + TRANSFORM(INT(nDuration)) + "s"
-
-IF nProductsCount > 0
- lcStatsEntry = lcStatsEntry + " | Products: " + TRANSFORM(nProductsCount)
-ENDIF
-
-IF nOrdersCount > 0
- lcStatsEntry = lcStatsEntry + " | Orders: " + TRANSFORM(nOrdersCount)
-ENDIF
-
-lcStatsEntry = lcStatsEntry + CHR(13) + CHR(10)
-
-*-- Adaugam la log
-LOCAL lcExistingContent
-lcExistingContent = ""
-IF FILE(cLogFile)
- lcExistingContent = FILETOSTR(cLogFile)
-ENDIF
-
-STRTOFILE(lcExistingContent + lcStatsEntry, cLogFile)
-
-RETURN .T.
-ENDFUNC
\ No newline at end of file