|
|
|
@@ -8,7 +8,7 @@ from typing import Dict, Iterable, Optional, Tuple
|
|
|
|
import psycopg2
|
|
|
|
import psycopg2
|
|
|
|
import psycopg2.extras
|
|
|
|
import psycopg2.extras
|
|
|
|
import requests
|
|
|
|
import requests
|
|
|
|
from dotenv import find_dotenv, load_dotenv
|
|
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
|
|
|
|
|
|
from uv_app.core.mssql import connect_to_mssql
|
|
|
|
from uv_app.core.mssql import connect_to_mssql
|
|
|
|
from uv_app.core.pgsql import connect_to_pgsql
|
|
|
|
from uv_app.core.pgsql import connect_to_pgsql
|
|
|
|
@@ -17,11 +17,13 @@ BASE_URL = "https://api.manorivile.lt/client/v2"
|
|
|
|
TIMEOUT = 30
|
|
|
|
TIMEOUT = 30
|
|
|
|
QUERY_PATH = Path(__file__).with_name("routeriai_query.sql")
|
|
|
|
QUERY_PATH = Path(__file__).with_name("routeriai_query.sql")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DOTENV_PATH = Path(__file__).resolve().parents[2] / ".env"
|
|
|
|
# Force reload environment variables from repo .env, ignoring system vars.
|
|
|
|
# Force reload environment variables from repo .env, ignoring system vars.
|
|
|
|
load_dotenv(find_dotenv(), override=True)
|
|
|
|
load_dotenv(DOTENV_PATH, override=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _post(api_key: str, payload: dict) -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
def _post(api_key: str, payload: dict) -> tuple[dict, int]:
|
|
|
|
headers = {
|
|
|
|
headers = {
|
|
|
|
"ApiKey": api_key,
|
|
|
|
"ApiKey": api_key,
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
@@ -35,14 +37,15 @@ def _post(api_key: str, payload: dict) -> dict:
|
|
|
|
timeout=TIMEOUT,
|
|
|
|
timeout=TIMEOUT,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if response.status_code != 200:
|
|
|
|
status_code = response.status_code
|
|
|
|
raise RuntimeError(f"Rivile HTTP {response.status_code}: {response.text}")
|
|
|
|
if status_code != 200:
|
|
|
|
|
|
|
|
raise RuntimeError(f"Rivile HTTP {status_code}: {response.text}")
|
|
|
|
|
|
|
|
|
|
|
|
data = response.json()
|
|
|
|
data = response.json()
|
|
|
|
if "errorMessage" in data:
|
|
|
|
if "errorMessage" in data:
|
|
|
|
raise RuntimeError(f"Rivile API error: {data}")
|
|
|
|
raise RuntimeError(f"Rivile API error: {data}")
|
|
|
|
|
|
|
|
|
|
|
|
return data
|
|
|
|
return data, status_code
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_api_key() -> str:
|
|
|
|
def _get_api_key() -> str:
|
|
|
|
@@ -62,11 +65,10 @@ def _fetch_mssql_client(
|
|
|
|
) -> Optional[Dict[str, object]]:
|
|
|
|
) -> Optional[Dict[str, object]]:
|
|
|
|
query = """
|
|
|
|
query = """
|
|
|
|
SELECT
|
|
|
|
SELECT
|
|
|
|
N08_KODAS,
|
|
|
|
|
|
|
|
N08_KODAS_KS,
|
|
|
|
N08_KODAS_KS,
|
|
|
|
N08_PAV,
|
|
|
|
N08_PAV,
|
|
|
|
N08_ADDR,
|
|
|
|
N08_ADR,
|
|
|
|
N08_E_EMAIL,
|
|
|
|
N08_E_MAIL,
|
|
|
|
N08_ADD_DATE,
|
|
|
|
N08_ADD_DATE,
|
|
|
|
N08_MOB_TEL,
|
|
|
|
N08_MOB_TEL,
|
|
|
|
N08_IM_KODAS,
|
|
|
|
N08_IM_KODAS,
|
|
|
|
@@ -95,8 +97,10 @@ def _fetch_mssql_client(
|
|
|
|
return dict(zip(columns, row))
|
|
|
|
return dict(zip(columns, row))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _normalize_person_type(value: Optional[str]) -> str:
|
|
|
|
def _normalize_person_type(value: Optional[object]) -> str:
|
|
|
|
cleaned = (value or "").strip()
|
|
|
|
if value is None:
|
|
|
|
|
|
|
|
return "1"
|
|
|
|
|
|
|
|
cleaned = str(value).strip()
|
|
|
|
if cleaned == "1":
|
|
|
|
if cleaned == "1":
|
|
|
|
return "1"
|
|
|
|
return "1"
|
|
|
|
if cleaned == "0":
|
|
|
|
if cleaned == "0":
|
|
|
|
@@ -109,21 +113,32 @@ def _build_n08_payload(row: Dict[str, object]) -> Dict[str, object]:
|
|
|
|
phone = (row.get("phone") or "").strip()
|
|
|
|
phone = (row.get("phone") or "").strip()
|
|
|
|
contact_phone = mobile or phone
|
|
|
|
contact_phone = mobile or phone
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
company_code = row.get("company_code")
|
|
|
|
|
|
|
|
if company_code is None or str(company_code).strip() == "":
|
|
|
|
|
|
|
|
company_code = "ND"
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
company_code = str(company_code).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vat_code = row.get("vat_code")
|
|
|
|
|
|
|
|
if vat_code is None or str(vat_code).strip() == "":
|
|
|
|
|
|
|
|
vat_code = "ND"
|
|
|
|
|
|
|
|
else:
|
|
|
|
|
|
|
|
vat_code = str(vat_code).strip()
|
|
|
|
|
|
|
|
|
|
|
|
creation_date = row.get("creation_date")
|
|
|
|
creation_date = row.get("creation_date")
|
|
|
|
if isinstance(creation_date, (date, datetime)):
|
|
|
|
if isinstance(creation_date, (date, datetime)):
|
|
|
|
creation_date = creation_date.isoformat()
|
|
|
|
creation_date = creation_date.isoformat()
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
return {
|
|
|
|
"N08_KODAS": row.get("client_code"),
|
|
|
|
|
|
|
|
"N08_KODAS_KS": row.get("client_code"),
|
|
|
|
"N08_KODAS_KS": row.get("client_code"),
|
|
|
|
"N08_PAV": row.get("name") or "",
|
|
|
|
"N08_PAV": row.get("name"),
|
|
|
|
"N08_ADDR": row.get("address") or "",
|
|
|
|
"N08_ADR": row.get("address"),
|
|
|
|
"N08_E_EMAIL": row.get("email") or "",
|
|
|
|
"N08_E_MAIL": row.get("email"),
|
|
|
|
"N08_ADD_DATE": creation_date or "",
|
|
|
|
"N08_ADD_DATE": creation_date,
|
|
|
|
"N08_MOB_TEL": contact_phone,
|
|
|
|
"N08_MOB_TEL": contact_phone,
|
|
|
|
"N08_IM_KODAS": row.get("company_code") or "",
|
|
|
|
"N08_IM_KODAS": company_code,
|
|
|
|
"N08_PVM_KODAS": row.get("vat_code") or "",
|
|
|
|
"N08_PVM_KODAS": vat_code,
|
|
|
|
"N08_ADDUSR": row.get("ucreated") or "",
|
|
|
|
"N08_ADDUSR": row.get("ucreated"),
|
|
|
|
"N08_TIPAS": _normalize_person_type(row.get("person_type")),
|
|
|
|
"N08_TIPAS": _normalize_person_type(row.get("person_type")),
|
|
|
|
"N08_KODAS_DS": "PT001",
|
|
|
|
"N08_KODAS_DS": "PT001",
|
|
|
|
"N08_BUSENA": "1",
|
|
|
|
"N08_BUSENA": "1",
|
|
|
|
@@ -134,7 +149,7 @@ def _build_n08_payload(row: Dict[str, object]) -> Dict[str, object]:
|
|
|
|
"N08_KODAS_XS_T": "NEPVM",
|
|
|
|
"N08_KODAS_XS_T": "NEPVM",
|
|
|
|
"N08_KODAS_XS_P": "NEPVM",
|
|
|
|
"N08_KODAS_XS_P": "NEPVM",
|
|
|
|
"N08_RUSIS": "1",
|
|
|
|
"N08_RUSIS": "1",
|
|
|
|
"N08_R_DATE": date.today().isoformat(),
|
|
|
|
"N08_R_DATE": datetime.now().isoformat(timespec="seconds"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -164,10 +179,14 @@ def _diff_fields(
|
|
|
|
return changes
|
|
|
|
return changes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _upsert_client(
|
|
|
|
def _upsert_client(
|
|
|
|
api_key: str,
|
|
|
|
api_key: str,
|
|
|
|
mssql_conn: "pyodbc.Connection",
|
|
|
|
mssql_conn: "pyodbc.Connection",
|
|
|
|
row: Dict[str, object],
|
|
|
|
row: Dict[str, object],
|
|
|
|
|
|
|
|
index: int,
|
|
|
|
|
|
|
|
total: int,
|
|
|
|
) -> None:
|
|
|
|
) -> None:
|
|
|
|
client_code = str(row.get("client_code") or "").strip()
|
|
|
|
client_code = str(row.get("client_code") or "").strip()
|
|
|
|
if not client_code:
|
|
|
|
if not client_code:
|
|
|
|
@@ -176,13 +195,17 @@ def _upsert_client(
|
|
|
|
n08 = _build_n08_payload(row)
|
|
|
|
n08 = _build_n08_payload(row)
|
|
|
|
user = _get_user_from_key(api_key)
|
|
|
|
user = _get_user_from_key(api_key)
|
|
|
|
existing = _fetch_mssql_client(mssql_conn, client_code)
|
|
|
|
existing = _fetch_mssql_client(mssql_conn, client_code)
|
|
|
|
|
|
|
|
if existing:
|
|
|
|
|
|
|
|
existing_web = _normalize_value(existing.get("N08_WEB_POZT"))
|
|
|
|
|
|
|
|
if existing_web:
|
|
|
|
|
|
|
|
n08["N08_WEB_POZT"] = existing_web
|
|
|
|
changes = _diff_fields(existing, n08)
|
|
|
|
changes = _diff_fields(existing, n08)
|
|
|
|
|
|
|
|
|
|
|
|
if existing:
|
|
|
|
if existing:
|
|
|
|
if not changes:
|
|
|
|
if not changes or set(changes.keys()) == {"N08_R_DATE"}:
|
|
|
|
print(f"No changes for client: {client_code}")
|
|
|
|
print(f"No changes for client: {client_code} ({index}/{total})")
|
|
|
|
return
|
|
|
|
return
|
|
|
|
print(f"Updating client: {client_code}")
|
|
|
|
print(f"Updating client: {client_code} ({index}/{total})")
|
|
|
|
for field, (old, new) in changes.items():
|
|
|
|
for field, (old, new) in changes.items():
|
|
|
|
print(f" {field}: '{old}' -> '{new}'")
|
|
|
|
print(f" {field}: '{old}' -> '{new}'")
|
|
|
|
payload = {
|
|
|
|
payload = {
|
|
|
|
@@ -196,7 +219,7 @@ def _upsert_client(
|
|
|
|
"data": {"N08": n08},
|
|
|
|
"data": {"N08": n08},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else:
|
|
|
|
else:
|
|
|
|
print(f"Creating client: {client_code}")
|
|
|
|
print(f"Creating client: {client_code} ({index}/{total})")
|
|
|
|
for field, (_, new) in changes.items():
|
|
|
|
for field, (_, new) in changes.items():
|
|
|
|
print(f" {field}: '' -> '{new}'")
|
|
|
|
print(f" {field}: '' -> '{new}'")
|
|
|
|
payload = {
|
|
|
|
payload = {
|
|
|
|
@@ -205,7 +228,8 @@ def _upsert_client(
|
|
|
|
"data": {"N08": n08},
|
|
|
|
"data": {"N08": n08},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_post(api_key, payload)
|
|
|
|
_, status_code = _post(api_key, payload)
|
|
|
|
|
|
|
|
print(f" Rivile response status: {status_code}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _read_query() -> str:
|
|
|
|
def _read_query() -> str:
|
|
|
|
@@ -226,13 +250,14 @@ def _fetch_clients() -> Iterable[Dict[str, object]]:
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
def main() -> None:
|
|
|
|
api_key = _get_api_key()
|
|
|
|
api_key = _get_api_key()
|
|
|
|
rows = _fetch_clients()
|
|
|
|
rows = list(_fetch_clients())
|
|
|
|
mssql_conn = connect_to_mssql()
|
|
|
|
mssql_conn = connect_to_mssql()
|
|
|
|
if mssql_conn is None:
|
|
|
|
if mssql_conn is None:
|
|
|
|
raise RuntimeError("Failed to connect to MSSQL.")
|
|
|
|
raise RuntimeError("Failed to connect to MSSQL.")
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
for row in rows:
|
|
|
|
total = len(rows)
|
|
|
|
_upsert_client(api_key, mssql_conn, row)
|
|
|
|
for index, row in enumerate(rows, start=1):
|
|
|
|
|
|
|
|
_upsert_client(api_key, mssql_conn, row, index, total)
|
|
|
|
finally:
|
|
|
|
finally:
|
|
|
|
mssql_conn.close()
|
|
|
|
mssql_conn.close()
|
|
|
|
|
|
|
|
|
|
|
|
|