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