SC Yazılım olarak, 10 yılı aşkın süredir işletmelere dijital pazarlama, SEO ve marka yönetimi alanlarında sonuç odaklı çözümler sunuyoruz.

LATEST NEWS
iletişim
//// müşteriler pip install python-multipart uvicorn app.main:app –reload –port 8000 pip install python-multipart playwright playwright install {# templates/auto_login.html #} {% extends “base.html” %} {% block title %}Otomatik Giriş{% endblock %} {% block page_title %}Otomatik Giriş{% endblock %} {% block head_extra %} {% endblock %} {% block content %}

Excel’den Otomatik Giriş

Excel dosyasından seçtiğin müşterinin bilgileriyle seçilen domaine tarayıcı açılır, alanlar doldurulur ve “Giriş Yap” tıklanır.

URL: /auto_login
DB Yok
Playwright
{% if error %}
Hata: {{ error }}
Excel yolu: EXCEL_LOGIN_PATH (env) veya app/data/users.xlsx
{% endif %} {% if result %}
Başarılı: {{ result }}
{% endif %} {# targets hiç gelmezse sayfa patlamasın #} {% if targets is not defined or not targets %}
Uyarı: Domain listesi (targets) backend’den gelmedi. Bu yüzden domain seçimi çalışmaz.
main.py içinde TemplateResponse’a targets=... göndermelisin.
{% endif %}
İki domain aynı site. Buradan hangisine giriş yapılacağını seçiyorsun.
Excel’de customer_code / username / password kolonları olmalı. Alternatif: eczane_kodu / kullanici_adi / sifre.
Not: Bu işlem tarayıcıyı (Chromium) açar. Sunucu Windows’ta çalışıyorsa ekranda görürsün. Sunucu farklı makinedeyse, tarayıcı o makinede açılır.
Alanlar
#pharmacyCode, #Customer_username, #Customer_password
Buton
button.Customer_login_button
{% endblock %}
from __future__ import annotations import os import time from pathlib import Path from typing import Dict, List, Optional, Tuple import anyio from fastapi import FastAPI, Request from fastapi.responses import HTMLResponse from starlette.templating import Jinja2Templates # Excel okumak için import openpyxl APP_DIR = Path(__file__).resolve().parent DATA_DIR = APP_DIR / “data” TEMPLATES_DIR = APP_DIR.parent / “templates” # sende templates kökte: Panel-rapor/templates templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) app = FastAPI() # —————————- # AYARLAR # —————————- # Excel dosya yolu (senin ekran görüntüsüne göre: app/data/users.xlsx) DEFAULT_EXCEL_PATH = str(DATA_DIR / “users.xlsx”) EXCEL_LOGIN_PATH = os.getenv(“EXCEL_LOGIN_PATH”, DEFAULT_EXCEL_PATH) # Hedef URL’ler (2 domain / 2 url) # Sen 2 URL göndermiştin; burada env ile yönet: TARGET_1_URL = os.getenv(“TARGET_1_URL”, “https://example1.com/login”) TARGET_2_URL = os.getenv(“TARGET_2_URL”, “https://example2.com/login”) def build_targets() -> Dict[str, str]: # key -> kullanıcıya gözüken url return { “target1”: TARGET_1_URL, “target2”: TARGET_2_URL, } # —————————- # EXCEL OKUMA # —————————- def _norm(s: str) -> str: return (s or “”).strip().lower() def load_users_from_excel(excel_path: str) -> Tuple[List[dict], Optional[str]]: “”” Excel kolonları: Zorunlu: customer_code, username, password Opsiyonel: musteri Alternatif kolon adları: eczane_kodu -> customer_code kullanici_adi -> username sifre -> password “”” p = Path(excel_path) if not p.exists(): return [], f”Excel bulunamadı: {p}” try: wb = openpyxl.load_workbook(p, data_only=True) ws = wb.active rows = list(ws.iter_rows(values_only=True)) if not rows or len(rows) < 2: return [], "Excel boş veya satır yok." header = [str(x).strip() if x is not None else "" for x in rows[0]] header_norm = [_norm(x) for x in header] # kolon index bulucu def col_idx(*names: str) -> Optional[int]: for nm in names: nm2 = _norm(nm) if nm2 in header_norm: return header_norm.index(nm2) return None idx_customer = col_idx(“customer_code”, “eczane_kodu”) idx_username = col_idx(“username”, “kullanici_adi”) idx_password = col_idx(“password”, “sifre”) idx_musteri = col_idx(“musteri”, “müşteri”, “customer”, “name”) missing = [] if idx_customer is None: missing.append(“customer_code (veya eczane_kodu)”) if idx_username is None: missing.append(“username (veya kullanici_adi)”) if idx_password is None: missing.append(“password (veya sifre)”) if missing: return [], “Excel kolonları eksik: ” + “, “.join(missing) users: List[dict] = [] for r in rows[1:]: # güvenli al def cell(i: Optional[int]) -> str: if i is None: return “” v = r[i] if i < len(r) else None if v is None: return "" return str(v).strip() customer_code = cell(idx_customer) username = cell(idx_username) password = cell(idx_password) musteri = cell(idx_musteri) # boş satır atla if not customer_code and not username and not password: continue users.append({ "customer_code": customer_code, "username": username, "password": password, "musteri": musteri, }) if not users: return [], "Excel okundu ama kullanılabilir kullanıcı satırı yok." return users, None except Exception as e: return [], f"Excel okuma hatası: {type(e).__name__}: {e}" # ---------------------------- # PLAYWRIGHT İLE OTOMATİK GİRİŞ # ---------------------------- def run_playwright_login(url: str, customer_code: str, username: str, password: str) -> None: “”” Senin attığın DOM’a göre: #pharmacyCode #Customer_username #Customer_password button.login_submit.login_submit.bg-color-pink.logg_button.Customer_login_button “”” from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=False, slow_mo=60) # GÖRÜNÜR + yavaşlat context = browser.new_context(viewport={“width”: 1200, “height”: 900}) page = context.new_page() page.goto(url, wait_until=”domcontentloaded”, timeout=60000) # Inputlar (birkaç fallback ile) def fill_any(selectors: List[str], value: str) -> None: last_err = None for sel in selectors: try: loc = page.locator(sel) loc.wait_for(state=”visible”, timeout=15000) loc.click(timeout=5000) loc.fill(value, timeout=5000) return except Exception as e: last_err = e raise RuntimeError(f”Input bulunamadı: {selectors}. Hata: {last_err}”) fill_any([“#pharmacyCode”, “input#pharmacyCode”], customer_code) fill_any([“#Customer_username”, “input#Customer_username”], username) fill_any([“#Customer_password”, “input#Customer_password”], password) # Buton tıkla (senin attığın class zinciri + fallback) click_selectors = [ “button.login_submit.login_submit.bg-color-pink.logg_button.Customer_login_button”, “button.Customer_login_button”, “button:has-text(‘Giriş Yap’)”, “text=Giriş Yap”, ] clicked = False last_err = None for sel in click_selectors: try: btn = page.locator(sel) btn.wait_for(state=”visible”, timeout=15000) btn.click(timeout=5000) clicked = True break except Exception as e: last_err = e if not clicked: raise RuntimeError(f”Giriş butonu bulunamadı. Son hata: {last_err}”) # Login sonrası biraz bekle try: page.wait_for_load_state(“networkidle”, timeout=20000) except Exception: pass time.sleep(2) # Tarayıcı açık kalsın istiyorsan kapatma: # browser.close() # Kapatmak istiyorsan aç: # browser.close() # —————————- # ROUTES # —————————- @app.get(“/api/health”) def health(): return {“ok”: True} # /auto_login (sen böyle kullanıyorsun) + /auto-login (template action böyleydi) @app.get(“/auto_login”, response_class=HTMLResponse) @app.get(“/auto-login”, response_class=HTMLResponse) def auto_login_page(request: Request): targets = build_targets() users, err = load_users_from_excel(EXCEL_LOGIN_PATH) # Dropdown düzgün görünsün: # musteri varsa: “musteri — customer_code” # yoksa: “customer_code — username” for u in users: if u.get(“musteri”): u[“label”] = f”{u[‘musteri’]} — {u[‘customer_code’]}” else: u[“label”] = f”{u[‘customer_code’]} — {u[‘username’]}” return templates.TemplateResponse( “auto_login.html”, { “request”: request, “targets”: targets, “users”: users, “error”: err, “result”: None, }, ) @app.post(“/auto_login”, response_class=HTMLResponse) @app.post(“/auto-login”, response_class=HTMLResponse) async def auto_login_submit(request: Request): targets = build_targets() users, err = load_users_from_excel(EXCEL_LOGIN_PATH) form = await request.form() target_key = str(form.get(“target_key”) or “”).strip() selected_customer_code = str(form.get(“selected_customer_code”) or “”).strip() if err: return templates.TemplateResponse( “auto_login.html”, { “request”: request, “targets”: targets, “users”: users, “error”: err, “result”: None, }, ) if target_key not in targets: return templates.TemplateResponse( “auto_login.html”, { “request”: request, “targets”: targets, “users”: users, “error”: “Domain seçimi geçersiz.”, “result”: None, }, ) if not selected_customer_code: return templates.TemplateResponse( “auto_login.html”, { “request”: request, “targets”: targets, “users”: users, “error”: “Lütfen listeden bir müşteri seç.”, “result”: None, }, ) # seçilen kullanıcıyı bul selected = None for u in users: if str(u.get(“customer_code”, “”)).strip() == selected_customer_code: selected = u break if not selected: return templates.TemplateResponse( “auto_login.html”, { “request”: request, “targets”: targets, “users”: users, “error”: “Seçilen customer_code Excel’de bulunamadı.”, “result”: None, }, ) url = targets[target_key] customer_code = selected[“customer_code”] username = selected[“username”] password = selected[“password”] # Tarayıcı işini thread’de çalıştır (FastAPI bloklamasın) try: await anyio.to_thread.run_sync( run_playwright_login, url, customer_code, username, password, ) result_msg = f”Otomatik giriş tetiklendi: {url} | {customer_code}” error_msg = None except Exception as e: result_msg = None error_msg = f”Otomatik giriş başarısız: {type(e).__name__}: {e}” return templates.TemplateResponse( “auto_login.html”, { “request”: request, “targets”: targets, “users”: users, “error”: error_msg, “result”: result_msg, }, ) # app/main.py from __future__ import annotations import os import time from pathlib import Path from datetime import date, datetime, timedelta from typing import Dict, List, Optional from dotenv import load_dotenv from fastapi import FastAPI, Query, Request, Form from fastapi.responses import ( HTMLResponse, JSONResponse, StreamingResponse, RedirectResponse, ) from fastapi.templating import Jinja2Templates from fastapi.staticfiles import StaticFiles from openpyxl import load_workbook, Workbook from openpyxl.utils import get_column_letter # DB helpers from app.db import fetch_all, fetch_one # Router from app.musteriler import router as musteriler_router load_dotenv() app = FastAPI(title=”Panel Rapor”, version=”1.0.0″) # ============================================================================= # PATHS # ============================================================================= APP_DIR = Path(__file__).resolve().parent BASE_DIR = APP_DIR.parent TEMPLATES_DIR = BASE_DIR / “templates” STATIC_DIR = BASE_DIR / “static” DATA_DIR = APP_DIR / “data” DEFAULT_EXCEL_PATH = DATA_DIR / “users.xlsx” # expected correct file name templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) if STATIC_DIR.is_dir(): app.mount(“/static”, StaticFiles(directory=str(STATIC_DIR)), name=”static”) app.include_router(musteriler_router, prefix=”/api/musteriler”, tags=[“Müşteriler”]) # ============================================================================= # AUTO LOGIN CONFIG # ============================================================================= # IMPORTANT: Use login page URL directly (your first one should be /login) TARGETS: Dict[str, str] = { “target1”: “https://esiparisv2.alliance-healthcare.com.tr/login”, “target2”: “https://esiparisv2.alliance-healthcare.com.tr/login”, # put your 2nd URL here if different } # Selectors from your screenshots SEL_PHARMACY = “#pharmacyCode” SEL_USERNAME = “#Customer_username” SEL_PASSWORD = “#Customer_password” SEL_LOGIN_BTN = “button.Customer_login_button” # ✅ the correct one (unique by class) def get_excel_path() -> Path: “”” Excel path priority: 1) ENV: EXCEL_LOGIN_PATH 2) Default: app/data/users.xlsx Also: if user mistakenly created users.xslx, try to auto-detect it. “”” env_path = os.getenv(“EXCEL_LOGIN_PATH”) if env_path: p = Path(env_path).expanduser().resolve() return p # default if DEFAULT_EXCEL_PATH.exists(): return DEFAULT_EXCEL_PATH # common typo: users.xslx typo = DATA_DIR / “users.xslx” if typo.exists(): return typo return DEFAULT_EXCEL_PATH def _norm_header(x: str) -> str: return str(x or “”).strip().lower() def load_users_from_excel(excel_path: Path) -> List[Dict[str, str]]: “”” Reads users from Excel using openpyxl (no pandas needed). Required columns (exact): – customer_code – username – password Optional: – musteri (display name only) Returns: [{musteri, customer_code, username, password}, …] “”” if not excel_path.exists(): return [] # openpyxl supports .xlsx; if someone uses .xls it will fail try: wb = load_workbook(filename=str(excel_path), data_only=True) except Exception: return [] ws = wb.active rows = list(ws.iter_rows(values_only=True)) if not rows: return [] headers = [_norm_header(c) for c in rows[0]] if not headers: return [] # map header -> index idx = {h: i for i, h in enumerate(headers) if h} required = [“customer_code”, “username”, “password”] if not all(k in idx for k in required): return [] out: List[Dict[str, str]] = [] for r in rows[1:]: if not r: continue def getv(key: str) -> str: i = idx.get(key) if i is None or i >= len(r): return “” v = r[i] if v is None: return “” return str(v).strip() customer_code = getv(“customer_code”) username = getv(“username”) password = getv(“password”) musteri = getv(“musteri”) if “musteri” in idx else “” # minimal validation if not customer_code or not username: continue out.append( { “musteri”: musteri, “customer_code”: customer_code, “username”: username, “password”: password, } ) return out def base_context(request: Request) -> Dict: return { “request”: request, “targets”: TARGETS, “db_connected”: True, “excel_path”: str(get_excel_path()), } # ============================================================================= # PLAYWRIGHT AUTO LOGIN # ============================================================================= def run_auto_login(url: str, customer_code: str, username: str, password: str) -> None: “”” Opens browser, fills fields, clicks ONLY the correct “Giriş Yap” button: button.Customer_login_button If you run server on a different machine, browser opens on that machine. “”” # You can control headless via env: # HEADLESS=1 => headless True headless = os.getenv(“HEADLESS”, “0”).strip() == “1” try: from playwright.sync_api import sync_playwright except Exception as e: raise RuntimeError( ‘Playwright is not installed. Run: “pip install playwright” and then: “playwright install”‘ ) from e with sync_playwright() as p: browser = p.chromium.launch(headless=headless) context = browser.new_context() page = context.new_page() # go to login page page.goto(url, wait_until=”domcontentloaded”, timeout=60000) # ensure inputs exist page.wait_for_selector(SEL_PHARMACY, timeout=30000) page.wait_for_selector(SEL_USERNAME, timeout=30000) page.wait_for_selector(SEL_PASSWORD, timeout=30000) # fill page.fill(SEL_PHARMACY, customer_code) page.fill(SEL_USERNAME, username) page.fill(SEL_PASSWORD, password) # ✅ FIX: click the UNIQUE button by class (no strict-mode violation) # If multiple exist for any reason, we click the first visible one. btn = page.locator(SEL_LOGIN_BTN).filter(has_text=”Giriş Yap”) if btn.count() == 0: # fallback: class only (some pages may not include exact text node) btn = page.locator(SEL_LOGIN_BTN) # wait for it to be visible/clickable btn.first.wait_for(state=”visible”, timeout=30000) btn.first.click() # optional: wait a bit for redirect/login page.wait_for_timeout(3000) # keep browser open for a short time if headful (so you see it) keep_open_sec = int(os.getenv(“KEEP_BROWSER_OPEN_SEC”, “2”)) if not headless and keep_open_sec > 0: page.wait_for_timeout(keep_open_sec * 1000) context.close() browser.close() # ============================================================================= # DATE HELPERS # ============================================================================= def _date_start_dt(d: date) -> datetime: return datetime(d.year, d.month, d.day, 0, 0, 0) def _date_end_dt_exclusive(d: date) -> datetime: return datetime(d.year, d.month, d.day, 0, 0, 0) + timedelta(days=1) def _first_day_of_month(dt: datetime) -> datetime: return datetime(dt.year, dt.month, 1, 0, 0, 0) def _add_months(dt: datetime, months: int) -> datetime: y = dt.year + (dt.month – 1 + months) // 12 m = (dt.month – 1 + months) % 12 + 1 return datetime(y, m, 1, 0, 0, 0) # ============================================================================= # SQL (RAPOR 1) # ============================================================================= BASE_INNER_SQL = “”” SELECT ( SELECT MAX(TO_NUMBER(pv.VALUETEXTTUR)) FROM CONTENTCC_C_PROPERTYVALUE pv JOIN CONTENTCC_C_TPROPERTYVALUE tpv ON pv.PROPERTYVALUEID = tpv.PROPERTYVALUEID WHERE pv.PROPERTYID = 19 AND tpv.TITLEID = tl.TITLEID ) AS HEDEFKODU, (pl.NAME || ‘,’ || adr.DISTRICTNAME || ‘,’ || adr.CITYNAME) AS MUSTERI, ppb.DATECREATED, srv.TIMESTARTS AS SERVICETIMESTARTS, ppb.DATEMODIFIED, ( SELECT InvoiceCode FROM ( SELECT i.Code AS InvoiceCode, ROW_NUMBER() OVER (ORDER BY ip.InvoiceID DESC) rn FROM Finance_InvoicePackage ip JOIN Finance_Invoice i ON i.InvoiceID = ip.InvoiceID WHERE ip.TradingPackageID = ppb.PackageID ) WHERE rn = 1 ) AS INVOICECODE, CASE ppb.DELIVERYVALIDATIONTYPE WHEN 1 THEN ‘Kart Okutuldu’ WHEN 0 THEN ‘Kart Okutulmadı’ ELSE NULL END AS DELIVERYVALIDATIONTYPE_TEXT, ppb.BARCODE, CASE ppb.SHIPPINGSTATUS WHEN 0 THEN ‘Sevk Bekliyor’ WHEN 1 THEN ‘Sevk Kontrolü Yapıldı’ WHEN 2 THEN ‘Sevk Başladı’ WHEN 3 THEN ‘Transfer Edildi’ WHEN 4 THEN ‘Teslim Edildi’ WHEN 5 THEN ‘Teslim Edilemedi’ WHEN 99 THEN ‘İptal Edildi’ ELSE NULL END AS SHIPPINGSTATUS_TEXT, CASE srv.ISEXPRESSSERVICE WHEN 1 THEN ‘Hızlı Teslimat’ WHEN 0 THEN ‘Servis’ ELSE NULL END AS SERVICETYPE_TEXT, reg.NAMETUR AS PACKAGEREGIONNAMETUR, srv.NAMETUR AS SERVICENAMETUR, wh.NAMETUR AS PACKAGEWAREHOUSENAMETUR, dep.NAMETUR AS DEPARTUREMANAGERNAMETUR, arr.NAMETUR AS ARRIVALMANAGERNAMETUR, drv.NAME AS SERVICEDRIVERNAME, veh.PLATE AS SERVICEVEHICLEPLATE, ( SELECT DeliveredPoint FROM ( SELECT sp.DeliveredPoint, ROW_NUMBER() OVER (ORDER BY sp.DeliveredTime DESC) rn FROM Transportation_ServicePackage sp WHERE sp.PackagePackBarcodeID = ppb.PackagePackBarcodeID AND sp.IsDelivered = 1 ) WHERE rn = 1 ) AS DELIVEREDLOCATION FROM TRS_PACKAGEPACKBARCODE ppb JOIN TRANSPORTATION_PACKAGE pkg ON pkg.PACKAGEID = ppb.PACKAGEID LEFT JOIN TRANSPORTATION_REGION reg ON reg.REGIONID = pkg.REGIONID LEFT JOIN TRANSPORTATION_SERVICE srv ON srv.SERVICEID = ppb.SERVICEID LEFT JOIN TRANSPORTATION_VEHICLE veh ON veh.VEHICLEID = srv.VEHICLEID LEFT JOIN COLLABORATION_NODE wh ON wh.NODEID = pkg.WAREHOUSEID LEFT JOIN COLLABORATION_NODE dep ON dep.NODEID = ppb.DEPARTUREMANAGERID LEFT JOIN COLLABORATION_NODE arr ON arr.NODEID = ppb.ARRIVALMANAGERID LEFT JOIN COLLABORATION_EMPLOYEE drv ON drv.EMPLOYEEID = srv.DRIVERID LEFT JOIN TRADING_LINK pl ON pl.LINKID = pkg.LINKID INNER JOIN TRADING_LINK tl ON tl.LINKID = pkg.LINKID LEFT JOIN CONTACTCC_CONTACT c ON c.CONTACTID = pl.CONTACTID LEFT JOIN CONTACTCC_ADDRESS adr ON adr.ADDRESSID = c.DEFAULTADDRESSID WHERE pl.NAME NOT LIKE ‘%All%’ AND reg.NAMETUR IS NOT NULL AND reg.NAMETUR <> ‘NULL’ AND ppb.DATECREATED >= :start_date AND ppb.DATECREATED < :end_date """ def filtered_outer_sql(with_order: bool) -> str: where = “”” WHERE 1=1 AND (:barcode IS NULL OR LOWER(t.barcode) LIKE LOWER(:barcode_like)) AND (:invoice IS NULL OR NVL(LOWER(t.invoicecode),”) = LOWER(:invoice)) AND (:status IS NULL OR t.shippingstatus_text = :status) AND (:driver IS NULL OR LOWER(NVL(t.servicedrivername,”)) LIKE LOWER(:driver_like)) AND (:region IS NULL OR LOWER(NVL(t.packageregionnametur,”)) LIKE LOWER(:region_like)) AND (:warehouse IS NULL OR LOWER(NVL(t.packagewarehousenametur,”)) LIKE LOWER(:warehouse_like)) AND (:departure IS NULL OR LOWER(NVL(t.departuremanagernametur,”)) LIKE LOWER(:departure_like)) AND (:card IS NULL OR t.deliveryvalidationtype_text = :card) AND (:service IS NULL OR LOWER(NVL(t.servicenametur,”)) LIKE LOWER(:service_like)) “”” order = ” ORDER BY t.datecreated DESC ” if with_order else “” return f””” SELECT * FROM ( {BASE_INNER_SQL} ) t {where} {order} “”” def count_sql() -> str: return f”SELECT COUNT(1) AS total FROM ( {filtered_outer_sql(with_order=False)} )” def paged_sql() -> str: return f””” SELECT * FROM ( {filtered_outer_sql(with_order=True)} ) OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY “”” def build_params( start: date, end: date, barcode: str | None, invoice: str | None, status: str | None, driver: str | None, region: str | None, warehouse: str | None, departure: str | None, card: str | None, service: str | None, ) -> dict: start_dt = _date_start_dt(start) end_dt = _date_end_dt_exclusive(end) barcode = (barcode or “”).strip() or None invoice = (invoice or “”).strip() or None status = (status or “”).strip() or None driver = (driver or “”).strip() or None region = (region or “”).strip() or None warehouse = (warehouse or “”).strip() or None departure = (departure or “”).strip() or None card = (card or “”).strip() or None service = (service or “”).strip() or None return { “start_date”: start_dt, “end_date”: end_dt, “barcode”: barcode, “barcode_like”: f”{barcode}%” if barcode else None, “invoice”: invoice, “status”: status, “driver”: driver, “driver_like”: f”%{driver}%” if driver else None, “region”: region, “region_like”: f”%{region}%” if region else None, “warehouse”: warehouse, “warehouse_like”: f”%{warehouse}%” if warehouse else None, “departure”: departure, “departure_like”: f”%{departure}%” if departure else None, “card”: card, “service”: service, “service_like”: f”%{service}%” if service else None, } # ============================================================================= # PAGES # ============================================================================= @app.get(“/”, include_in_schema=False) def root(): return RedirectResponse(url=”/rapor”) @app.get(“/rapor”, response_class=HTMLResponse) def rapor_page(request: Request): return templates.TemplateResponse(“rapor.html”, {**base_context(request)}) @app.get(“/rapor2”, response_class=HTMLResponse) def rapor2_page(request: Request): return templates.TemplateResponse(“rapor2.html”, {**base_context(request)}) @app.get(“/rapor_pesin”, response_class=HTMLResponse) def rapor_pesin_page(request: Request): return templates.TemplateResponse(“rapor_pesin.html”, {**base_context(request)}) @app.get(“/musteriler”, response_class=HTMLResponse) def musteriler_page(request: Request): return templates.TemplateResponse(“musteriler.html”, {**base_context(request)}) # ============================================================================= # AUTO LOGIN (GET/POST) + ALIASES # ============================================================================= def _render_auto_login(request: Request, users, error: Optional[str], result: Optional[str]): return templates.TemplateResponse( “auto_login.html”, { **base_context(request), “users”: users, “error”: error, “result”: result, }, ) @app.get(“/auto_login”, response_class=HTMLResponse) @app.get(“/auto-login”, response_class=HTMLResponse) def auto_login_page(request: Request): excel_path = get_excel_path() users = load_users_from_excel(excel_path) error = None if not excel_path.exists(): error = f”Excel not found: {excel_path}” elif len(users) == 0: error = f”Excel loaded but empty or headers mismatch. Required headers: customer_code, username, password. File: {excel_path}” return _render_auto_login(request, users, error, None) @app.post(“/auto_login”, response_class=HTMLResponse) @app.post(“/auto-login”, response_class=HTMLResponse) def auto_login_submit( request: Request, target_key: str = Form(…), selected_customer_code: str = Form(…), ): “”” IMPORTANT: – Form(…) requires: pip install python-multipart “”” excel_path = get_excel_path() users = load_users_from_excel(excel_path) if target_key not in TARGETS: return _render_auto_login(request, users, “Invalid domain selection.”, None) if not excel_path.exists(): return _render_auto_login(request, [], f”Excel not found: {excel_path}”, None) if not users: return _render_auto_login( request, users, “Excel is empty or column headers do not match. Required: customer_code, username, password”, None, ) chosen = next((u for u in users if u[“customer_code”] == selected_customer_code), None) if not chosen: return _render_auto_login(request, users, “Selected customer_code not found in Excel.”, None) url = TARGETS[target_key] try: run_auto_login( url=url, customer_code=chosen[“customer_code”], username=chosen[“username”], password=chosen[“password”], ) except Exception as e: return _render_auto_login(request, users, f”Auto login failed: {str(e)}”, None) label = chosen[“musteri”].strip() if label: result = f”Auto login triggered: {url} | {label} | {chosen[‘customer_code’]} / {chosen[‘username’]}” else: result = f”Auto login triggered: {url} | {chosen[‘customer_code’]} / {chosen[‘username’]}” return _render_auto_login(request, users, None, result) # ============================================================================= # HEALTH # ============================================================================= @app.get(“/api/health”) def health(): try: row = fetch_one(“SELECT 1 AS ok FROM dual”) return {“db”: “ok”, “ok”: row.get(“ok”, 1) if row else 1} except Exception as e: return JSONResponse(status_code=500, content={“db”: “fail”, “error”: str(e)}) # ============================================================================= # RAPOR 1 API # ============================================================================= @app.get(“/api/rapor”) def rapor_data( start: date = Query(default_factory=date.today), end: date = Query(default_factory=date.today), barcode: str | None = None, invoice: str | None = None, status: str | None = None, driver: str | None = None, region: str | None = None, warehouse: str | None = None, departure: str | None = None, card: str | None = None, service: str | None = None, page: int = 1, page_size: int = 50, ): try: page = max(1, page) page_size = min(max(1, page_size), 500) params = build_params(start, end, barcode, invoice, status, driver, region, warehouse, departure, card, service) total_row = fetch_one(count_sql(), params) total = int(total_row[“total”]) if total_row and total_row.get(“total”) is not None else 0 params_items = dict(params) params_items[“offset”] = (page – 1) * page_size params_items[“limit”] = page_size rows = fetch_all(paged_sql(), params_items) kpi_sql = f””” SELECT shippingstatus_text AS durum, COUNT(1) AS adet FROM ( {filtered_outer_sql(with_order=False)} ) GROUP BY shippingstatus_text ORDER BY COUNT(1) DESC “”” kpi_rows = fetch_all(kpi_sql, params) kpis = [] for r in kpi_rows: adet = int(r.get(“adet”) or 0) yuzde = round((adet / total) * 100, 2) if total else 0 kpis.append({“durum”: r.get(“durum”), “adet”: adet, “yuzde”: yuzde}) return { “range”: {“start”: str(start), “end”: str(end)}, “page”: page, “page_size”: page_size, “total”: total, “kpis”: kpis, “rows”: rows, } except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) @app.get(“/api/rapor/group”) def rapor_group( start: date = Query(default_factory=date.today), end: date = Query(default_factory=date.today), barcode: str | None = None, invoice: str | None = None, status: str | None = None, driver: str | None = None, region: str | None = None, warehouse: str | None = None, departure: str | None = None, card: str | None = None, service: str | None = None, group_by: str = “service”, ): try: params = build_params(start, end, barcode, invoice, status, driver, region, warehouse, departure, card, service) group_map = { “service”: “t.servicenametur”, “customer”: “t.musteri”, “region”: “t.packageregionnametur”, “warehouse”: “t.packagewarehousenametur”, “departure”: “t.departuremanagernametur”, “card”: “t.deliveryvalidationtype_text”, “status”: “t.shippingstatus_text”, “driver”: “t.servicedrivername”, } col = group_map.get(group_by, “t.servicenametur”) sql = f””” SELECT NVL({col}, ‘-‘) AS label, COUNT(1) AS adet FROM ( {filtered_outer_sql(with_order=False)} ) t GROUP BY {col} ORDER BY COUNT(1) DESC “”” items_raw = fetch_all(sql, params) total = sum(int(x.get(“adet”) or 0) for x in items_raw) or 0 items = [] for x in items_raw[:200]: adet = int(x.get(“adet”) or 0) yuzde = round((adet / total) * 100, 2) if total else 0 items.append({“label”: x.get(“label”), “adet”: adet, “yuzde”: yuzde}) return {“group_by”: group_by, “total”: total, “items”: items} except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) # ============================================================================= # TREND KPI # ============================================================================= @app.get(“/api/rapor/trend-kpi”) def rapor_trend_kpi( months: int = Query(default=6, ge=1, le=24), start: date | None = None, end: date | None = None, ): try: if start and end: use_start = _date_start_dt(start) use_end = _date_end_dt_exclusive(end) else: now = datetime.now() cur_month = _first_day_of_month(now) start_month = _add_months(cur_month, -(months – 1)) end_month_excl = _add_months(cur_month, 1) use_start = start_month use_end = end_month_excl sql = “”” SELECT TO_CHAR(TRUNC(ppb.DATECREATED, ‘MM’), ‘YYYY-MM’) AS ay, SUM(CASE WHEN ppb.SHIPPINGSTATUS = 4 THEN 1 ELSE 0 END) AS teslim_edildi, SUM(CASE WHEN ppb.SHIPPINGSTATUS = 0 THEN 1 ELSE 0 END) AS sevk_bekliyor, SUM(CASE WHEN ppb.DELIVERYVALIDATIONTYPE = 0 THEN 1 ELSE 0 END) AS kart_okutulmadi FROM TRS_PACKAGEPACKBARCODE ppb WHERE ppb.DATECREATED >= :start_dt AND ppb.DATECREATED < :end_dt GROUP BY TRUNC(ppb.DATECREATED, 'MM') ORDER BY TRUNC(ppb.DATECREATED, 'MM') """ rows = fetch_all(sql, {"start_dt": use_start, "end_dt": use_end}) def pct(curr: int, prev: int): if prev == 0: return None return round(((curr - prev) / prev) * 100, 2) out = [] prev = None for r in rows: cur = { "ay": r.get("ay"), "teslim_edildi": int(r.get("teslim_edildi") or 0), "sevk_bekliyor": int(r.get("sevk_bekliyor") or 0), "kart_okutulmadi": int(r.get("kart_okutulmadi") or 0), } if prev: cur["degisim"] = { "teslim_edildi_pct": pct(cur["teslim_edildi"], prev["teslim_edildi"]), "sevk_bekliyor_pct": pct(cur["sevk_bekliyor"], prev["sevk_bekliyor"]), "kart_okutulmadi_pct": pct(cur["kart_okutulmadi"], prev["kart_okutulmadi"]), } else: cur["degisim"] = { "teslim_edildi_pct": None, "sevk_bekliyor_pct": None, "kart_okutulmadi_pct": None, } out.append(cur) prev = cur return {"months": out} except Exception as e: return JSONResponse(status_code=500, content={"error": str(e)}) @app.get("/api/trend-kpi") def trend_kpi_alias( months: int = Query(default=6, ge=1, le=24), start: date | None = None, end: date | None = None, ): return rapor_trend_kpi(months=months, start=start, end=end) # ============================================================================= # EXCEL EXPORT (RAPOR 1) # ============================================================================= def _make_xlsx(headers: list[str], rows: list[dict], sheet_name: str = "Rapor") -> bytes: wb = Workbook() ws = wb.active ws.title = sheet_name ws.append(headers) for r in rows: ws.append([r.get(h) for h in headers]) for i, h in enumerate(headers, start=1): max_len = len(h) for rr in rows[:300]: val = rr.get(h) if val is None: continue s = str(val) if len(s) > max_len: max_len = len(s) ws.column_dimensions[get_column_letter(i)].width = max(12, min(48, max_len + 2)) import io bio = io.BytesIO() wb.save(bio) return bio.getvalue() @app.get(“/api/rapor/export.xlsx”) def export_xlsx( start: date = Query(default_factory=date.today), end: date = Query(default_factory=date.today), barcode: str | None = None, invoice: str | None = None, status: str | None = None, driver: str | None = None, region: str | None = None, warehouse: str | None = None, departure: str | None = None, card: str | None = None, service: str | None = None, ): try: params = build_params(start, end, barcode, invoice, status, driver, region, warehouse, departure, card, service) sql = filtered_outer_sql(with_order=True) rows = fetch_all(sql, params) headers = [ “datecreated”, “hedefkodu”, “musteri”, “barcode”, “invoicecode”, “shippingstatus_text”, “servicetype_text”, “packageregionnametur”, “servicenametur”, “packagewarehousenametur”, “departuremanagernametur”, “arrivalmanagernametur”, “servicedrivername”, “servicevehicleplate”, “deliveredlocation”, “deliveryvalidationtype_text”, “servicetimestarts”, “datemodified”, ] data = _make_xlsx(headers, rows, sheet_name=”Sevkiyat”) filename = f”sevkiyat_{start}_{end}.xlsx” return StreamingResponse( iter([data]), media_type=”application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”, headers={“Content-Disposition”: f’attachment; filename=”{filename}”‘}, ) except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) @app.get(“/api/export.xlsx”) def export_xlsx_alias( start: date = Query(default_factory=date.today), end: date = Query(default_factory=date.today), barcode: str | None = None, invoice: str | None = None, status: str | None = None, driver: str | None = None, region: str | None = None, warehouse: str | None = None, departure: str | None = None, card: str | None = None, service: str | None = None, ): return export_xlsx( start=start, end=end, barcode=barcode, invoice=invoice, status=status, driver=driver, region=region, warehouse=warehouse, departure=departure, card=card, service=service, ) //// müşteriler WITH CUSTOMERS AS (SELECT Trading_Sales_Customer.LinkID,Trading_Sales_Customer.AccountID,Trading_Sales_Customer.SalesManagerID FROM “TRADING_LINK” INNER JOIN “TRADING_SALES_CUSTOMER” ON “TRADING_LINK”.”LINKID” = “TRADING_SALES_CUSTOMER”.”LINKID” WHERE “TRADING_SALES_CUSTOMER”.”SALESMANAGERID” = :Parameter_2 AND “TRADING_LINK”.”STATUSID” = :Parameter_3)SELECT (CUSTOMERWAREHOUSE.NameTUR) AS WAREHOUSENAME, (Coalesce(UNBILLEDORDER.GROSSTOTAL,0)) AS ORDERTOTAL, ( Case when Trading_Sales_Customer.RiskLimit>-1 Then coalesce(Trading_Sales_Customer.RiskLimit,0)- coalesce(Trading_Sales_Customer.Risk,0) -(Coalesce(UNBILLEDORDER.GROSSTOTAL,0)) Else -1 End ) AS AVAILABLERISKLIMIT, (Coalesce(CUSTOMERDEAL.MAXDAYSDUE,0)) AS DEALDAYSDUE, (Coalesce(CUSTOMERDEAL.MAXRATE,0)) AS DEALRATE, ( COALESCE(( SELECT LISTAGG(CA.NameTUR ,’,’) WITHIN GROUP (ORDER BY CA.NameTUR ) FROM Crm_Audience CA INNER JOIN Crm_AudienceLink CAL ON CA.AudienceID = CAL.AudienceID WHERE CAL.LinkID = Trading_Link.LinkID AND CAL.StatusID = 12 ),’ ‘) ) AS AUDIENCES, (Select CONTENTCC_C_PROPERTYVALUE.ValueTextTUR From CONTENTCC_C_PROPERTYVALUE Inner join CONTENTCC_C_TPROPERTYVALUE on CONTENTCC_C_TPROPERTYVALUE.PropertyValueID=CONTENTCC_C_PROPERTYVALUE.PropertyValueID AND CONTENTCC_C_PROPERTYVALUE.StatusID=12 INNER JOIN ContentCC_Catalog_Title CCT ON CCT.TitleID=CONTENTCC_C_TPROPERTYVALUE.TitleID WHERE CCT.TitleID=TRADING_LINK.TitleID and CONTENTCC_C_PROPERTYVALUE.PropertyID=18 AND ROWNUM<=1 ) AS TITLEPROPERTY18, (Select CONTENTCC_C_PROPERTYVALUE.ValueTextTUR From CONTENTCC_C_PROPERTYVALUE Inner join CONTENTCC_C_TPROPERTYVALUE on CONTENTCC_C_TPROPERTYVALUE.PropertyValueID=CONTENTCC_C_PROPERTYVALUE.PropertyValueID AND CONTENTCC_C_PROPERTYVALUE.StatusID=12 INNER JOIN ContentCC_Catalog_Title CCT ON CCT.TitleID=CONTENTCC_C_TPROPERTYVALUE.TitleID WHERE CCT.TitleID=TRADING_LINK.TitleID and CONTENTCC_C_PROPERTYVALUE.PropertyID=6 AND ROWNUM<=1 ) AS TITLEPROPERTY6, (Select CONTENTCC_C_PROPERTYVALUE.ValueTextTUR From CONTENTCC_C_PROPERTYVALUE Inner join CONTENTCC_C_TPROPERTYVALUE on CONTENTCC_C_TPROPERTYVALUE.PropertyValueID=CONTENTCC_C_PROPERTYVALUE.PropertyValueID AND CONTENTCC_C_PROPERTYVALUE.StatusID=12 INNER JOIN ContentCC_Catalog_Title CCT ON CCT.TitleID=CONTENTCC_C_TPROPERTYVALUE.TitleID WHERE CCT.TitleID=TRADING_LINK.TitleID and CONTENTCC_C_PROPERTYVALUE.PropertyID=19 AND ROWNUM<=1 ) AS TITLEPROPERTY19, (Select CONTENTCC_C_PROPERTYVALUE.ValueTextTUR From CONTENTCC_C_PROPERTYVALUE Inner join CONTENTCC_C_TPROPERTYVALUE on CONTENTCC_C_TPROPERTYVALUE.PropertyValueID=CONTENTCC_C_PROPERTYVALUE.PropertyValueID AND CONTENTCC_C_PROPERTYVALUE.StatusID=12 INNER JOIN ContentCC_Catalog_Title CCT ON CCT.TitleID=CONTENTCC_C_TPROPERTYVALUE.TitleID WHERE CCT.TitleID=TRADING_LINK.TitleID and CONTENTCC_C_PROPERTYVALUE.PropertyID=20 AND ROWNUM<=1 ) AS TITLEPROPERTY20, (Select CONTENTCC_C_PROPERTYVALUE.ValueTextTUR From CONTENTCC_C_PROPERTYVALUE Inner join CONTENTCC_C_TPROPERTYVALUE on CONTENTCC_C_TPROPERTYVALUE.PropertyValueID=CONTENTCC_C_PROPERTYVALUE.PropertyValueID AND CONTENTCC_C_PROPERTYVALUE.StatusID=12 INNER JOIN ContentCC_Catalog_Title CCT ON CCT.TitleID=CONTENTCC_C_TPROPERTYVALUE.TitleID WHERE CCT.TitleID=TRADING_LINK.TitleID and CONTENTCC_C_PROPERTYVALUE.PropertyID=7 AND ROWNUM<=1 ) AS TITLEPROPERTY7, (Select CONTENTCC_C_PROPERTYVALUE.ValueTextTUR From CONTENTCC_C_PROPERTYVALUE Inner join CONTENTCC_C_TPROPERTYVALUE on CONTENTCC_C_TPROPERTYVALUE.PropertyValueID=CONTENTCC_C_PROPERTYVALUE.PropertyValueID AND CONTENTCC_C_PROPERTYVALUE.StatusID=12 INNER JOIN ContentCC_Catalog_Title CCT ON CCT.TitleID=CONTENTCC_C_TPROPERTYVALUE.TitleID WHERE CCT.TitleID=TRADING_LINK.TitleID and CONTENTCC_C_PROPERTYVALUE.PropertyID=8 AND ROWNUM<=1 ) AS TITLEPROPERTY8, "TRADING_LINK".*, "TRADING_SALES_CUSTOMER".*, "LOGISTICSREGION"."NAME" AS LOGISTICSREGIONNAME, "TRADEAGREEMENTTYPE"."NAMETUR" AS TRADEAGREEMENTTYPENAMETUR, "CUSTOMERCOIN"."APPROVEDWONCOIN" AS CUSTOMERCOINAPPROVEDWONCOIN, "DEFAULTUSER"."NAME" AS DEFAULTUSERNAME, "TYPE"."NAME" AS TYPENAME, "SALESTEAM"."NAMETUR" AS SALESTEAMNAMETUR, "SALESCLERK"."NAMETUR" AS SALESCLERKNAMETUR, "STATUS"."NAMETUR" AS STATUSNAMETUR, "DEFAULTSEGMENTATION"."NAMETUR" AS DEFAULTSEGMENTATIONNAMETUR, "PAYMENTOPTION"."NAMETUR" AS PAYMENTOPTIONNAMETUR, "CONTACTDEFAULTEMAIL"."ADDRESS" AS CONTACTDEFAULTEMAILADDRESS, "POS"."NAMETUR" AS POSNAMETUR, "REGION"."NAMETUR" AS REGIONNAMETUR, "CONTACTDEFAULTADDRESS"."COUNTRYNAME" AS CONTACTDEFAULTADDRESSCOUNTRYNAME, "CONTACTDEFAULTADDRESS"."CITYNAME" AS CONTACTDEFAULTADDRESSCITYNAME, "CONTACTDEFAULTADDRESS"."COUNTYNAME" AS CONTACTDEFAULTADDRESSCOUNTYNAME, "CONTACTDEFAULTADDRESS"."DISTRICTNAME" AS CONTACTDEFAULTADDRESSDISTRICTNAME, "CONTACTDEFAULTADDRESS"."BODY" AS CONTACTDEFAULTADDRESSBODY, "CONTACTDEFAULTADDRESS"."LATITUDE" AS CONTACTDEFAULTADDRESSLATITUDE, "CONTACTDEFAULTADDRESS"."LONGITUDE" AS CONTACTDEFAULTADDRESSLONGITUDE, "CONTACTDEFAULTPHONE"."COUNTRYCODE" AS CONTACTDEFAULTPHONECOUNTRYCODE, "CONTACTDEFAULTPHONE"."REGIONCODE" AS CONTACTDEFAULTPHONEREGIONCODE, "CONTACTDEFAULTPHONE"."NUMBER" AS CONTACTDEFAULTPHONENUMBER, "CONTACTDEFAULTPHONE"."EXTENSION" AS CONTACTDEFAULTPHONEEXTENSION FROM "TRADING_LINK" INNER JOIN "TRADING_SALES_CUSTOMER" ON "TRADING_LINK"."LINKID" = "TRADING_SALES_CUSTOMER"."LINKID" LEFT JOIN "LOGISTICS_TRANSPORTATION_REGION" "LOGISTICSREGION" ON "LOGISTICSREGION"."REGIONID" = "TRADING_SALES_CUSTOMER"."LOGISTICSREGIONID" LEFT JOIN "CRM_TRADEAGREEMENTTYPE" "TRADEAGREEMENTTYPE" ON "TRADEAGREEMENTTYPE"."TRADEAGREEMENTTYPEID" = "TRADING_LINK"."TRADEAGREEMENTTYPEID" LEFT JOIN "TRD_SAL_CSTMRCOIN" "CUSTOMERCOIN" ON "CUSTOMERCOIN"."CUSTOMERCOINID" = "TRADING_SALES_CUSTOMER"."CUSTOMERCOINID" LEFT JOIN "NETVISSA_USER" "DEFAULTUSER" ON "DEFAULTUSER"."USERID" = "TRADING_LINK"."DEFAULTUSERID" LEFT JOIN "TRADING_LINKTYPE" "TYPE" ON "TYPE"."LINKTYPEID" = "TRADING_LINK"."TYPEID" LEFT JOIN "TRADING_SALES_SALESTEAM" "SALESTEAM" ON "SALESTEAM"."SALESTEAMID" = "TRADING_SALES_CUSTOMER"."SALESTEAMID" LEFT JOIN "COLLABORATION_NODE" "SALESCLERK" ON "SALESCLERK"."NODEID" = "TRADING_SALES_CUSTOMER"."SALESCLERKID" LEFT JOIN "FRAMEWORK_STATUS" "STATUS" ON "STATUS"."STATUSID" = "TRADING_LINK"."STATUSID" LEFT JOIN "CRM_SEGMENTATION" "DEFAULTSEGMENTATION" ON "DEFAULTSEGMENTATION"."SEGMENTATIONID" = "TRADING_LINK"."DEFAULTSEGMENTATIONID" LEFT JOIN "FINANCE_PAYMENTOPTION" "PAYMENTOPTION" ON "PAYMENTOPTION"."PAYMENTOPTIONID" = "TRADING_LINK"."PAYMENTOPTIONID" LEFT JOIN "CONTACTCC_CONTACT" "CONTACT" ON "CONTACT"."CONTACTID" = "TRADING_LINK"."CONTACTID" LEFT JOIN "CONTACTCC_EMAIL" "CONTACTDEFAULTEMAIL" ON "CONTACTDEFAULTEMAIL"."EMAILID" = "CONTACT"."DEFAULTEMAILID" LEFT JOIN "COLLABORATION_NODE" "POS" ON "POS"."NODEID" = "TRADING_SALES_CUSTOMER"."POSID" LEFT JOIN "TRANSPORTATION_REGION" "REGION" ON "REGION"."REGIONID" = "TRADING_SALES_CUSTOMER"."REGIONID" LEFT JOIN "CONTACTCC_ADDRESS" "CONTACTDEFAULTADDRESS" ON "CONTACTDEFAULTADDRESS"."ADDRESSID" = "CONTACT"."DEFAULTADDRESSID" LEFT JOIN "CONTACTCC_PHONE" "CONTACTDEFAULTPHONE" ON "CONTACTDEFAULTPHONE"."PHONEID" = "CONTACT"."DEFAULTPHONEID" LEFT JOIN ( Select GT.CustomerID,Collaboration_Node.NodeID,Collaboration_Node.NameTUR FROM Collaboration_Node INNER JOIN (Select TRD_SAL_CUSTOMERWAREHOUSE.CustomerID, MAX(TRD_SAL_CUSTOMERWAREHOUSE.TradingWarehouseID) WAREHOUSEID FROM TRD_SAL_CUSTOMERWAREHOUSE INNER JOIN (SELECT CustomerID,MIN (Priority) MINPR FROM TRD_SAL_CUSTOMERWAREHOUSE Group By CustomerID ) GT ON GT.CustomerID=TRD_SAL_CUSTOMERWAREHOUSE.CustomerID AND GT.MINPR=TRD_SAL_CUSTOMERWAREHOUSE.Priority Group by TRD_SAL_CUSTOMERWAREHOUSE.CustomerID) GT ON GT.WAREHOUSEID=Collaboration_Node.NodeID ) CUSTOMERWAREHOUSE ON CUSTOMERWAREHOUSE.CustomerID=Trading_Sales_Customer.LinkID LEFT JOIN ( Select CustomerID,Sum(GROSSTOTAL) GROSSTOTAL FROM ( Select Trading_Sales_SalesOrder.CustomerID, Sum(Coalesce(Trading_Sales_SalesOrder.GrossTotal,0)) GROSSTOTAL From Trading_Sales_SalesOrder Where Trading_Sales_SalesOrder.StatusId In (8,9,103,112) GROUP BY Trading_Sales_SalesOrder.CustomerID UNION ALL Select Trading_Sales_SalesOrder.CustomerID, Sum(Coalesce(TRD_SAL_SALESORDERITEM.GrossTotal,0)) GROSSTOTAL From Trading_Sales_SalesOrder Inner join TRD_SAL_SALESORDERITEM ON TRD_SAL_SALESORDERITEM.SalesOrderID=Trading_Sales_SalesOrder.SalesOrderID Inner join TRD_SAL_SALORDITEMPCKGITEM ON TRD_SAL_SALORDITEMPCKGITEM.SalesOrderItemID=TRD_SAL_SALESORDERITEM.SalesOrderItemID Inner join Transportation_PackageItem ON Transportation_PackageItem.PackageItemID=TRD_SAL_SALORDITEMPCKGITEM.PackageItemID Inner join Transportation_Package ON Transportation_Package.PackageID=Transportation_PackageItem.PackageID Where Trading_Sales_SalesOrder.StatusID=101 AND Trading_Sales_SalesOrder.DateCreated>= TRUNC(SYSDATE) + -30 AND Coalesce(Transportation_PackageItem.InvoiceItemID,0)=0 AND Coalesce(Transportation_PackageItem.QuantityCancelled,0)=0 AND Transportation_Package.StatusID in (45,74) GROUP BY Trading_Sales_SalesOrder.CustomerID) UNBILLEDORDER GROUP BY CustomerID ) UNBILLEDORDER ON UNBILLEDORDER.CustomerID=Trading_Sales_Customer.LinkID LEFT JOIN ( SELECT CD.CustomerID,CD.SalesManagerID , MAX(CASE WHEN CD.Type = 2 THEN CDA.DaysDue ELSE 0 END) MAXDAYSDUE , MAX(CASE WHEN CD.Type IN (1,3,4) THEN CDA.Rate ELSE 0 END) MAXRATE FROM TRD_SAL_CSTMRDEALACTVTY CDA INNER JOIN TRD_SAL_CSTMRDEAL CD ON CDA.CustomerDealID = CD.CustomerDealID WHERE CDA.StatusID = 12 AND SYSDATE BETWEEN CDA.TimeStarts AND CDA.TimeEnds GROUP BY CD.CustomerID, CD.SalesManagerID ) CUSTOMERDEAL ON CUSTOMERDEAL.CustomerID=Trading_Sales_Customer.LinkID AND CUSTOMERDEAL.SalesManagerID=Trading_Sales_Customer.SalesManagerID WHERE “TRADING_SALES_CUSTOMER”.”SALESMANAGERID” = :Parameter_2 AND “TRADING_LINK”.”STATUSID” = :Parameter_3 ORDER BY “TRADING_LINK”.”NAME” ASC DECLARE :Parameter_2 AS Decimal = 9 , :Parameter_3 AS Decimal Sevkiyat Raporu

Cencora

Yükleniyor…

Sevkiyat Raporu

DB kontrol ediliyor…
Filtreler
Filtreleri doldur → Raporu Çalıştır. Excel, filtreli tüm satırları indirir.

Gruplama

Gruplama (2)

DATECREATED HEDEFKODU MUSTERI BARCODE INVOICE DURUM SERVIS_TIPI BOLGE SERVIS WAREHOUSE DEPARTURE ARRIVAL SOFOR ARAC DELIVERED_LOC KART SERVICE_START DATEMODIFIED
Henüz veri yok.
Servet Çelik — Tüm hakları saklıdır.
Cencora Alliance İstanbul
# app/musteriler.py from datetime import date from fastapi import APIRouter from fastapi.responses import JSONResponse, StreamingResponse from openpyxl import Workbook from openpyxl.utils import get_column_letter from app.db import fetch_all, fetch_one router = APIRouter() BASE_SQL = “”” SELECT — TRADING_LINK TL.LINKID AS linkid, TL.CONTACTID AS contactid, TL.TITLEID AS titleid, TL.NAME AS name, TL.DESCRIPTION AS description, TL.STATUSID AS statusid, TL.DATECREATED AS datecreated, TL.DATEMODIFIED AS datemodified, — TRADING_SALES_CUSTOMER TSC.CUSTOMERID AS customerid, TSC.ACCOUNTID AS accountid, TSC.SALESMANAGERID AS salesmanagerid, TSC.SALESCLERKID AS salesclerkid, TSC.SALESCLERK2ID AS salesclerk2id, — ✅ DÜZELTİLDİ: SALETEAMID ❌ -> SALESTEAMID ✅ TSC.SALESTEAMID AS salesteamid, TSC.POSID AS posid, TSC.ISACTIVE AS isactive, TSC.RISK AS risk, TSC.RISKLIMIT AS risklimit, CASE WHEN NVL(TSC.RISKLIMIT,0) > -1 THEN NVL(TSC.RISKLIMIT,0) – NVL(TSC.RISK,0) ELSE -1 END AS availablerisklimit, TSC.LOGISTICSREGIONID AS logisticsregionid, TSC.REGIONID AS regionid, TSC.MAINWAREHOUSEID AS mainwarehouseid, TSC.CUSTOMERCOINID AS customercoinid, TSC.DATEOFRISK AS dateofrisk, TSC.FIRSTORDERDATE AS firstorderdate, TSC.LASTORDERDATE AS lastorderdate, — CONTACT + ADDRESS (doğru zincir) C.DEFAULTADDRESSID AS defaultaddressid, ADR.ADDRESSID AS addressid, ADR.BODY AS body, ADR.AVENUE AS avenue, ADR.PARISH AS parish, ADR.STREET AS street, ADR.”NUMBER” AS number_, ADR.ZIPCODE AS zipcode, ADR.COUNTRYNAME AS countryname, ADR.CITYNAME AS cityname, ADR.COUNTYNAME AS countyname, ADR.DISTRICTNAME AS districtname, ADR.COUNTRYID AS countryid, ADR.CITYID AS cityid, ADR.COUNTYID AS countyid, ADR.DISTRICTID AS districtid, ADR.LATITUDE AS latitude, ADR.LONGITUDE AS longitude FROM TRADING_LINK TL JOIN TRADING_SALES_CUSTOMER TSC ON TSC.LINKID = TL.LINKID LEFT JOIN CONTACTCC_CONTACT C ON C.CONTACTID = TL.CONTACTID LEFT JOIN CONTACTCC_ADDRESS ADR ON ADR.ADDRESSID = C.DEFAULTADDRESSID WHERE 1=1 “”” FILTER_SQL = “”” AND (:sales_manager_id IS NULL OR TSC.SALESMANAGERID = :sales_manager_id) AND (:status_id IS NULL OR TL.STATUSID = :status_id) AND (:name IS NULL OR LOWER(TL.NAME) LIKE LOWER(:name_like)) AND (:city IS NULL OR LOWER(NVL(ADR.CITYNAME,”)) LIKE LOWER(:city_like)) AND (:county IS NULL OR LOWER(NVL(ADR.COUNTYNAME,”)) LIKE LOWER(:county_like)) AND (:district IS NULL OR LOWER(NVL(ADR.DISTRICTNAME,”)) LIKE LOWER(:district_like)) “”” def _sql(with_order: bool) -> str: order = ” ORDER BY TL.NAME ASC ” if with_order else “” return f”{BASE_SQL}\n{FILTER_SQL}\n{order}\n” def _make_xlsx(headers: list[str], rows: list[dict], sheet_name: str) -> bytes: wb = Workbook() ws = wb.active ws.title = sheet_name ws.append(headers) for r in rows: ws.append([r.get(h) for h in headers]) for i, h in enumerate(headers, start=1): max_len = len(h) for rr in rows[:300]: v = rr.get(h) if v is None: continue s = str(v) if len(s) > max_len: max_len = len(s) ws.column_dimensions[get_column_letter(i)].width = max(12, min(48, max_len + 2)) import io bio = io.BytesIO() wb.save(bio) return bio.getvalue() @router.get(“”) def musteriler_liste( sales_manager_id: int | None = None, status_id: int | None = None, name: str | None = None, city: str | None = None, county: str | None = None, district: str | None = None, page: int = 1, page_size: int = 50, ): try: if page < 1: page = 1 if page_size < 1: page_size = 50 if page_size > 500: page_size = 500 name = (name or “”).strip() or None city = (city or “”).strip() or None county = (county or “”).strip() or None district = (district or “”).strip() or None params = { “sales_manager_id”: sales_manager_id, “status_id”: status_id, “name”: name, “name_like”: f”%{name}%” if name else None, “city”: city, “city_like”: f”%{city}%” if city else None, “county”: county, “county_like”: f”%{county}%” if county else None, “district”: district, “district_like”: f”%{district}%” if district else None, } total_row = fetch_one(f”SELECT COUNT(1) AS total FROM ( {_sql(False)} )”, params) total = int(total_row[“total”]) if total_row and total_row.get(“total”) is not None else 0 params_paged = dict(params) params_paged[“offset”] = (page – 1) * page_size params_paged[“limit”] = page_size paged_sql = f””” SELECT * FROM ( {_sql(True)} ) OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY “”” rows = fetch_all(paged_sql, params_paged) return {“page”: page, “page_size”: page_size, “total”: total, “rows”: rows} except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) @router.get(“/export.xlsx”) def musteriler_export( sales_manager_id: int | None = None, status_id: int | None = None, name: str | None = None, city: str | None = None, county: str | None = None, district: str | None = None, ): try: name = (name or “”).strip() or None city = (city or “”).strip() or None county = (county or “”).strip() or None district = (district or “”).strip() or None params = { “sales_manager_id”: sales_manager_id, “status_id”: status_id, “name”: name, “name_like”: f”%{name}%” if name else None, “city”: city, “city_like”: f”%{city}%” if city else None, “county”: county, “county_like”: f”%{county}%” if county else None, “district”: district, “district_like”: f”%{district}%” if district else None, } rows = fetch_all(_sql(True), params) headers = [ “linkid”, “customerid”, “salesmanagerid”, “salesteamid”, “name”, “statusid”, “isactive”, “risk”, “risklimit”, “availablerisklimit”, “datecreated”, “datemodified”, “countryname”, “cityname”, “countyname”, “districtname”, “body”, “avenue”, “parish”, “street”, “number_”, “zipcode”, “defaultaddressid”, “addressid”, “logisticsregionid”, “regionid”, “mainwarehouseid”, ] data = _make_xlsx(headers, rows, “Musteriler”) filename = f”musteriler_{date.today()}.xlsx” return StreamingResponse( iter([data]), media_type=”application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”, headers={“Content-Disposition”: f’attachment; filename=”{filename}”‘}, ) except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) {% extends “base.html” %} {% block title %}Müşteriler Raporu{% endblock %} {% block page_title %}Müşteriler Raporu{% endblock %} {% block nav_musteriler_active %}active{% endblock %} {% block head_extra %} {% endblock %} {% block content %}

Cencora

Yükleniyor…

Müşteriler Raporu
DB kontrol ediliyor…
Filtreler
Filtreleri doldur → Raporu Çalıştır. Excel, filtreli tüm satırları indirir.

Gruplama

Gruplama (2)

MÜŞTERİ DURUM BÖLGE İL İLÇE MAHALLE ADRES RISK_LIMIT RISK FATURASIZ_SIP KULLANILABILIR_LIMIT PROP_19
Henüz veri yok.
Servet Çelik — Tüm hakları saklıdır.
Cencora Alliance İstanbul
{% endblock %} {% block body_extra %} {% block body_extra %} {% endblock %} {% extends “base.html” %} {% block title %}Müşteriler Raporu{% endblock %} {% block page_title %}Müşteriler Raporu{% endblock %} {% block content %}
Loading…
Fetching customers…
Müşteriler Raporu
Toplam: 0
Filtreler
Müşteri adı / kodu / telefon ile ara → Raporu Çalıştır
0 / 0
NAME CUSTOMERID STATUSID CITY COUNTY DISTRICT ADDRESS DATECREATED DATEMODIFIED
Henüz veri yok.
{% endblock %} {% extends “base.html” %} {% block title %}Müşteriler Raporu{% endblock %} {% block page_title %}Müşteriler Raporu{% endblock %} {% block content %}
Loading…
Fetching customers…
Müşteriler Raporu
Toplam: 0
Filtreler
Müşteri adı / kodu / telefon ile ara → Raporu Çalıştır. Excel filtreli indirir.
0 / 0

Gruplama

Özet

Aktif / Pasif / Sayfa içi
NAME CUSTOMERID STATUS CITY COUNTY DISTRICT ADDRESS RISK_LIMIT RISK AVAILABLE_LIMIT DATECREATED DATEMODIFIED
Henüz veri yok.
{% endblock %} {% block scripts %} {% endblock %}

# app/main.py import os from datetime import date, datetime, timedelta from urllib.parse import urljoin from fastapi import FastAPI, Query, Request from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse, RedirectResponse from fastapi.templating import Jinja2Templates from fastapi.staticfiles import StaticFiles from dotenv import load_dotenv from openpyxl import Workbook, load_workbook from openpyxl.utils import get_column_letter from app.db import fetch_all, fetch_one # ✅ EKLENDİ: musteriler router (app/musteriler.py) from app.musteriler import router as musteriler_router load_dotenv() app = FastAPI(title=”Panel Rapor”, version=”1.0.0″) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) TEMPLATES_DIR = os.path.join(BASE_DIR, “templates”) templates = Jinja2Templates(directory=TEMPLATES_DIR) # ✅ STATIC MOUNT (base.html url_for(‘static’, …) için şart) STATIC_DIR = os.path.join(BASE_DIR, “static”) if os.path.isdir(STATIC_DIR): app.mount(“/static”, StaticFiles(directory=STATIC_DIR), name=”static”) # ✅ EKLENDİ: /api/musteriler (musteriler.py router) app.include_router(musteriler_router, prefix=”/api/musteriler”, tags=[“Müşteriler”]) # ============================================================ # ✅ AUTO LOGIN (EXCEL) – DB YOK, python-multipart YOK # ============================================================ AUTO_LOGIN_XLSX = os.getenv(“EXCEL_LOGIN_PATH”, os.path.join(BASE_DIR, “app”, “data”, “users.xlsx”)) # Domain seçimi (whitelist) AUTO_LOGIN_DOMAINS = { “firsatgunleri”: “https://firsatgunleri.alliance-healthcare.com.tr/”, “esiparisv2”: “https://esiparisv2.alliance-healthcare.com.tr/”, } # Login POST ayarları (gerekirse düzelt) # NOT: Bu action path’i ve field name’leri hedef sitenin login formuna göre değişir. # Chrome F12 -> Network -> login isteğinde “Request URL” ve “Form Data”dan bulup burayı düzelt. AUTO_LOGIN_CONFIG = { # iki domain aynı site klon dediğin için tek config kullandım “LOGIN_ACTION”: os.getenv(“AUTO_LOGIN_ACTION”, “/Account/Login”), # örnek, gerekirse değiştir “METHOD”: os.getenv(“AUTO_LOGIN_METHOD”, “POST”), “USER_FIELD”: os.getenv(“AUTO_LOGIN_USER_FIELD”, “Username”), # örnek, gerekirse değiştir “PASS_FIELD”: os.getenv(“AUTO_LOGIN_PASS_FIELD”, “Password”), # örnek, gerekirse değiştir # ekstra hidden alan gerekiyorsa buraya ekle: “EXTRA_FIELDS”: { # “RememberMe”: “true”, # “__RequestVerificationToken”: “…..” # token gerekiyorsa auto-post ile olmayabilir (aşağıda not var) }, } def _safe_domain_key(domain_key: str) -> str: domain_key = (domain_key or “”).strip().lower() if domain_key not in AUTO_LOGIN_DOMAINS: raise ValueError(“Geçersiz domain seçimi.”) return domain_key def _read_users_from_xlsx(xlsx_path: str) -> list[dict]: “”” users.xlsx beklenen kolonlar (başlıklar esnek): – kod / musteri_kodu / hedefkodu / customer_code – kullanici / username / user – sifre / password / pass “”” if not os.path.isfile(xlsx_path): return [] wb = load_workbook(xlsx_path, data_only=True) ws = wb.active rows = list(ws.iter_rows(values_only=True)) if not rows: return [] header = [str(x).strip().lower() if x is not None else “” for x in rows[0]] def find_col(*cands: str) -> int | None: for cand in cands: cand = cand.lower() if cand in header: return header.index(cand) return None c_kod = find_col(“kod”, “musteri_kodu”, “müşteri_kodu”, “hedefkodu”, “customer_code”) c_user = find_col(“kullanici”, “kullanıcı”, “username”, “user”, “mail”, “email”) c_pass = find_col(“sifre”, “şifre”, “password”, “pass”, “parola”) # Eğer başlık yoksa, 1-2-3 kolon varsayımı (kod, user, pass) if c_kod is None and c_user is None and c_pass is None: c_kod, c_user, c_pass = 0, 1, 2 out: list[dict] = [] for r in rows[1:]: if r is None: continue kod = str(r[c_kod]).strip() if c_kod is not None and c_kod < len(r) and r[c_kod] is not None else "" user = str(r[c_user]).strip() if c_user is not None and c_user < len(r) and r[c_user] is not None else "" pw = str(r[c_pass]).strip() if c_pass is not None and c_pass < len(r) and r[c_pass] is not None else "" if kod and user and pw: out.append({"kod": kod, "username": user, "password": pw}) return out def _find_user_cred(kod: str) -> dict | None: kod = (kod or “”).strip() if not kod: return None users = _read_users_from_xlsx(AUTO_LOGIN_XLSX) for u in users: if str(u.get(“kod”)) == kod: return u return None def _date_start_dt(d: date) -> datetime: return datetime(d.year, d.month, d.day, 0, 0, 0) def _date_end_dt_exclusive(d: date) -> datetime: return datetime(d.year, d.month, d.day, 0, 0, 0) + timedelta(days=1) def _first_day_of_month(dt: datetime) -> datetime: return datetime(dt.year, dt.month, 1, 0, 0, 0) def _add_months(dt: datetime, months: int) -> datetime: y = dt.year + (dt.month – 1 + months) // 12 m = (dt.month – 1 + months) % 12 + 1 return datetime(y, m, 1, 0, 0, 0) # ========================= # ✅ YOUR INNER SELECT (RAPOR 1) # ========================= BASE_INNER_SQL = “”” SELECT ( SELECT MAX(TO_NUMBER(pv.VALUETEXTTUR)) FROM CONTENTCC_C_PROPERTYVALUE pv JOIN CONTENTCC_C_TPROPERTYVALUE tpv ON pv.PROPERTYVALUEID = tpv.PROPERTYVALUEID WHERE pv.PROPERTYID = 19 AND tpv.TITLEID = tl.TITLEID ) AS HEDEFKODU, (pl.NAME || ‘,’ || adr.DISTRICTNAME || ‘,’ || adr.CITYNAME) AS MUSTERI, ppb.DATECREATED, srv.TIMESTARTS AS SERVICETIMESTARTS, ppb.DATEMODIFIED, ( SELECT InvoiceCode FROM ( SELECT i.Code AS InvoiceCode, ROW_NUMBER() OVER (ORDER BY ip.InvoiceID DESC) rn FROM Finance_InvoicePackage ip JOIN Finance_Invoice i ON i.InvoiceID = ip.InvoiceID WHERE ip.TradingPackageID = ppb.PackageID ) WHERE rn = 1 ) AS INVOICECODE, CASE ppb.DELIVERYVALIDATIONTYPE WHEN 1 THEN ‘Kart Okutuldu’ WHEN 0 THEN ‘Kart Okutulmadı’ ELSE NULL END AS DELIVERYVALIDATIONTYPE_TEXT, ppb.BARCODE, CASE ppb.SHIPPINGSTATUS WHEN 0 THEN ‘Sevk Bekliyor’ WHEN 1 THEN ‘Sevk Kontrolü Yapıldı’ WHEN 2 THEN ‘Sevk Başladı’ WHEN 3 THEN ‘Transfer Edildi’ WHEN 4 THEN ‘Teslim Edildi’ WHEN 5 THEN ‘Teslim Edilemedi’ WHEN 99 THEN ‘İptal Edildi’ ELSE NULL END AS SHIPPINGSTATUS_TEXT, CASE srv.ISEXPRESSSERVICE WHEN 1 THEN ‘Hızlı Teslimat’ WHEN 0 THEN ‘Servis’ ELSE NULL END AS SERVICETYPE_TEXT, reg.NAMETUR AS PACKAGEREGIONNAMETUR, srv.NAMETUR AS SERVICENAMETUR, wh.NAMETUR AS PACKAGEWAREHOUSENAMETUR, dep.NAMETUR AS DEPARTUREMANAGERNAMETUR, arr.NAMETUR AS ARRIVALMANAGERNAMETUR, drv.NAME AS SERVICEDRIVERNAME, veh.PLATE AS SERVICEVEHICLEPLATE, ( SELECT DeliveredPoint FROM ( SELECT sp.DeliveredPoint, ROW_NUMBER() OVER (ORDER BY sp.DeliveredTime DESC) rn FROM Transportation_ServicePackage sp WHERE sp.PackagePackBarcodeID = ppb.PackagePackBarcodeID AND sp.IsDelivered = 1 ) WHERE rn = 1 ) AS DELIVEREDLOCATION FROM TRS_PACKAGEPACKBARCODE ppb JOIN TRANSPORTATION_PACKAGE pkg ON pkg.PACKAGEID = ppb.PACKAGEID LEFT JOIN TRANSPORTATION_REGION reg ON reg.REGIONID = pkg.REGIONID LEFT JOIN TRANSPORTATION_SERVICE srv ON srv.SERVICEID = ppb.SERVICEID LEFT JOIN TRANSPORTATION_VEHICLE veh ON veh.VEHICLEID = srv.VEHICLEID LEFT JOIN COLLABORATION_NODE wh ON wh.NODEID = pkg.WAREHOUSEID LEFT JOIN COLLABORATION_NODE dep ON dep.NODEID = ppb.DEPARTUREMANAGERID LEFT JOIN COLLABORATION_NODE arr ON arr.NODEID = ppb.ARRIVALMANAGERID LEFT JOIN COLLABORATION_EMPLOYEE drv ON drv.EMPLOYEEID = srv.DRIVERID LEFT JOIN TRADING_LINK pl ON pl.LINKID = pkg.LINKID INNER JOIN TRADING_LINK tl ON tl.LINKID = pkg.LINKID LEFT JOIN CONTACTCC_CONTACT c ON c.CONTACTID = pl.CONTACTID LEFT JOIN CONTACTCC_ADDRESS adr ON adr.ADDRESSID = c.DEFAULTADDRESSID WHERE pl.NAME NOT LIKE ‘%All%’ AND reg.NAMETUR IS NOT NULL AND reg.NAMETUR <> ‘NULL’ AND ppb.DATECREATED >= :start_date AND ppb.DATECREATED < :end_date """ def filtered_outer_sql(with_order: bool) -> str: where = “”” WHERE 1=1 AND (:barcode IS NULL OR LOWER(t.barcode) LIKE LOWER(:barcode_like)) AND (:invoice IS NULL OR NVL(LOWER(t.invoicecode),”) = LOWER(:invoice)) AND (:status IS NULL OR t.shippingstatus_text = :status) AND (:driver IS NULL OR LOWER(NVL(t.servicedrivername,”)) LIKE LOWER(:driver_like)) AND (:region IS NULL OR LOWER(NVL(t.packageregionnametur,”)) LIKE LOWER(:region_like)) AND (:warehouse IS NULL OR LOWER(NVL(t.packagewarehousenametur,”)) LIKE LOWER(:warehouse_like)) AND (:departure IS NULL OR LOWER(NVL(t.departuremanagernametur,”)) LIKE LOWER(:departure_like)) AND (:card IS NULL OR t.deliveryvalidationtype_text = :card) AND (:service IS NULL OR LOWER(NVL(t.servicenametur,”)) LIKE LOWER(:service_like)) “”” order = ” ORDER BY t.datecreated DESC ” if with_order else “” return f””” SELECT * FROM ( {BASE_INNER_SQL} ) t {where} {order} “”” def count_sql() -> str: return f”SELECT COUNT(1) AS total FROM ( {filtered_outer_sql(with_order=False)} )” def paged_sql() -> str: return f””” SELECT * FROM ( {filtered_outer_sql(with_order=True)} ) OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY “”” # ========================= # ✅ PAGES # ========================= @app.get(“/”, include_in_schema=False) def root(): return RedirectResponse(url=”/rapor”) @app.get(“/base”, response_class=HTMLResponse) def base_page(request: Request): return templates.TemplateResponse(“base.html”, {“request”: request}) @app.get(“/rapor”, response_class=HTMLResponse) def rapor_page(request: Request): return templates.TemplateResponse(“rapor.html”, {“request”: request}) @app.get(“/rapor2”, response_class=HTMLResponse) def rapor2_page(request: Request): return templates.TemplateResponse(“rapor2.html”, {“request”: request}) @app.get(“/rapor_pesin”, response_class=HTMLResponse) def rapor_pesin_page(request: Request): return templates.TemplateResponse(“rapor_pesin.html”, {“request”: request}) # ✅ EKLENDİ: Müşteriler sayfası (templates/musteriler.html) @app.get(“/musteriler”, response_class=HTMLResponse) def musteriler_page(request: Request): return templates.TemplateResponse(“musteriler.html”, {“request”: request}) # ============================================================ # ✅ EKLENDİ: AUTO LOGIN SAYFALARI # ============================================================ @app.get(“/auto_login”, response_class=HTMLResponse) def auto_login_page(request: Request): “”” Domain seç + müşteri kodu gir -> /auto_login/go “”” # Exceldeki kodları dropdown yapmak istersen: users = _read_users_from_xlsx(AUTO_LOGIN_XLSX) kodlar = sorted({u[“kod”] for u in users})[:2000] # çoksa sınırladım return templates.TemplateResponse( “auto_login.html”, { “request”: request, “domains”: AUTO_LOGIN_DOMAINS, “kodlar”: kodlar, “xlsx_path”: AUTO_LOGIN_XLSX, }, ) @app.get(“/auto_login/go”, response_class=HTMLResponse) def auto_login_go( request: Request, domain: str, kod: str, ): “”” Excel’den kullanıcı/şifre bulur, seçilen domaine auto-submit yapar. “”” try: domain_key = _safe_domain_key(domain) except Exception as e: return HTMLResponse(f”

Hata

{str(e)}

“, status_code=400) cred = _find_user_cred(kod) if not cred: return HTMLResponse( f”

Kayıt bulunamadı

Excel içinde bu kod yok: {kod}
Dosya: {AUTO_LOGIN_XLSX}

“, status_code=404, ) base_url = AUTO_LOGIN_DOMAINS[domain_key] action = urljoin(base_url, AUTO_LOGIN_CONFIG[“LOGIN_ACTION”]) # HTML: hidden form + otomatik submit user_field = AUTO_LOGIN_CONFIG[“USER_FIELD”] pass_field = AUTO_LOGIN_CONFIG[“PASS_FIELD”] method = (AUTO_LOGIN_CONFIG.get(“METHOD”) or “POST”).upper() extras = AUTO_LOGIN_CONFIG.get(“EXTRA_FIELDS”) or {} html = f””” Otomatik Giriş
AUTO LOGIN
Seçilen Domain: {base_url}
Müşteri Kodu: {cred[“kod”]}
Action: {action}
Birazdan otomatik giriş denenecek…
Eğer login olmuyorsa: hedef sitenin login formundaki action URL ve input name değerlerini F12 → Network üzerinden kontrol edip main.py içindeki AUTO_LOGIN_CONFIG bölümünü güncelle.

