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:
2026-03-14 00:15:37 +02:00
parent 8681a92eec
commit 2e65855fe2
24 changed files with 485 additions and 3458 deletions

2
.gitignore vendored
View File

@@ -22,7 +22,7 @@ __pycache__/
# Settings files with secrets
settings.ini
vfp/settings.ini
vfp/output/
output/
vfp/*.json
*.~pck
.claude/HANDOFF.md

View File

@@ -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])

View File

@@ -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()

View File

@@ -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}")

View File

@@ -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 ""

View File

@@ -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

View File

@@ -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}")

View File

@@ -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>`;
}
}

View File

@@ -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]}`);
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
******************************************

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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, '&#259;', 'a')
lcResult = Strtran(lcResult, '&#537;', 's')
lcResult = Strtran(lcResult, '&#539;', 't')
lcResult = Strtran(lcResult, '&#238;', 'i')
lcResult = Strtran(lcResult, '&#226;', 'a')
lcResult = Strtran(lcResult, '&amp;', '&')
lcResult = Strtran(lcResult, '&lt;', '<')
lcResult = Strtran(lcResult, '&gt;', '>')
lcResult = Strtran(lcResult, '&quot;', '"')
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

View File

@@ -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