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