Not: Bazı siteler CSRF token istiyorsa (RequestVerificationToken gibi) bu yöntem çalışmayabilir. O durumda önce login sayfasını açıp token alarak (requests ile) session yönetmek gerekir.
“”” for k, v in extras.items(): html += f’\n ‘ html += “””
“”” return HTMLResponse(html) # ========================= # ✅ HEALTH # ========================= @app.get(“/api/health”) def health(): try: row = fetch_one(“SELECT 1 AS ok FROM dual”) return {“db”: “ok”, “ok”: row.get(“ok”, 1) if row else 1} except Exception as e: return JSONResponse(status_code=500, content={“db”: “fail”, “error”: str(e)}) # ———————————————————— # AŞAĞIDAN SONRASI: SENİN MEVCUT RAPOR / EXPORT / RAPOR2 / PEŞİN # KODLARIN AYNEN DEVAM EDİYOR (DEĞİŞTİRMEDİM) # ———————————————————— # (Buradan sonra senin attığın dosyadaki build_params, /api/rapor, /api/rapor2, # /api/rapor_pesin vs. aynı şekilde devam etmeli.) {# templates/auto_login.html #} {% extends “base.html” %} {% block title %}Otomatik Giriş{% endblock %} {% block page_title %}Otomatik Giriş{% endblock %} {# Eğer base.html içinde menü active alanların varsa kendine göre ekleyebilirsin #} {# {% block nav_auto_login_active %}active{% endblock %} #} {% block head_extra %} {% endblock %} {% block content %}

