Full sutartis port works

This commit is contained in:
2026-02-02 18:37:50 +02:00
parent 8c749a4968
commit 891ae221e0
6 changed files with 581 additions and 16 deletions

View File

@@ -0,0 +1,170 @@
from __future__ import annotations
from pathlib import Path
from typing import Dict, Iterable, Optional
import psycopg2
import psycopg2.extras
from dotenv import load_dotenv
from uv_app.core.mssql import connect_to_mssql
from uv_app.core.pgsql import connect_to_pgsql
MIN_YEAR = 2000
OPEN_END_DATE = "3999-01-01"
SERVICE_CODE = "STATUSAS"
DOTENV_PATH = Path(__file__).resolve().parents[2] / ".env"
load_dotenv(DOTENV_PATH, override=True)
QUERY_PATH = Path(__file__).with_name("bukle_by_sutarties_kodas.sql")
def _read_query() -> str:
return QUERY_PATH.read_text(encoding="utf-8")
def _fetch_pg_rows(sutarties_kodas: str) -> Iterable[Dict[str, object]]:
conn = connect_to_pgsql()
if conn is None:
raise RuntimeError("Failed to connect to PostgreSQL.")
try:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
cursor.execute(_read_query(), (sutarties_kodas,))
return cursor.fetchall()
finally:
conn.close()
def _fetch_mssql_appendices(contract_code: str) -> list[dict[str, object]]:
conn = connect_to_mssql()
if conn is None:
raise RuntimeError("Failed to connect to MSSQL.")
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
N52_KODAS_KT,
N52_KODAS_K0,
N52_DOK_NR,
N52_KODAS,
CONVERT(date, N52_BEG_DATE) AS N52_BEG_DATE,
CONVERT(date, N52_END_DATE) AS N52_END_DATE,
N52_VISKAS
FROM dbo.N52_SUTD
WHERE N52_KODAS_KT = ?
AND N52_KODAS = ?
""",
(contract_code, SERVICE_CODE),
)
rows = cursor.fetchall()
columns = [c[0] for c in cursor.description]
return [dict(zip(columns, row)) for row in rows]
finally:
conn.close()
def _normalize_date(value: object) -> str:
if value is None:
return ""
return str(value)
def _build_periods(rows: list[dict[str, object]]) -> list[dict[str, object]]:
normalized = []
for row in rows:
start = row.get("data")
if start is None or str(start) < f"{MIN_YEAR}-01-01":
start = f"{MIN_YEAR}-01-01"
normalized.append(
{
"start": start,
"bukle": int(row["bukle"]),
}
)
normalized.sort(key=lambda item: str(item["start"]))
periods = []
for idx, item in enumerate(normalized):
start_date = str(item["start"])
if idx == len(normalized) - 1:
end_date = OPEN_END_DATE
else:
next_start = normalized[idx + 1]["start"]
end_date = str(next_start - date.resolution)
active = item["bukle"] in {2, 3}
periods.append(
{
"start": start_date,
"end": end_date,
"active": active,
"k0": f"{SERVICE_CODE}-{idx + 1}",
"dok_nr": str(idx + 1),
}
)
return periods
def _explain_mismatch(existing: list[dict[str, object]], desired: list[dict[str, object]]) -> None:
print("=== Desired (from PGSQL) ===")
for row in desired:
print(row)
print("=== Existing (from MSSQL) ===")
for row in existing:
print(
{
"k0": str(row.get("N52_KODAS_K0") or "").strip(),
"dok_nr": str(row.get("N52_DOK_NR") or "").strip(),
"start": _normalize_date(row.get("N52_BEG_DATE")),
"end": _normalize_date(row.get("N52_END_DATE")),
"active": int(row.get("N52_VISKAS") or 0) == 1,
}
)
existing_keys = {
(
_normalize_date(row.get("N52_BEG_DATE")),
_normalize_date(row.get("N52_END_DATE")),
int(row.get("N52_VISKAS") or 0) == 1,
str(row.get("N52_KODAS_K0") or "").strip(),
str(row.get("N52_DOK_NR") or "").strip(),
)
for row in existing
}
desired_keys = {
(
row["start"],
row["end"],
row["active"],
row["k0"],
row["dok_nr"],
)
for row in desired
}
print("=== Missing in MSSQL ===")
for key in sorted(desired_keys - existing_keys):
print(key)
print("=== Extra in MSSQL ===")
for key in sorted(existing_keys - desired_keys):
print(key)
def main() -> None:
sutarties_kodas = input("Sutarties kodas: ").strip()
if not sutarties_kodas:
print("Missing sutarties kodas.")
return
rows = list(_fetch_pg_rows(sutarties_kodas))
if not rows:
print("No bukle rows found in PGSQL.")
return
contract_code = f"SUT-{sutarties_kodas}"
existing = _fetch_mssql_appendices(contract_code)
desired = _build_periods(rows)
_explain_mismatch(existing, desired)
if __name__ == "__main__":
main()

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
import os
from datetime import date
from datetime import date, timedelta
from pathlib import Path
from typing import Dict, Iterable, Optional
@@ -274,21 +274,17 @@ def _is_desired_state(
) -> bool:
if len(existing) != len(periods):
return False
existing_by_k0 = {str(r.get("N52_KODAS_K0") or "").strip(): r for r in existing}
for idx, period in enumerate(periods, start=1):
expected_k0 = f"{SERVICE_CODE}-{idx}"
row = existing_by_k0.get(expected_k0)
if not row:
existing_keys = []
for row in existing:
start = row.get("N52_BEG_DATE")
end = row.get("N52_END_DATE")
if not start or not end:
return False
if str(row.get("N52_DOK_NR") or "").strip() != str(idx):
return False
if row.get("N52_BEG_DATE") != period["start"]:
return False
if row.get("N52_END_DATE") != period["end"]:
return False
if bool(row.get("N52_VISKAS")) != period["active"]:
return False
return True
existing_keys.append((start, end))
desired_keys = [(p["start"], p["end"]) for p in periods]
existing_keys.sort()
desired_keys.sort()
return existing_keys == desired_keys
def _normalize_date(value: Optional[date]) -> date:
@@ -317,7 +313,7 @@ def _build_periods(rows: list[dict[str, object]]) -> list[dict[str, object]]:
end_date = OPEN_END_DATE
else:
next_start = normalized[idx + 1]["start"]
end_date = next_start - date.resolution
end_date = next_start - timedelta(days=1)
bukle = item["bukle"]
active = bukle in {2, 3}
periods.append(

View File

@@ -0,0 +1,38 @@
from __future__ import annotations
import subprocess
import sys
from pathlib import Path
SCRIPTS = [
"port_sutartis_to_rivile.py",
"port_menesine_kaina_to_rivile.py",
"port_bukle_to_rivile.py",
]
def _run_script(script_path: Path, sutarties_kodas: str) -> None:
result = subprocess.run(
[sys.executable, str(script_path)],
input=f"{sutarties_kodas}\n",
text=True,
check=True,
)
return None
def main() -> None:
sutarties_kodas = input("Sutarties kodas: ").strip()
if not sutarties_kodas:
print("Missing sutarties kodas.")
return
base_dir = Path(__file__).resolve().parent
for script_name in SCRIPTS:
script_path = base_dir / script_name
print(f"Running {script_name}...")
_run_script(script_path, sutarties_kodas)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,321 @@
from __future__ import annotations
import os
from datetime import date, datetime
from pathlib import Path
from typing import Dict, Iterable, Optional
import psycopg2
import psycopg2.extras
import requests
from dotenv import load_dotenv
from uv_app.core.mssql import connect_to_mssql
from uv_app.core.pgsql import connect_to_pgsql
BASE_URL = "https://api.manorivile.lt/client/v2"
TIMEOUT = 30
MIN_YEAR = 2000
DOTENV_PATH = Path(__file__).resolve().parents[2] / ".env"
load_dotenv(DOTENV_PATH, override=True)
QUERY_PATH = Path(__file__).with_name("sutartys_by_sutarties_kodas.sql")
def _post(api_key: str, payload: dict) -> dict:
headers = {
"ApiKey": api_key,
"Content-Type": "application/json",
"Accept": "application/json",
}
response = requests.post(
BASE_URL,
json=payload,
headers=headers,
timeout=TIMEOUT,
)
if response.status_code != 200:
raise RuntimeError(f"Rivile HTTP {response.status_code}: {response.text}")
data = response.json()
if "errorMessage" in data:
raise RuntimeError(f"Rivile API error: {data}")
return data
def _get_api_key() -> str:
api_key = os.getenv("RIVILE_API_KEY", "").strip()
if not api_key:
raise RuntimeError("Missing RIVILE_API_KEY environment variable.")
return api_key
def _read_query() -> str:
return QUERY_PATH.read_text(encoding="utf-8")
def _fetch_rows(sutarties_kodas: str) -> Iterable[Dict[str, object]]:
conn = connect_to_pgsql()
if conn is None:
raise RuntimeError("Failed to connect to PostgreSQL.")
try:
with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cursor:
cursor.execute(_read_query(), (sutarties_kodas,))
return cursor.fetchall()
finally:
conn.close()
def _contract_exists(conn: "pyodbc.Connection", contract_code: str) -> bool:
cursor = conn.cursor()
cursor.execute(
"""
SELECT 1
FROM dbo.N51_SUTH
WHERE N51_KODAS_KT = ?
""",
(contract_code,),
)
return cursor.fetchone() is not None
def _fetch_existing_contract(
conn: "pyodbc.Connection",
contract_code: str,
) -> Optional[Dict[str, object]]:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
N51_KODAS_KT,
N51_KODAS_KS,
N51_PAV,
CONVERT(date, N51_OP_DATA) AS N51_OP_DATA,
CONVERT(date, N51_BEG_DATE) AS N51_BEG_DATE,
N51_TIPAS,
N51_APRASYMAS1,
N51_APRASYMAS2
FROM dbo.N51_SUTH
WHERE N51_KODAS_KT = ?
""",
(contract_code,),
)
row = cursor.fetchone()
if not row:
return None
columns = [c[0] for c in cursor.description]
return dict(zip(columns, row))
def _normalize_text(value: object) -> str:
if value is None:
return ""
return str(value).strip()
def _normalize_date(value: object) -> date:
if isinstance(value, date):
result = value
elif isinstance(value, datetime):
result = value.date()
else:
return date.today()
if result < date(MIN_YEAR, 1, 1):
return date.today()
return result
def _is_desired_contract(
existing: Optional[Dict[str, object]],
contract_code: str,
client_code: str,
contract_type: str,
start: date,
address: str,
password: str,
) -> bool:
if not existing:
return False
return (
_normalize_text(existing.get("N51_KODAS_KT")) == contract_code
and _normalize_text(existing.get("N51_KODAS_KS")) == client_code
and _normalize_text(existing.get("N51_PAV")) == contract_code
and _normalize_date(existing.get("N51_OP_DATA")) == start
and _normalize_date(existing.get("N51_BEG_DATE")) == start
and _normalize_text(existing.get("N51_TIPAS")) == contract_type
and _normalize_text(existing.get("N51_APRASYMAS1")) == address
and _normalize_text(existing.get("N51_APRASYMAS2")) == password
)
def _choose_contract_type(conn: "pyodbc.Connection") -> Optional[str]:
cursor = conn.cursor()
cursor.execute(
"""
SELECT N51_TIPAS, COUNT(*) AS cnt
FROM dbo.N51_SUTH
WHERE N51_TIPAS IS NOT NULL
GROUP BY N51_TIPAS
ORDER BY cnt DESC, N51_TIPAS
"""
)
row = cursor.fetchone()
if not row:
return None
return str(row[0]).strip()
def _build_address(row: Dict[str, object]) -> str:
parts = []
gatve = (row.get("gatve") or "").strip()
namas = (row.get("namas") or "").strip()
miestas = (row.get("miestas") or "").strip()
rajonas = (row.get("rajonas") or "").strip()
if gatve or namas:
parts.append(" ".join(p for p in [gatve, namas] if p))
if miestas:
parts.append(miestas)
if rajonas:
parts.append(rajonas)
return ", ".join(parts)
def _create_contract(
api_key: str,
contract_code: str,
client_code: str,
start: date,
contract_type: str,
address: str,
password: str,
) -> None:
payload = {
"method": "EDIT_N51_FULL",
"params": {
"oper": "I",
"user": api_key.split(".", 1)[0],
},
"data": {
"N51": {
"N51_KODAS_KT": contract_code,
"N51_KODAS_KS": client_code,
"N51_PAV": contract_code,
"N51_OP_DATA": start.isoformat(),
"N51_BEG_DATE": start.isoformat(),
"N51_TIPAS": contract_type,
"N51_APRASYMAS1": address,
"N51_APRASYMAS2": password,
}
},
}
_post(api_key, payload)
def _update_contract(
api_key: str,
contract_code: str,
client_code: str,
contract_type: str,
address: str,
password: str,
start: date,
) -> None:
payload = {
"method": "EDIT_N51",
"params": {
"oper": "U",
"user": api_key.split(".", 1)[0],
"fld": "N51_KODAS_KT",
"val": contract_code,
},
"data": {
"N51": {
"N51_KODAS_KT": contract_code,
"N51_KODAS_KS": client_code,
"N51_PAV": contract_code,
"N51_TIPAS": contract_type,
"N51_OP_DATA": start.isoformat(),
"N51_BEG_DATE": start.isoformat(),
"N51_APRASYMAS1": address,
"N51_APRASYMAS2": password,
}
},
}
_post(api_key, payload)
def main() -> None:
sutarties_kodas = input("Sutarties kodas: ").strip()
if not sutarties_kodas:
print("Missing sutarties kodas.")
return
rows = list(_fetch_rows(sutarties_kodas))
if not rows:
print("No sutartis rows found.")
return
row = rows[0]
client_code = str(row.get("client_code") or "").strip()
if not client_code:
raise RuntimeError("Missing client code for sutartis.")
contract_code = f"SUT-{sutarties_kodas}"
start_date = _normalize_date(row.get("data"))
address = _build_address(row)
password = str(row.get("password") or "").strip()
api_key = _get_api_key()
mssql_conn = connect_to_mssql()
if mssql_conn is None:
raise RuntimeError("Failed to connect to MSSQL.")
try:
contract_type = _choose_contract_type(mssql_conn)
if not contract_type:
raise RuntimeError("Failed to resolve N51_TIPAS for Pardavimo.")
existing = _fetch_existing_contract(mssql_conn, contract_code)
if _is_desired_contract(
existing,
contract_code,
client_code,
contract_type,
start_date,
address,
password,
):
print("Already in desired state; no changes.")
return
if existing:
_update_contract(
api_key,
contract_code,
client_code,
contract_type,
address,
password,
start_date,
)
print(f"Updated contract {contract_code}")
else:
_create_contract(
api_key,
contract_code,
client_code,
start_date,
contract_type,
address,
password,
)
print(f"Created contract {contract_code}")
finally:
mssql_conn.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,20 @@
SELECT
s.*,
t.pav AS sutartis_type,
k.kodas AS client_code,
k.vardas,
k.pavarde,
k.email,
k.mob_telefonas,
k.telefonas,
g.pav AS gatve,
m.pav AS miestas,
r.pav AS rajonas
FROM sutartis s
JOIN klientas k ON k.klientasid = s.klientasid
LEFT JOIN sutartis_type t ON t.sutartis_typeid = s.sutartis_typeid
LEFT JOIN gatve g ON g.gatveid = s.gatveid
LEFT JOIN miestas m ON m.miestasid = s.miestasid
LEFT JOIN rajonas r ON r.rajonasid = s.rajonasid
WHERE k.kodas = 'XXXXXX'
ORDER BY s.sutartisid;

View File

@@ -0,0 +1,20 @@
SELECT
s.*,
t.pav AS sutartis_type,
k.kodas AS client_code,
k.vardas,
k.pavarde,
k.email,
k.mob_telefonas,
k.telefonas,
g.pav AS gatve,
m.pav AS miestas,
r.pav AS rajonas
FROM sutartis s
JOIN klientas k ON k.klientasid = s.klientasid
LEFT JOIN sutartis_type t ON t.sutartis_typeid = s.sutartis_typeid
LEFT JOIN gatve g ON g.gatveid = s.gatveid
LEFT JOIN miestas m ON m.miestasid = s.miestasid
LEFT JOIN rajonas r ON r.rajonasid = s.rajonasid
WHERE s.sutartiesid = %s
ORDER BY s.sutartisid;