feat(sync): already_imported tracking, invoice cache, path fixes, remove vfp
- Track already_imported/new_imported counts separately in sync_runs and surface them in status API + dashboard last-run card - Cache invoice data in SQLite orders table (factura_* columns); dashboard falls back to Oracle only for uncached imported orders - Resolve JSON_OUTPUT_DIR and SQLITE_DB_PATH relative to known anchored roots in config.py, independent of CWD (fixes WSL2 start) - Use single Oracle connection for entire validation phase (perf) - Batch upsert web_products instead of one-by-one - Remove stale VFP scripts (replaced by gomag-vending.prg workflow) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,7 +22,7 @@ __pycache__/
|
||||
# Settings files with secrets
|
||||
settings.ini
|
||||
vfp/settings.ini
|
||||
vfp/output/
|
||||
output/
|
||||
vfp/*.json
|
||||
*.~pck
|
||||
.claude/HANDOFF.md
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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 '<span class="badge bg-success">Importat</span>';
|
||||
case 'SKIPPED': return '<span class="badge bg-warning text-dark">Omis</span>';
|
||||
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
||||
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
||||
case 'IMPORTED': return '<span class="badge bg-success">Importat</span>';
|
||||
case 'ALREADY_IMPORTED': return '<span class="badge bg-info">Deja importat</span>';
|
||||
case 'SKIPPED': return '<span class="badge bg-warning text-dark">Omis</span>';
|
||||
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
||||
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 '<span class="badge bg-success">Importat</span>';
|
||||
case 'SKIPPED': return '<span class="badge bg-warning text-dark">Omis</span>';
|
||||
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
||||
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
||||
case 'IMPORTED': return '<span class="badge bg-success">Importat</span>';
|
||||
case 'ALREADY_IMPORTED': return '<span class="badge bg-info">Deja importat</span>';
|
||||
case 'SKIPPED': return '<span class="badge bg-warning text-dark">Omis</span>';
|
||||
case 'ERROR': return '<span class="badge bg-danger">Eroare</span>';
|
||||
default: return `<span class="badge bg-secondary">${esc(status)}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 `<option value="${esc(r.run_id)}" ${selected}>${esc(label)}</option>`;
|
||||
}).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]}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
<button type="button" class="btn btn-sm btn-outline-success" onclick="filterOrders('IMPORTED')">
|
||||
Importate <span class="badge bg-light text-dark ms-1" id="countImported">0</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-info" onclick="filterOrders('ALREADY_IMPORTED')">
|
||||
Deja imp. <span class="badge bg-light text-dark ms-1" id="countAlreadyImported">0</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-warning" onclick="filterOrders('SKIPPED')">
|
||||
Omise <span class="badge bg-light text-dark ms-1" id="countSkipped">0</span>
|
||||
</button>
|
||||
|
||||
8
start.sh
8
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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
<CLOB>
|
||||
-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
|
||||
@@ -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<63>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
|
||||
@@ -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
|
||||
******************************************
|
||||
|
||||
|
||||
268
vfp/regex.prg
268
vfp/regex.prg
@@ -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""$$<24>]
|
||||
?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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
@@ -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
|
||||
@@ -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, '<br>', ' ')
|
||||
lcResult = Strtran(lcResult, '<br/>', ' ')
|
||||
lcResult = Strtran(lcResult, '<br />', ' ')
|
||||
|
||||
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
|
||||
203
vfp/utils.prg
203
vfp/utils.prg
@@ -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
|
||||
Reference in New Issue
Block a user