Excel’den Otomatik Giriş

Excel dosyasından seçtiğin müşterinin bilgileriyle seçilen domaine tarayıcı açılır, alanlar doldurulur ve “Giriş Yap” tıklanır.

URL: /auto-login
DB Yok
Playwright
{# Hata / Başarı Mesajları #} {% if error %}
Hata: {{ error }}
Excel yolu: EXCEL_LOGIN_PATH (env) veya /data/login_users.xlsx
{% endif %} {% if result %}
Başarılı: {{ result }}
{% endif %}
İki domain aynı site. Buradan hangisine giriş yapılacağını seçiyorsun.
Excel’de customer_code / username / password kolonları olmalı. Alternatif: eczane_kodu / kullanici_adi / sifre.
Not: Bu işlem tarayıcıyı (Chromium) açar. Sunucu Windows’ta çalışıyorsa ekranda görürsün. Sunucu farklı makinedeyse, tarayıcı o makinede açılır.
Alanlar
#pharmacyCode, #Customer_username, #Customer_password
Buton
button.Customer_login_button
{% endblock %} pip install playwright playwright install
///////////// müşteri end # app/main.py import os from datetime import date, datetime, timedelta from fastapi import FastAPI, Query, Request from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse, RedirectResponse from fastapi.templating import Jinja2Templates from fastapi.staticfiles import StaticFiles from dotenv import load_dotenv from openpyxl import Workbook from openpyxl.utils import get_column_letter from app.db import fetch_all, fetch_one load_dotenv() app = FastAPI(title=”Panel Rapor”, version=”1.0.0″) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) TEMPLATES_DIR = os.path.join(BASE_DIR, “templates”) templates = Jinja2Templates(directory=TEMPLATES_DIR) # ✅ STATIC MOUNT (base.html url_for(‘static’, …) için şart) STATIC_DIR = os.path.join(BASE_DIR, “static”) if os.path.isdir(STATIC_DIR): app.mount(“/static”, StaticFiles(directory=STATIC_DIR), name=”static”) def _date_start_dt(d: date) -> datetime: return datetime(d.year, d.month, d.day, 0, 0, 0) def _date_end_dt_exclusive(d: date) -> datetime: return datetime(d.year, d.month, d.day, 0, 0, 0) + timedelta(days=1) def _first_day_of_month(dt: datetime) -> datetime: return datetime(dt.year, dt.month, 1, 0, 0, 0) def _add_months(dt: datetime, months: int) -> datetime: y = dt.year + (dt.month – 1 + months) // 12 m = (dt.month – 1 + months) % 12 + 1 return datetime(y, m, 1, 0, 0, 0) # ========================= # ✅ YOUR INNER SELECT (RAPOR 1) # ========================= BASE_INNER_SQL = “”” SELECT ( SELECT MAX(TO_NUMBER(pv.VALUETEXTTUR)) FROM CONTENTCC_C_PROPERTYVALUE pv JOIN CONTENTCC_C_TPROPERTYVALUE tpv ON pv.PROPERTYVALUEID = tpv.PROPERTYVALUEID WHERE pv.PROPERTYID = 19 AND tpv.TITLEID = tl.TITLEID ) AS HEDEFKODU, (pl.NAME || ‘,’ || adr.DISTRICTNAME || ‘,’ || adr.CITYNAME) AS MUSTERI, ppb.DATECREATED, srv.TIMESTARTS AS SERVICETIMESTARTS, ppb.DATEMODIFIED, ( SELECT InvoiceCode FROM ( SELECT i.Code AS InvoiceCode, ROW_NUMBER() OVER (ORDER BY ip.InvoiceID DESC) rn FROM Finance_InvoicePackage ip JOIN Finance_Invoice i ON i.InvoiceID = ip.InvoiceID WHERE ip.TradingPackageID = ppb.PackageID ) WHERE rn = 1 ) AS INVOICECODE, CASE ppb.DELIVERYVALIDATIONTYPE WHEN 1 THEN ‘Kart Okutuldu’ WHEN 0 THEN ‘Kart Okutulmadı’ ELSE NULL END AS DELIVERYVALIDATIONTYPE_TEXT, ppb.BARCODE, CASE ppb.SHIPPINGSTATUS WHEN 0 THEN ‘Sevk Bekliyor’ WHEN 1 THEN ‘Sevk Kontrolü Yapıldı’ WHEN 2 THEN ‘Sevk Başladı’ WHEN 3 THEN ‘Transfer Edildi’ WHEN 4 THEN ‘Teslim Edildi’ WHEN 5 THEN ‘Teslim Edilemedi’ WHEN 99 THEN ‘İptal Edildi’ ELSE NULL END AS SHIPPINGSTATUS_TEXT, CASE srv.ISEXPRESSSERVICE WHEN 1 THEN ‘Hızlı Teslimat’ WHEN 0 THEN ‘Servis’ ELSE NULL END AS SERVICETYPE_TEXT, reg.NAMETUR AS PACKAGEREGIONNAMETUR, srv.NAMETUR AS SERVICENAMETUR, wh.NAMETUR AS PACKAGEWAREHOUSENAMETUR, dep.NAMETUR AS DEPARTUREMANAGERNAMETUR, arr.NAMETUR AS ARRIVALMANAGERNAMETUR, drv.NAME AS SERVICEDRIVERNAME, veh.PLATE AS SERVICEVEHICLEPLATE, ( SELECT DeliveredPoint FROM ( SELECT sp.DeliveredPoint, ROW_NUMBER() OVER (ORDER BY sp.DeliveredTime DESC) rn FROM Transportation_ServicePackage sp WHERE sp.PackagePackBarcodeID = ppb.PackagePackBarcodeID AND sp.IsDelivered = 1 ) WHERE rn = 1 ) AS DELIVEREDLOCATION FROM TRS_PACKAGEPACKBARCODE ppb JOIN TRANSPORTATION_PACKAGE pkg ON pkg.PACKAGEID = ppb.PACKAGEID LEFT JOIN TRANSPORTATION_REGION reg ON reg.REGIONID = pkg.REGIONID LEFT JOIN TRANSPORTATION_SERVICE srv ON srv.SERVICEID = ppb.SERVICEID LEFT JOIN TRANSPORTATION_VEHICLE veh ON veh.VEHICLEID = srv.VEHICLEID LEFT JOIN COLLABORATION_NODE wh ON wh.NODEID = pkg.WAREHOUSEID LEFT JOIN COLLABORATION_NODE dep ON dep.NODEID = ppb.DEPARTUREMANAGERID LEFT JOIN COLLABORATION_NODE arr ON arr.NODEID = ppb.ARRIVALMANAGERID LEFT JOIN COLLABORATION_EMPLOYEE drv ON drv.EMPLOYEEID = srv.DRIVERID LEFT JOIN TRADING_LINK pl ON pl.LINKID = pkg.LINKID INNER JOIN TRADING_LINK tl ON tl.LINKID = pkg.LINKID LEFT JOIN CONTACTCC_CONTACT c ON c.CONTACTID = pl.CONTACTID LEFT JOIN CONTACTCC_ADDRESS adr ON adr.ADDRESSID = c.DEFAULTADDRESSID WHERE pl.NAME NOT LIKE ‘%All%’ AND reg.NAMETUR IS NOT NULL AND reg.NAMETUR <> ‘NULL’ AND ppb.DATECREATED >= :start_date AND ppb.DATECREATED < :end_date """ def filtered_outer_sql(with_order: bool) -> str: where = “”” WHERE 1=1 AND (:barcode IS NULL OR LOWER(t.barcode) LIKE LOWER(:barcode_like)) AND (:invoice IS NULL OR NVL(LOWER(t.invoicecode),”) = LOWER(:invoice)) AND (:status IS NULL OR t.shippingstatus_text = :status) AND (:driver IS NULL OR LOWER(NVL(t.servicedrivername,”)) LIKE LOWER(:driver_like)) AND (:region IS NULL OR LOWER(NVL(t.packageregionnametur,”)) LIKE LOWER(:region_like)) AND (:warehouse IS NULL OR LOWER(NVL(t.packagewarehousenametur,”)) LIKE LOWER(:warehouse_like)) AND (:departure IS NULL OR LOWER(NVL(t.departuremanagernametur,”)) LIKE LOWER(:departure_like)) AND (:card IS NULL OR t.deliveryvalidationtype_text = :card) AND (:service IS NULL OR LOWER(NVL(t.servicenametur,”)) LIKE LOWER(:service_like)) “”” order = ” ORDER BY t.datecreated DESC ” if with_order else “” return f””” SELECT * FROM ( {BASE_INNER_SQL} ) t {where} {order} “”” def count_sql() -> str: return f”SELECT COUNT(1) AS total FROM ( {filtered_outer_sql(with_order=False)} )” def paged_sql() -> str: return f””” SELECT * FROM ( {filtered_outer_sql(with_order=True)} ) OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY “”” # ========================= # ✅ PAGES # ========================= @app.get(“/”, include_in_schema=False) def root(): return RedirectResponse(url=”/rapor”) @app.get(“/base”, response_class=HTMLResponse) def base_page(request: Request): return templates.TemplateResponse(“base.html”, {“request”: request}) @app.get(“/rapor”, response_class=HTMLResponse) def rapor_page(request: Request): return templates.TemplateResponse(“rapor.html”, {“request”: request}) @app.get(“/rapor2”, response_class=HTMLResponse) def rapor2_page(request: Request): return templates.TemplateResponse(“rapor2.html”, {“request”: request}) # ✅ YENİ: Peşin Dükkanlar sayfası @app.get(“/rapor_pesin”, response_class=HTMLResponse) def rapor_pesin_page(request: Request): return templates.TemplateResponse(“rapor_pesin.html”, {“request”: request}) # ========================= # ✅ HEALTH # ========================= @app.get(“/api/health”) def health(): try: row = fetch_one(“SELECT 1 AS ok FROM dual”) return {“db”: “ok”, “ok”: row.get(“ok”, 1) if row else 1} except Exception as e: return JSONResponse(status_code=500, content={“db”: “fail”, “error”: str(e)}) def build_params( start: date, end: date, barcode: str | None, invoice: str | None, status: str | None, driver: str | None, region: str | None, warehouse: str | None, departure: str | None, card: str | None, service: str | None, ) -> dict: start_dt = _date_start_dt(start) end_dt = _date_end_dt_exclusive(end) barcode = (barcode or “”).strip() or None invoice = (invoice or “”).strip() or None status = (status or “”).strip() or None driver = (driver or “”).strip() or None region = (region or “”).strip() or None warehouse = (warehouse or “”).strip() or None departure = (departure or “”).strip() or None card = (card or “”).strip() or None service = (service or “”).strip() or None return { “start_date”: start_dt, “end_date”: end_dt, “barcode”: barcode, “barcode_like”: f”{barcode}%” if barcode else None, “invoice”: invoice, “status”: status, “driver”: driver, “driver_like”: f”%{driver}%” if driver else None, “region”: region, “region_like”: f”%{region}%” if region else None, “warehouse”: warehouse, “warehouse_like”: f”%{warehouse}%” if warehouse else None, “departure”: departure, “departure_like”: f”%{departure}%” if departure else None, “card”: card, “service”: service, “service_like”: f”%{service}%” if service else None, } # ========================= # ✅ RAPOR 1 API # ========================= @app.get(“/api/rapor”) def rapor_data( start: date = Query(default_factory=date.today), end: date = Query(default_factory=date.today), barcode: str | None = None, invoice: str | None = None, status: str | None = None, driver: str | None = None, region: str | None = None, warehouse: str | None = None, departure: str | None = None, card: str | None = None, service: str | None = None, page: int = 1, page_size: int = 50, ): try: if page < 1: page = 1 if page_size < 1: page_size = 50 if page_size > 500: page_size = 500 params = build_params(start, end, barcode, invoice, status, driver, region, warehouse, departure, card, service) total_row = fetch_one(count_sql(), params) total = int(total_row[“total”]) if total_row and total_row.get(“total”) is not None else 0 params_items = dict(params) params_items[“offset”] = (page – 1) * page_size params_items[“limit”] = page_size rows = fetch_all(paged_sql(), params_items) kpi_sql = f””” SELECT shippingstatus_text AS durum, COUNT(1) AS adet FROM ( {filtered_outer_sql(with_order=False)} ) GROUP BY shippingstatus_text ORDER BY COUNT(1) DESC “”” kpi_rows = fetch_all(kpi_sql, params) kpis = [] for r in kpi_rows: adet = int(r.get(“adet”) or 0) yuzde = round((adet / total) * 100, 2) if total else 0 kpis.append({“durum”: r.get(“durum”), “adet”: adet, “yuzde”: yuzde}) return { “range”: {“start”: str(start), “end”: str(end)}, “page”: page, “page_size”: page_size, “total”: total, “kpis”: kpis, “rows”: rows, } except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) @app.get(“/api/rapor/group”) def rapor_group( start: date = Query(default_factory=date.today), end: date = Query(default_factory=date.today), barcode: str | None = None, invoice: str | None = None, status: str | None = None, driver: str | None = None, region: str | None = None, warehouse: str | None = None, departure: str | None = None, card: str | None = None, service: str | None = None, group_by: str = “service”, ): try: params = build_params(start, end, barcode, invoice, status, driver, region, warehouse, departure, card, service) group_map = { “service”: “t.servicenametur”, “customer”: “t.musteri”, “region”: “t.packageregionnametur”, “warehouse”: “t.packagewarehousenametur”, “departure”: “t.departuremanagernametur”, “card”: “t.deliveryvalidationtype_text”, “status”: “t.shippingstatus_text”, “driver”: “t.servicedrivername”, } col = group_map.get(group_by, “t.servicenametur”) sql = f””” SELECT NVL({col}, ‘-‘) AS label, COUNT(1) AS adet FROM ( {filtered_outer_sql(with_order=False)} ) t GROUP BY {col} ORDER BY COUNT(1) DESC “”” items_raw = fetch_all(sql, params) total = sum(int(x.get(“adet”) or 0) for x in items_raw) or 0 items = [] for x in items_raw[:200]: adet = int(x.get(“adet”) or 0) yuzde = round((adet / total) * 100, 2) if total else 0 items.append({“label”: x.get(“label”), “adet”: adet, “yuzde”: yuzde}) return {“group_by”: group_by, “total”: total, “items”: items} except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) # ============================================================ # ✅ TREND KPI # ============================================================ @app.get(“/api/rapor/trend-kpi”) def rapor_trend_kpi( months: int = Query(default=6, ge=1, le=24), start: date | None = None, end: date | None = None, ): try: if start and end: use_start = _date_start_dt(start) use_end = _date_end_dt_exclusive(end) else: now = datetime.now() cur_month = _first_day_of_month(now) start_month = _add_months(cur_month, -(months – 1)) end_month_excl = _add_months(cur_month, 1) use_start = start_month use_end = end_month_excl sql = “”” SELECT TO_CHAR(TRUNC(ppb.DATECREATED, ‘MM’), ‘YYYY-MM’) AS ay, SUM(CASE WHEN ppb.SHIPPINGSTATUS = 4 THEN 1 ELSE 0 END) AS teslim_edildi, SUM(CASE WHEN ppb.SHIPPINGSTATUS = 0 THEN 1 ELSE 0 END) AS sevk_bekliyor, SUM(CASE WHEN ppb.DELIVERYVALIDATIONTYPE = 0 THEN 1 ELSE 0 END) AS kart_okutulmadi FROM TRS_PACKAGEPACKBARCODE ppb WHERE ppb.DATECREATED >= :start_dt AND ppb.DATECREATED < :end_dt GROUP BY TRUNC(ppb.DATECREATED, 'MM') ORDER BY TRUNC(ppb.DATECREATED, 'MM') """ rows = fetch_all(sql, {"start_dt": use_start, "end_dt": use_end}) def pct(curr: int, prev: int): if prev == 0: return None return round(((curr - prev) / prev) * 100, 2) out = [] prev = None for r in rows: cur = { "ay": r.get("ay"), "teslim_edildi": int(r.get("teslim_edildi") or 0), "sevk_bekliyor": int(r.get("sevk_bekliyor") or 0), "kart_okutulmadi": int(r.get("kart_okutulmadi") or 0), } if prev: cur["degisim"] = { "teslim_edildi_pct": pct(cur["teslim_edildi"], prev["teslim_edildi"]), "sevk_bekliyor_pct": pct(cur["sevk_bekliyor"], prev["sevk_bekliyor"]), "kart_okutulmadi_pct": pct(cur["kart_okutulmadi"], prev["kart_okutulmadi"]), } else: cur["degisim"] = { "teslim_edildi_pct": None, "sevk_bekliyor_pct": None, "kart_okutulmadi_pct": None, } out.append(cur) prev = cur return {"months": out} except Exception as e: return JSONResponse(status_code=500, content={"error": str(e)}) @app.get("/api/trend-kpi") def trend_kpi_alias( months: int = Query(default=6, ge=1, le=24), start: date | None = None, end: date | None = None, ): return rapor_trend_kpi(months=months, start=start, end=end) def _make_xlsx(headers: list[str], rows: list[dict], sheet_name: str = "Rapor") -> bytes: wb = Workbook() ws = wb.active ws.title = sheet_name ws.append(headers) for r in rows: ws.append([r.get(h) for h in headers]) for i, h in enumerate(headers, start=1): max_len = len(h) for rr in rows[:300]: val = rr.get(h) if val is None: continue s = str(val) if len(s) > max_len: max_len = len(s) ws.column_dimensions[get_column_letter(i)].width = max(12, min(48, max_len + 2)) import io bio = io.BytesIO() wb.save(bio) return bio.getvalue() @app.get(“/api/rapor/export.xlsx”) def export_xlsx( start: date = Query(default_factory=date.today), end: date = Query(default_factory=date.today), barcode: str | None = None, invoice: str | None = None, status: str | None = None, driver: str | None = None, region: str | None = None, warehouse: str | None = None, departure: str | None = None, card: str | None = None, service: str | None = None, ): try: params = build_params(start, end, barcode, invoice, status, driver, region, warehouse, departure, card, service) sql = filtered_outer_sql(with_order=True) rows = fetch_all(sql, params) headers = [ “datecreated”, “hedefkodu”, “musteri”, “barcode”, “invoicecode”, “shippingstatus_text”, “servicetype_text”, “packageregionnametur”, “servicenametur”, “packagewarehousenametur”, “departuremanagernametur”, “arrivalmanagernametur”, “servicedrivername”, “servicevehicleplate”, “deliveredlocation”, “deliveryvalidationtype_text”, “servicetimestarts”, “datemodified”, ] data = _make_xlsx(headers, rows, sheet_name=”Sevkiyat”) filename = f”sevkiyat_{start}_{end}.xlsx” return StreamingResponse( iter([data]), media_type=”application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”, headers={“Content-Disposition”: f’attachment; filename=”{filename}”‘}, ) except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) @app.get(“/api/export.xlsx”) def export_xlsx_alias( start: date = Query(default_factory=date.today), end: date = Query(default_factory=date.today), barcode: str | None = None, invoice: str | None = None, status: str | None = None, driver: str | None = None, region: str | None = None, warehouse: str | None = None, departure: str | None = None, card: str | None = None, service: str | None = None, ): return export_xlsx( start=start, end=end, barcode=barcode, invoice=invoice, status=status, driver=driver, region=region, warehouse=warehouse, departure=departure, card=card, service=service ) # ============================================================ # ✅ RAPOR 2 # ============================================================ RAPOR2_BASE_SQL = “”” SELECT CI.ITEMID AS urunkodu, CI.NAMETUR AS urunadi, CN.NAMETUR AS depo, LOK.DISPLAYNAME AS lokasyon, SUM(SP.QUANTITY) AS stok, CC.NAMETUR AS baskin_sinif FROM CATALOG_ITEM CI INNER JOIN STOCK_PARTY SP ON CI.ITEMID = SP.ITEMID LEFT JOIN CATALOG_CLASS CC ON CI.DOMINANTCLASSID = CC.CLASSID INNER JOIN COLLABORATION_NODE CN ON SP.WAREHOUSEID = CN.NODEID LEFT JOIN ( SELECT DISTINCT CII.ITEMID, SC.WAREHOUSEID, SC.DISPLAYNAME FROM CATALOG_INVENTORYITEM CII INNER JOIN STOCK_STOCKINVENTORYITEM SSII ON CII.INVENTORYITEMID = SSII.INVENTORYITEMID INNER JOIN STOCK_CONTAINER SC ON SSII.EXITCONTAINERID = SC.CONTAINERID ) LOK ON CI.ITEMID = LOK.ITEMID AND SP.WAREHOUSEID = LOK.WAREHOUSEID WHERE LOK.ITEMID IS NULL GROUP BY CI.ITEMID, CI.NAMETUR, CN.NAMETUR, LOK.DISPLAYNAME, CC.NAMETUR HAVING (SUM(SP.QUANTITY) > 0) “”” def rapor2_filtered_sql(with_order: bool) -> str: where = “”” WHERE 1=1 AND (:urunkodu IS NULL OR LOWER(t.urunkodu) LIKE LOWER(:urunkodu_like)) AND (:urunadi IS NULL OR LOWER(t.urunadi) LIKE LOWER(:urunadi_like)) AND (:depo IS NULL OR LOWER(t.depo) LIKE LOWER(:depo_like)) AND (:baskin_sinif IS NULL OR LOWER(NVL(t.baskin_sinif,”)) LIKE LOWER(:baskin_sinif_like)) “”” order = ” ORDER BY t.stok DESC ” if with_order else “” return f””” SELECT * FROM ( {RAPOR2_BASE_SQL} ) t {where} {order} “”” def rapor2_count_sql() -> str: return f”SELECT COUNT(1) AS total FROM ( {rapor2_filtered_sql(with_order=False)} )” def rapor2_paged_sql() -> str: return f””” SELECT * FROM ( {rapor2_filtered_sql(with_order=True)} ) OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY “”” def rapor2_params( urunkodu: str | None, urunadi: str | None, depo: str | None, baskin_sinif: str | None, ) -> dict: urunkodu = (urunkodu or “”).strip() or None urunadi = (urunadi or “”).strip() or None depo = (depo or “”).strip() or None baskin_sinif = (baskin_sinif or “”).strip() or None return { “urunkodu”: urunkodu, “urunkodu_like”: f”{urunkodu}%” if urunkodu else None, “urunadi”: urunadi, “urunadi_like”: f”%{urunadi}%” if urunadi else None, “depo”: depo, “depo_like”: f”%{depo}%” if depo else None, “baskin_sinif”: baskin_sinif, “baskin_sinif_like”: f”%{baskin_sinif}%” if baskin_sinif else None, } @app.get(“/api/rapor2”) def rapor2_data( urunkodu: str | None = None, urunadi: str | None = None, depo: str | None = None, baskin_sinif: str | None = None, page: int = 1, page_size: int = 50, ): try: if page < 1: page = 1 if page_size < 1: page_size = 50 if page_size > 500: page_size = 500 params = rapor2_params(urunkodu, urunadi, depo, baskin_sinif) total_row = fetch_one(rapor2_count_sql(), params) total = int(total_row[“total”]) if total_row and total_row.get(“total”) is not None else 0 params_items = dict(params) params_items[“offset”] = (page – 1) * page_size params_items[“limit”] = page_size rows = fetch_all(rapor2_paged_sql(), params_items) return { “page”: page, “page_size”: page_size, “total”: total, “rows”: rows, } except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) @app.get(“/api/rapor2/export.xlsx”) def rapor2_export_xlsx( urunkodu: str | None = None, urunadi: str | None = None, depo: str | None = None, baskin_sinif: str | None = None, ): try: params = rapor2_params(urunkodu, urunadi, depo, baskin_sinif) rows = fetch_all(rapor2_filtered_sql(with_order=True), params) headers = [“urunkodu”, “urunadi”, “depo”, “lokasyon”, “stok”, “baskin_sinif”] data = _make_xlsx(headers, rows, sheet_name=”Lokasyonsuz_Urunler”) filename = f”lokasyonsuz_urunler_{date.today()}.xlsx” return StreamingResponse( iter([data]), media_type=”application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”, headers={“Content-Disposition”: f’attachment; filename=”{filename}”‘}, ) except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) # ============================================================ # ✅ YENİ RAPOR: PEŞİN DÜKKANLAR (KAMPANYA) # ============================================================ PESIN_BASE_SQL = “”” SELECT MCOT.TIMESTARTS AS baslangic_tarihi, MCOT.TIMEENDS AS bitis_tarihi, TL.LINKID AS linkid, HDFKOD.HEDEFKODU AS hedefkodu, TL.NAME AS hesapadi, CA.DISTRICTNAME AS semt, CA.CITYNAME AS sehir, MCOT.NAME AS kampanya FROM MARKETING_CAMP_ACTIVITY MCA JOIN MARKETING_CAMP_OTEMP MCOT ON MCA.CAMPAIGNID = MCOT.ACTIVITYID JOIN TRD_SAL_OFFRTMPLTCUSTOMER TSO ON TSO.OFFERTEMPLATEID = MCOT.OFFERTEMPLATEID JOIN TRADING_LINK TL ON TL.LINKID = TSO.CUSTOMERID JOIN CONTACTCC_CONTACT CC ON TL.CONTACTID = CC.CONTACTID JOIN CONTACTCC_ADDRESS CA ON CC.DEFAULTADDRESSID = CA.ADDRESSID JOIN ( SELECT /*+ MATERIALIZE */ CTPV.TITLEID, CAST(CPV.VALUETEXTTUR AS NUMBER) AS HEDEFKODU FROM CONTENTCC_C_TPROPERTYVALUE CTPV JOIN CONTENTCC_C_PROPERTYVALUE CPV ON CTPV.PROPERTYVALUEID = CPV.PROPERTYVALUEID WHERE CPV.PROPERTYID = 19 ) HDFKOD ON HDFKOD.TITLEID = TL.TITLEID WHERE MCA.ACTIVITYID = :activity_id AND SUBSTR(MCOT.NAME, 1, 3) IN (‘005′,’006′,’007′,’008′,’015′,’044′,’045′,’046′,’047’) “”” def pesin_filtered_sql(with_order: bool) -> str: where = “”” WHERE 1=1 AND (:linkid IS NULL OR t.linkid = :linkid) AND (:hedefkodu IS NULL OR t.hedefkodu = :hedefkodu) AND (:hesapadi IS NULL OR LOWER(t.hesapadi) LIKE LOWER(:hesapadi_like)) AND (:sehir IS NULL OR LOWER(t.sehir) LIKE LOWER(:sehir_like)) AND (:semt IS NULL OR LOWER(t.semt) LIKE LOWER(:semt_like)) AND (:kampanya IS NULL OR LOWER(t.kampanya) LIKE LOWER(:kampanya_like)) AND (:active_only = 0 OR t.bitis_tarihi >= TRUNC(SYSDATE)) AND (:start_dt IS NULL OR t.bitis_tarihi >= :start_dt) AND (:end_dt IS NULL OR t.bitis_tarihi < :end_dt) """ order = " ORDER BY t.bitis_tarihi ASC, t.baslangic_tarihi ASC " if with_order else "" return f""" SELECT * FROM ( {PESIN_BASE_SQL} ) t {where} {order} """ def pesin_count_sql() -> str: return f”SELECT COUNT(1) AS total FROM ( {pesin_filtered_sql(with_order=False)} )” def pesin_paged_sql() -> str: return f””” SELECT * FROM ( {pesin_filtered_sql(with_order=True)} ) OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY “”” def pesin_params( activity_id: int, linkid: int | None, hedefkodu: int | None, hesapadi: str | None, sehir: str | None, semt: str | None, kampanya: str | None, active_only: bool, end_start: date | None, end_end: date | None, ) -> dict: hesapadi = (hesapadi or “”).strip() or None sehir = (sehir or “”).strip() or None semt = (semt or “”).strip() or None kampanya = (kampanya or “”).strip() or None start_dt = _date_start_dt(end_start) if end_start else None end_dt = _date_end_dt_exclusive(end_end) if end_end else None return { “activity_id”: activity_id, “linkid”: linkid, “hedefkodu”: hedefkodu, “hesapadi”: hesapadi, “hesapadi_like”: f”%{hesapadi}%” if hesapadi else None, “sehir”: sehir, “sehir_like”: f”%{sehir}%” if sehir else None, “semt”: semt, “semt_like”: f”%{semt}%” if semt else None, “kampanya”: kampanya, “kampanya_like”: f”%{kampanya}%” if kampanya else None, “active_only”: 1 if active_only else 0, “start_dt”: start_dt, “end_dt”: end_dt, } @app.get(“/api/rapor_pesin”) def rapor_pesin_data( # Aktivite sabit istiyorsun: 3 activity_id: int = 3, linkid: int | None = None, hedefkodu: int | None = None, hesapadi: str | None = None, sehir: str | None = None, semt: str | None = None, kampanya: str | None = None, active_only: bool = True, # BİTİŞ TARİHİNE göre tarih filtre (opsiyonel) end_start: date | None = None, end_end: date | None = None, page: int = 1, page_size: int = 50, ): try: if page < 1: page = 1 if page_size < 1: page_size = 50 if page_size > 500: page_size = 500 params = pesin_params( activity_id=activity_id, linkid=linkid, hedefkodu=hedefkodu, hesapadi=hesapadi, sehir=sehir, semt=semt, kampanya=kampanya, active_only=active_only, end_start=end_start, end_end=end_end, ) total_row = fetch_one(pesin_count_sql(), params) total = int(total_row[“total”]) if total_row and total_row.get(“total”) is not None else 0 params_items = dict(params) params_items[“offset”] = (page – 1) * page_size params_items[“limit”] = page_size rows = fetch_all(pesin_paged_sql(), params_items) return { “filters”: { “activity_id”: activity_id, “active_only”: active_only, “end_start”: str(end_start) if end_start else None, “end_end”: str(end_end) if end_end else None, }, “page”: page, “page_size”: page_size, “total”: total, “rows”: rows, } except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) @app.get(“/api/rapor_pesin/summary”) def rapor_pesin_summary( activity_id: int = 3, linkid: int | None = None, hedefkodu: int | None = None, hesapadi: str | None = None, sehir: str | None = None, semt: str | None = None, kampanya: str | None = None, active_only: bool = True, end_start: date | None = None, end_end: date | None = None, group_by: str = “sehir”, ): try: params = pesin_params( activity_id=activity_id, linkid=linkid, hedefkodu=hedefkodu, hesapadi=hesapadi, sehir=sehir, semt=semt, kampanya=kampanya, active_only=active_only, end_start=end_start, end_end=end_end, ) group_map = { “sehir”: “t.sehir”, “semt”: “t.semt”, “kampanya”: “t.kampanya”, “hedefkodu”: “TO_CHAR(t.hedefkodu)”, “hesapadi”: “t.hesapadi”, } col = group_map.get(group_by, “t.sehir”) sql = f””” SELECT NVL({col}, ‘-‘) AS label, COUNT(1) AS adet FROM ( {pesin_filtered_sql(with_order=False)} ) t GROUP BY {col} ORDER BY COUNT(1) DESC “”” items_raw = fetch_all(sql, params) total = sum(int(x.get(“adet”) or 0) for x in items_raw) or 0 items = [] for x in items_raw[:300]: adet = int(x.get(“adet”) or 0) yuzde = round((adet / total) * 100, 2) if total else 0 items.append({“label”: x.get(“label”), “adet”: adet, “yuzde”: yuzde}) return {“group_by”: group_by, “total”: total, “items”: items} except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) @app.get(“/api/rapor_pesin/export.xlsx”) def rapor_pesin_export_xlsx( activity_id: int = 3, linkid: int | None = None, hedefkodu: int | None = None, hesapadi: str | None = None, sehir: str | None = None, semt: str | None = None, kampanya: str | None = None, active_only: bool = True, end_start: date | None = None, end_end: date | None = None, ): try: params = pesin_params( activity_id=activity_id, linkid=linkid, hedefkodu=hedefkodu, hesapadi=hesapadi, sehir=sehir, semt=semt, kampanya=kampanya, active_only=active_only, end_start=end_start, end_end=end_end, ) rows = fetch_all(pesin_filtered_sql(with_order=True), params) headers = [ “baslangic_tarihi”, “bitis_tarihi”, “linkid”, “hedefkodu”, “hesapadi”, “semt”, “sehir”, “kampanya”, ] data = _make_xlsx(headers, rows, sheet_name=”Pesin_Dukkanlar”) filename = f”pesin_dukkanlar_{date.today()}.xlsx” return StreamingResponse( iter([data]), media_type=”application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”, headers={“Content-Disposition”: f’attachment; filename=”{filename}”‘}, ) except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) {% block title %}Panel{% endblock %} {% block head_extra %}{% endblock %}

{% block page_title %}Panel{% endblock %}

Hazır
DB Test
{% block content %} {% endblock %}
{% block body_extra %}{% endblock %} {% extends “base.html” %} {% block title %}Lokasyonu Olmayan Ürünler{% endblock %} {% block page_title %}Lokasyonu Olmayan Ürünler{% endblock %} {% block nav_rapor2_active %}active{% endblock %} {% block head_extra %} {% endblock %} {% block content %}

Cencora

Yükleniyor…

Lokasyonu Olmayan Ürünler

Filtreler
Ara → Getir. Excel tüm filtreli satırları indirir.
Toplam: 0 Lokasyonsuz: 0
Ürün Kodu Ürün Adı Depo Lokasyon Stok Baskın Sınıf Durum
Henüz veri yok.
{% endblock %} {% block scripts_extra %} {% endblock %} {% extends “base.html” %} {% block title %}Peşin Dükkanlar{% endblock %} {% block page_title %}Peşin Dükkanlar{% endblock %} {% block nav_rapor_pesin_active %}active{% endblock %} {% block head_extra %} {% endblock %} {% block content %}

Peşin Dükkanlar

Yükleniyor…

Peşin Dükkanlar (Kampanya)

Filtreler
Filtreleri doldur → Getir. Excel tüm filtreli satırları indirir.
Toplam: 0
Başlangıç Bitiş LinkID Hedef Kodu Hesap Adı Semt Şehir Kampanya
Henüz veri yok.
{% endblock %} {% block scripts_extra %} {% endblock %} —— Sevkiyat Raporu

Cencora

Yükleniyor…

Sevkiyat Raporu

DB kontrol ediliyor…
Filtreler
Filtreleri doldur → Raporu Çalıştır. Excel, filtreli tüm satırları indirir.

Gruplama

Gruplama (2)

DATECREATED HEDEFKODU MUSTERI BARCODE INVOICE DURUM SERVIS_TIPI BOLGE SERVIS WAREHOUSE DEPARTURE ARRIVAL SOFOR ARAC DELIVERED_LOC KART SERVICE_START DATEMODIFIED
Henüz veri yok.
Servet Çelik — Tüm hakları saklıdır.
Cencora Alliance İstanbul
SELECT MCOT.TIMESTARTS AS BASLANGIC_TARIHI, MCOT.TIMEENDS AS BITIS_TARIHI, TL.LINKID, HDFKOD.HEDEFKODU, TL.NAME AS HESAPADI, CA.DISTRICTNAME AS SEMT, CA.CITYNAME AS SEHIR, MCOT.NAME AS KAMPANYA FROM MARKETING_CAMP_ACTIVITY MCA JOIN MARKETING_CAMP_OTEMP MCOT ON MCA.CAMPAIGNID = MCOT.ACTIVITYID JOIN TRD_SAL_OFFRTMPLTCUSTOMER TSO ON TSO.OFFERTEMPLATEID = MCOT.OFFERTEMPLATEID JOIN TRADING_LINK TL ON TL.LINKID = TSO.CUSTOMERID JOIN CONTACTCC_CONTACT CC ON TL.CONTACTID = CC.CONTACTID JOIN CONTACTCC_ADDRESS CA ON CC.DEFAULTADDRESSID = CA.ADDRESSID JOIN ( SELECT /*+ MATERIALIZE */ CTPV.TITLEID, CAST(CPV.VALUETEXTTUR AS NUMBER) AS HEDEFKODU FROM CONTENTCC_C_TPROPERTYVALUE CTPV JOIN CONTENTCC_C_PROPERTYVALUE CPV ON CTPV.PROPERTYVALUEID = CPV.PROPERTYVALUEID WHERE CPV.PROPERTYID = 19 ) HDFKOD ON HDFKOD.TITLEID = TL.TITLEID WHERE MCA.ACTIVITYID = 3 /* AND TSO.OFFERTEMPLATEID = 21 */ — Peşin statü lazımsa açın AND MCOT.TIMEENDS >= TRUNC(SYSDATE) — Aktif kampanyalar AND SUBSTR(MCOT.NAME, 1, 3) IN (‘005′,’006′,’007′,’008′,’015′,’044′,’045′,’046′,’047’); /////////// {% extends “base.html” %} {% block title %}Lokasyonsuz Ürünler{% endblock %} {% block content %}

Lokasyonsuz Ürünler

URUNKODU URUNADI DEPO LOKASYON STOK BASKIN_SINIF
Henüz veri yok.
{% endblock %} Sevkiyat Raporu

Cencora

Yükleniyor…

Sevkiyat Raporu

DB kontrol ediliyor…
Filtreleri doldur → Raporu Getir ile sorguyu çalıştır. Excel: filtreli tüm kayıt iner.

Gruplama

Özet

Kural: Teslim Edildi artarsa iyi ✅  |  Sevk Bekliyor ve Kart Okutulmadı azalırsa iyi ✅
Durum Adet %
Henüz KPI yok.
“Son 6 Ay KPI” butonuna basınca üstte trend tablosu görünür.
DATECREATED HEDEFKODU MUSTERI BARCODE INVOICE DURUM SERVIS_TIPI BOLGE SERVIS WAREHOUSE DEPARTURE ARRIVAL SOFOR ARAC DELIVERED_LOC KART SERVICE_START DATEMODIFIED
Henüz veri yok.
************************** # app/main.py import os from datetime import date, datetime, timedelta from fastapi import FastAPI, Query, Request from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse, RedirectResponse from fastapi.templating import Jinja2Templates from fastapi.staticfiles import StaticFiles from dotenv import load_dotenv from openpyxl import Workbook from openpyxl.utils import get_column_letter from app.db import fetch_all, fetch_one load_dotenv() app = FastAPI(title=”Panel Rapor”, version=”1.0.0″) # ============================================================ # ✅ TEMPLATE / STATIC PATH # ============================================================ APP_DIR = os.path.dirname(os.path.abspath(__file__)) # …/app PROJECT_DIR = os.path.abspath(os.path.join(APP_DIR, “..”)) # proje kökü CANDIDATE_TEMPLATE_DIRS = [ os.path.join(PROJECT_DIR, “templates”), os.path.join(APP_DIR, “templates”), ] TEMPLATES_DIR = next((p for p in CANDIDATE_TEMPLATE_DIRS if os.path.isdir(p)), None) if not TEMPLATES_DIR: raise RuntimeError( “templates klasörü bulunamadı. Şunlardan birini oluştur:\n” f”- {os.path.join(PROJECT_DIR, ‘templates’)}\n” f”- {os.path.join(APP_DIR, ‘templates’)}” ) templates = Jinja2Templates(directory=TEMPLATES_DIR) CANDIDATE_STATIC_DIRS = [ os.path.join(PROJECT_DIR, “static”), os.path.join(APP_DIR, “static”), ] STATIC_DIR = next((p for p in CANDIDATE_STATIC_DIRS if os.path.isdir(p)), None) if STATIC_DIR: app.mount(“/static”, StaticFiles(directory=STATIC_DIR), name=”static”) def _date_start_dt(d: date) -> datetime: return datetime(d.year, d.month, d.day, 0, 0, 0) def _date_end_dt_exclusive(d: date) -> datetime: return datetime(d.year, d.month, d.day, 0, 0, 0) + timedelta(days=1) def _first_day_of_month(dt: datetime) -> datetime: return datetime(dt.year, dt.month, 1, 0, 0, 0) def _add_months(dt: datetime, months: int) -> datetime: y = dt.year + (dt.month – 1 + months) // 12 m = (dt.month – 1 + months) % 12 + 1 return datetime(y, m, 1, 0, 0, 0) # ========================= # ✅ YOUR INNER SELECT (RAPOR 1) # ========================= BASE_INNER_SQL = “”” SELECT ( SELECT MAX(TO_NUMBER(pv.VALUETEXTTUR)) FROM CONTENTCC_C_PROPERTYVALUE pv JOIN CONTENTCC_C_TPROPERTYVALUE tpv ON pv.PROPERTYVALUEID = tpv.PROPERTYVALUEID WHERE pv.PROPERTYID = 19 AND tpv.TITLEID = tl.TITLEID ) AS HEDEFKODU, (pl.NAME || ‘,’ || adr.DISTRICTNAME || ‘,’ || adr.CITYNAME) AS MUSTERI, ppb.DATECREATED, srv.TIMESTARTS AS SERVICETIMESTARTS, ppb.DATEMODIFIED, ( SELECT InvoiceCode FROM ( SELECT i.Code AS InvoiceCode, ROW_NUMBER() OVER (ORDER BY ip.InvoiceID DESC) rn FROM Finance_InvoicePackage ip JOIN Finance_Invoice i ON i.InvoiceID = ip.InvoiceID WHERE ip.TradingPackageID = ppb.PackageID ) WHERE rn = 1 ) AS INVOICECODE, CASE ppb.DELIVERYVALIDATIONTYPE WHEN 1 THEN ‘Kart Okutuldu’ WHEN 0 THEN ‘Kart Okutulmadı’ ELSE NULL END AS DELIVERYVALIDATIONTYPE_TEXT, ppb.BARCODE, CASE ppb.SHIPPINGSTATUS WHEN 0 THEN ‘Sevk Bekliyor’ WHEN 1 THEN ‘Sevk Kontrolü Yapıldı’ WHEN 2 THEN ‘Sevk Başladı’ WHEN 3 THEN ‘Transfer Edildi’ WHEN 4 THEN ‘Teslim Edildi’ WHEN 5 THEN ‘Teslim Edilemedi’ WHEN 99 THEN ‘İptal Edildi’ ELSE NULL END AS SHIPPINGSTATUS_TEXT, CASE srv.ISEXPRESSSERVICE WHEN 1 THEN ‘Hızlı Teslimat’ WHEN 0 THEN ‘Servis’ ELSE NULL END AS SERVICETYPE_TEXT, reg.NAMETUR AS PACKAGEREGIONNAMETUR, srv.NAMETUR AS SERVICENAMETUR, wh.NAMETUR AS PACKAGEWAREHOUSENAMETUR, dep.NAMETUR AS DEPARTUREMANAGERNAMETUR, arr.NAMETUR AS ARRIVALMANAGERNAMETUR, drv.NAME AS SERVICEDRIVERNAME, veh.PLATE AS SERVICEVEHICLEPLATE, ( SELECT DeliveredPoint FROM ( SELECT sp.DeliveredPoint, ROW_NUMBER() OVER (ORDER BY sp.DeliveredTime DESC) rn FROM Transportation_ServicePackage sp WHERE sp.PackagePackBarcodeID = ppb.PackagePackBarcodeID AND sp.IsDelivered = 1 ) WHERE rn = 1 ) AS DELIVEREDLOCATION FROM TRS_PACKAGEPACKBARCODE ppb JOIN TRANSPORTATION_PACKAGE pkg ON pkg.PACKAGEID = ppb.PACKAGEID LEFT JOIN TRANSPORTATION_REGION reg ON reg.REGIONID = pkg.REGIONID LEFT JOIN TRANSPORTATION_SERVICE srv ON srv.SERVICEID = ppb.SERVICEID LEFT JOIN TRANSPORTATION_VEHICLE veh ON veh.VEHICLEID = srv.VEHICLEID LEFT JOIN COLLABORATION_NODE wh ON wh.NODEID = pkg.WAREHOUSEID LEFT JOIN COLLABORATION_NODE dep ON dep.NODEID = ppb.DEPARTUREMANAGERID LEFT JOIN COLLABORATION_NODE arr ON arr.NODEID = ppb.ARRIVALMANAGERID LEFT JOIN COLLABORATION_EMPLOYEE drv ON drv.EMPLOYEEID = srv.DRIVERID LEFT JOIN TRADING_LINK pl ON pl.LINKID = pkg.LINKID INNER JOIN TRADING_LINK tl ON tl.LINKID = pkg.LINKID LEFT JOIN CONTACTCC_CONTACT c ON c.CONTACTID = pl.CONTACTID LEFT JOIN CONTACTCC_ADDRESS adr ON adr.ADDRESSID = c.DEFAULTADDRESSID WHERE pl.NAME NOT LIKE ‘%All%’ AND reg.NAMETUR IS NOT NULL AND reg.NAMETUR <> ‘NULL’ AND ppb.DATECREATED >= :start_date AND ppb.DATECREATED < :end_date """ def filtered_outer_sql(with_order: bool) -> str: where = “”” WHERE 1=1 AND (:barcode IS NULL OR LOWER(t.barcode) LIKE LOWER(:barcode_like)) AND (:invoice IS NULL OR NVL(LOWER(t.invoicecode),”) = LOWER(:invoice)) AND (:status IS NULL OR t.shippingstatus_text = :status) AND (:driver IS NULL OR LOWER(NVL(t.servicedrivername,”)) LIKE LOWER(:driver_like)) AND (:region IS NULL OR LOWER(NVL(t.packageregionnametur,”)) LIKE LOWER(:region_like)) AND (:warehouse IS NULL OR LOWER(NVL(t.packagewarehousenametur,”)) LIKE LOWER(:warehouse_like)) AND (:departure IS NULL OR LOWER(NVL(t.departuremanagernametur,”)) LIKE LOWER(:departure_like)) AND (:card IS NULL OR t.deliveryvalidationtype_text = :card) AND (:service IS NULL OR LOWER(NVL(t.servicenametur,”)) LIKE LOWER(:service_like)) “”” order = ” ORDER BY t.datecreated DESC ” if with_order else “” return f””” SELECT * FROM ( {BASE_INNER_SQL} ) t {where} {order} “”” def count_sql() -> str: return f”SELECT COUNT(1) AS total FROM ( {filtered_outer_sql(with_order=False)} )” def paged_sql() -> str: return f””” SELECT * FROM ( {filtered_outer_sql(with_order=True)} ) OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY “”” # ========================= # ✅ PAGES # ========================= @app.get(“/”, include_in_schema=False) def root(): return RedirectResponse(url=”/base”, status_code=302) @app.get(“/base”, response_class=HTMLResponse) def base_page(request: Request): return templates.TemplateResponse(“base.html”, {“request”: request}) @app.get(“/rapor”, response_class=HTMLResponse) def rapor_page(request: Request): return templates.TemplateResponse(“rapor.html”, {“request”: request}) @app.get(“/rapor2”, response_class=HTMLResponse) def rapor2_page(request: Request): return templates.TemplateResponse(“rapor2.html”, {“request”: request}) # ========================= # ✅ HEALTH # ========================= @app.get(“/api/health”) def health(): try: row = fetch_one(“SELECT 1 AS ok FROM dual”) return {“db”: “ok”, “ok”: row.get(“ok”, 1) if row else 1} except Exception as e: return JSONResponse(status_code=500, content={“db”: “fail”, “error”: str(e)}) def build_params( start: date, end: date, barcode: str | None, invoice: str | None, status: str | None, driver: str | None, region: str | None, warehouse: str | None, departure: str | None, card: str | None, service: str | None, ) -> dict: start_dt = _date_start_dt(start) end_dt = _date_end_dt_exclusive(end) barcode = (barcode or “”).strip() or None invoice = (invoice or “”).strip() or None status = (status or “”).strip() or None driver = (driver or “”).strip() or None region = (region or “”).strip() or None warehouse = (warehouse or “”).strip() or None departure = (departure or “”).strip() or None card = (card or “”).strip() or None service = (service or “”).strip() or None return { “start_date”: start_dt, “end_date”: end_dt, “barcode”: barcode, “barcode_like”: f”{barcode}%” if barcode else None, “invoice”: invoice, “status”: status, “driver”: driver, “driver_like”: f”%{driver}%” if driver else None, “region”: region, “region_like”: f”%{region}%” if region else None, “warehouse”: warehouse, “warehouse_like”: f”%{warehouse}%” if warehouse else None, “departure”: departure, “departure_like”: f”%{departure}%” if departure else None, “card”: card, “service”: service, “service_like”: f”%{service}%” if service else None, } # ========================= # ✅ RAPOR 1 API # ========================= @app.get(“/api/rapor”) def rapor_data( start: date = Query(default_factory=date.today), end: date = Query(default_factory=date.today), barcode: str | None = None, invoice: str | None = None, status: str | None = None, driver: str | None = None, region: str | None = None, warehouse: str | None = None, departure: str | None = None, card: str | None = None, service: str | None = None, page: int = 1, page_size: int = 50, ): try: if page < 1: page = 1 if page_size < 1: page_size = 50 if page_size > 500: page_size = 500 params = build_params(start, end, barcode, invoice, status, driver, region, warehouse, departure, card, service) total_row = fetch_one(count_sql(), params) total = int(total_row[“total”]) if total_row and total_row.get(“total”) is not None else 0 params_items = dict(params) params_items[“offset”] = (page – 1) * page_size params_items[“limit”] = page_size rows = fetch_all(paged_sql(), params_items) kpi_sql = f””” SELECT shippingstatus_text AS durum, COUNT(1) AS adet FROM ( {filtered_outer_sql(with_order=False)} ) GROUP BY shippingstatus_text ORDER BY COUNT(1) DESC “”” kpi_rows = fetch_all(kpi_sql, params) kpis = [] for r in kpi_rows: adet = int(r.get(“adet”) or 0) yuzde = round((adet / total) * 100, 2) if total else 0 kpis.append({“durum”: r.get(“durum”), “adet”: adet, “yuzde”: yuzde}) return { “range”: {“start”: str(start), “end”: str(end)}, “page”: page, “page_size”: page_size, “total”: total, “kpis”: kpis, “rows”: rows, } except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) @app.get(“/api/rapor/group”) def rapor_group( start: date = Query(default_factory=date.today), end: date = Query(default_factory=date.today), barcode: str | None = None, invoice: str | None = None, status: str | None = None, driver: str | None = None, region: str | None = None, warehouse: str | None = None, departure: str | None = None, card: str | None = None, service: str | None = None, group_by: str = “service”, ): try: params = build_params(start, end, barcode, invoice, status, driver, region, warehouse, departure, card, service) group_map = { “service”: “t.servicenametur”, “customer”: “t.musteri”, “region”: “t.packageregionnametur”, “warehouse”: “t.packagewarehousenametur”, “departure”: “t.departuremanagernametur”, “card”: “t.deliveryvalidationtype_text”, “status”: “t.shippingstatus_text”, “driver”: “t.servicedrivername”, } col = group_map.get(group_by, “t.servicenametur”) sql = f””” SELECT NVL({col}, ‘-‘) AS label, COUNT(1) AS adet FROM ( {filtered_outer_sql(with_order=False)} ) t GROUP BY {col} ORDER BY COUNT(1) DESC “”” items_raw = fetch_all(sql, params) total = sum(int(x.get(“adet”) or 0) for x in items_raw) or 0 items = [] for x in items_raw[:200]: adet = int(x.get(“adet”) or 0) yuzde = round((adet / total) * 100, 2) if total else 0 items.append({“label”: x.get(“label”), “adet”: adet, “yuzde”: yuzde}) return {“group_by”: group_by, “total”: total, “items”: items} except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) # ============================================================ # ✅ TREND KPI # ============================================================ @app.get(“/api/rapor/trend-kpi”) def rapor_trend_kpi( months: int = Query(default=6, ge=1, le=24), start: date | None = None, end: date | None = None, ): try: if start and end: use_start = _date_start_dt(start) use_end = _date_end_dt_exclusive(end) else: now = datetime.now() cur_month = _first_day_of_month(now) start_month = _add_months(cur_month, -(months – 1)) end_month_excl = _add_months(cur_month, 1) use_start = start_month use_end = end_month_excl sql = “”” SELECT TO_CHAR(TRUNC(ppb.DATECREATED, ‘MM’), ‘YYYY-MM’) AS ay, SUM(CASE WHEN ppb.SHIPPINGSTATUS = 4 THEN 1 ELSE 0 END) AS teslim_edildi, SUM(CASE WHEN ppb.SHIPPINGSTATUS = 0 THEN 1 ELSE 0 END) AS sevk_bekliyor, SUM(CASE WHEN ppb.DELIVERYVALIDATIONTYPE = 0 THEN 1 ELSE 0 END) AS kart_okutulmadi FROM TRS_PACKAGEPACKBARCODE ppb WHERE ppb.DATECREATED >= :start_dt AND ppb.DATECREATED < :end_dt GROUP BY TRUNC(ppb.DATECREATED, 'MM') ORDER BY TRUNC(ppb.DATECREATED, 'MM') """ rows = fetch_all(sql, {"start_dt": use_start, "end_dt": use_end}) def pct(curr: int, prev: int): if prev == 0: return None return round(((curr - prev) / prev) * 100, 2) out = [] prev = None for r in rows: cur = { "ay": r.get("ay"), "teslim_edildi": int(r.get("teslim_edildi") or 0), "sevk_bekliyor": int(r.get("sevk_bekliyor") or 0), "kart_okutulmadi": int(r.get("kart_okutulmadi") or 0), } if prev: cur["degisim"] = { "teslim_edildi_pct": pct(cur["teslim_edildi"], prev["teslim_edildi"]), "sevk_bekliyor_pct": pct(cur["sevk_bekliyor"], prev["sevk_bekliyor"]), "kart_okutulmadi_pct": pct(cur["kart_okutulmadi"], prev["kart_okutulmadi"]), } else: cur["degisim"] = { "teslim_edildi_pct": None, "sevk_bekliyor_pct": None, "kart_okutulmadi_pct": None, } out.append(cur) prev = cur return {"months": out} except Exception as e: return JSONResponse(status_code=500, content={"error": str(e)}) @app.get("/api/trend-kpi") def trend_kpi_alias( months: int = Query(default=6, ge=1, le=24), start: date | None = None, end: date | None = None, ): return rapor_trend_kpi(months=months, start=start, end=end) def _make_xlsx(headers: list[str], rows: list[dict], sheet_name: str = "Rapor") -> bytes: wb = Workbook() ws = wb.active ws.title = sheet_name ws.append(headers) for r in rows: ws.append([r.get(h) for h in headers]) for i, h in enumerate(headers, start=1): max_len = len(h) for rr in rows[:300]: val = rr.get(h) if val is None: continue s = str(val) if len(s) > max_len: max_len = len(s) ws.column_dimensions[get_column_letter(i)].width = max(12, min(48, max_len + 2)) import io bio = io.BytesIO() wb.save(bio) return bio.getvalue() @app.get(“/api/rapor/export.xlsx”) def export_xlsx( start: date = Query(default_factory=date.today), end: date = Query(default_factory=date.today), barcode: str | None = None, invoice: str | None = None, status: str | None = None, driver: str | None = None, region: str | None = None, warehouse: str | None = None, departure: str | None = None, card: str | None = None, service: str | None = None, ): try: params = build_params(start, end, barcode, invoice, status, driver, region, warehouse, departure, card, service) sql = filtered_outer_sql(with_order=True) rows = fetch_all(sql, params) headers = [ “datecreated”, “hedefkodu”, “musteri”, “barcode”, “invoicecode”, “shippingstatus_text”, “servicetype_text”, “packageregionnametur”, “servicenametur”, “packagewarehousenametur”, “departuremanagernametur”, “arrivalmanagernametur”, “servicedrivername”, “servicevehicleplate”, “deliveredlocation”, “deliveryvalidationtype_text”, “servicetimestarts”, “datemodified”, ] data = _make_xlsx(headers, rows, sheet_name=”Sevkiyat”) filename = f”sevkiyat_{start}_{end}.xlsx” return StreamingResponse( iter([data]), media_type=”application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”, headers={“Content-Disposition”: f’attachment; filename=”{filename}”‘}, ) except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) @app.get(“/api/export.xlsx”) def export_xlsx_alias( start: date = Query(default_factory=date.today), end: date = Query(default_factory=date.today), barcode: str | None = None, invoice: str | None = None, status: str | None = None, driver: str | None = None, region: str | None = None, warehouse: str | None = None, departure: str | None = None, card: str | None = None, service: str | None = None, ): return export_xlsx( start=start, end=end, barcode=barcode, invoice=invoice, status=status, driver=driver, region=region, warehouse=warehouse, departure=departure, card=card, service=service ) # ============================================================ # ✅ RAPOR 2 (Lokasyonu olmayan ürünler) # ============================================================ RAPOR2_BASE_SQL = “”” SELECT CI.ITEMID AS urunkodu, CI.NAMETUR AS urunadi, CN.NAMETUR AS depo, LOK.DISPLAYNAME AS lokasyon, SUM(SP.QUANTITY) AS stok, CC.NAMETUR AS baskin_sinif FROM CATALOG_ITEM CI INNER JOIN STOCK_PARTY SP ON CI.ITEMID = SP.ITEMID LEFT JOIN CATALOG_CLASS CC ON CI.DOMINANTCLASSID = CC.CLASSID INNER JOIN COLLABORATION_NODE CN ON SP.WAREHOUSEID = CN.NODEID LEFT JOIN ( SELECT DISTINCT CII.ITEMID, SC.WAREHOUSEID, SC.DISPLAYNAME FROM CATALOG_INVENTORYITEM CII INNER JOIN STOCK_STOCKINVENTORYITEM SSII ON CII.INVENTORYITEMID = SSII.INVENTORYITEMID INNER JOIN STOCK_CONTAINER SC ON SSII.EXITCONTAINERID = SC.CONTAINERID ) LOK ON CI.ITEMID = LOK.ITEMID AND SP.WAREHOUSEID = LOK.WAREHOUSEID WHERE LOK.ITEMID IS NULL GROUP BY CI.ITEMID, CI.NAMETUR, CN.NAMETUR, LOK.DISPLAYNAME, CC.NAMETUR HAVING (SUM(SP.QUANTITY) > 0) “”” def rapor2_filtered_sql(with_order: bool) -> str: where = “”” WHERE 1=1 AND (:urunkodu IS NULL OR LOWER(t.urunkodu) LIKE LOWER(:urunkodu_like)) AND (:urunadi IS NULL OR LOWER(t.urunadi) LIKE LOWER(:urunadi_like)) AND (:depo IS NULL OR LOWER(t.depo) LIKE LOWER(:depo_like)) AND (:baskin_sinif IS NULL OR LOWER(NVL(t.baskin_sinif,”)) LIKE LOWER(:baskin_sinif_like)) “”” order = ” ORDER BY t.stok DESC ” if with_order else “” return f””” SELECT * FROM ( {RAPOR2_BASE_SQL} ) t {where} {order} “”” def rapor2_count_sql() -> str: return f”SELECT COUNT(1) AS total FROM ( {rapor2_filtered_sql(with_order=False)} )” def rapor2_paged_sql() -> str: return f””” SELECT * FROM ( {rapor2_filtered_sql(with_order=True)} ) OFFSET :offset ROWS FETCH NEXT :limit ROWS ONLY “”” def rapor2_params( urunkodu: str | None, urunadi: str | None, depo: str | None, baskin_sinif: str | None, ) -> dict: urunkodu = (urunkodu or “”).strip() or None urunadi = (urunadi or “”).strip() or None depo = (depo or “”).strip() or None baskin_sinif = (baskin_sinif or “”).strip() or None return { “urunkodu”: urunkodu, “urunkodu_like”: f”{urunkodu}%” if urunkodu else None, “urunadi”: urunadi, “urunadi_like”: f”%{urunadi}%” if urunadi else None, “depo”: depo, “depo_like”: f”%{depo}%” if depo else None, “baskin_sinif”: baskin_sinif, “baskin_sinif_like”: f”%{baskin_sinif}%” if baskin_sinif else None, } @app.get(“/api/rapor2”) def rapor2_data( urunkodu: str | None = None, urunadi: str | None = None, depo: str | None = None, baskin_sinif: str | None = None, page: int = 1, page_size: int = 50, ): try: if page < 1: page = 1 if page_size < 1: page_size = 50 if page_size > 500: page_size = 500 params = rapor2_params(urunkodu, urunadi, depo, baskin_sinif) total_row = fetch_one(rapor2_count_sql(), params) total = int(total_row[“total”]) if total_row and total_row.get(“total”) is not None else 0 params_items = dict(params) params_items[“offset”] = (page – 1) * page_size params_items[“limit”] = page_size rows = fetch_all(rapor2_paged_sql(), params_items) return { “page”: page, “page_size”: page_size, “total”: total, “rows”: rows, } except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) @app.get(“/api/rapor2/export.xlsx”) def rapor2_export_xlsx( urunkodu: str | None = None, urunadi: str | None = None, depo: str | None = None, baskin_sinif: str | None = None, ): try: params = rapor2_params(urunkodu, urunadi, depo, baskin_sinif) rows = fetch_all(rapor2_filtered_sql(with_order=True), params) headers = [“urunkodu”, “urunadi”, “depo”, “lokasyon”, “stok”, “baskin_sinif”] data = _make_xlsx(headers, rows, sheet_name=”Lokasyonsuz_Urunler”) filename = f”lokasyonsuz_urunler_{date.today()}.xlsx” return StreamingResponse( iter([data]), media_type=”application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”, headers={“Content-Disposition”: f’attachment; filename=”{filename}”‘}, ) except Exception as e: return JSONResponse(status_code=500, content={“error”: str(e)}) # ============================================================ # ✅ FRONTEND UYUMLULUK ALIASLARI (API ERROR ÇÖZÜMÜ) # ============================================================ # Frontend bazı sayfalarda şunları çağırıyor olabilir: # /api/products/no-location # /api/products/no-location/export.xlsx # Senin backend’de karşılığı zaten /api/rapor2 ve /api/rapor2/export.xlsx # Bu yüzden alias ekledik. @app.get(“/api/products/no-location”) def products_no_location_alias( urunkodu: str | None = None, urunadi: str | None = None, depo: str | None = None, baskin_sinif: str | None = None, page: int = 1, page_size: int = 50, ): return rapor2_data( urunkodu=urunkodu, urunadi=urunadi, depo=depo, baskin_sinif=baskin_sinif, page=page, page_size=page_size, ) @app.get(“/api/products/no-location/export.xlsx”) def products_no_location_export_alias( urunkodu: str | None = None, urunadi: str | None = None, depo: str | None = None, baskin_sinif: str | None = None, ): return rapor2_export_xlsx( urunkodu=urunkodu, urunadi=urunadi, depo=depo, baskin_sinif=baskin_sinif, ) {% extends “base.html” %} {% block title %}Lokasyonu Olmayan Ürünler{% endblock %} {% block page_title %}Lokasyonu Olmayan Ürünler{% endblock %} {# base menüde aktif olsun diye (base buna bakıyorsa) #} {% block nav_rapor2_active %}active{% endblock %} {% block head_extra %} {% endblock %} {% block content %}

Cencora

Yükleniyor…

Lokasyonu Olmayan Ürünler

Filtreler
Ara → Getir. Excel tüm filtreli satırları indirir.
Toplam: 0 Lokasyonsuz: 0
Ürün Kodu Ürün Adı Depo Lokasyon Stok Baskın Sınıf Durum
Henüz veri yok.
{% endblock %} {% block scripts_extra %} {% endblock %} {% block scripts_extra %}{% endblock %} ************************** \\\\\\\\\\\\\\\\\\\\ {% extends “base.html” %} {% block title %}Sevkiyat Yönetimi{% endblock %} {% block page_title %}Sevkiyat Yönetimi{% endblock %} {% block nav_sevkiyat_active %}active{% endblock %} {% block head_extra %} {% endblock %} {% block overlays %}

Cencora

Loading…

{% endblock %} {% block content %}
Filters
Fill filters → Run Report. Excel exports all filtered rows.

Grouping

Grouping (2)

DATECREATED HEDEFKODU MUSTERI BARCODE INVOICE STATUS SERVICE_TYPE REGION SERVICE WAREHOUSE DEPARTURE ARRIVAL DRIVER VEHICLE DELIVERED_LOC CARD SERVICE_START DATEMODIFIED
No data yet.
{% endblock %} {% block body_end %} {% endblock %} {% block title %}Dashboard{% endblock %} {% block head_extra %}{% endblock %} {% block overlays %}{% endblock %}

{% block page_title %}Dashboard{% endblock %}

{% block page_meta %}-{% endblock %}
Checking DB…
{% block content %}{% endblock %}
Servet Çelik — All rights reserved.
Cencora Alliance Istanbul
{% block body_end %}{% endblock %}