From 8c749a4968ddf12341fb259e2b611005d436e92e Mon Sep 17 00:00:00 2001 From: alzyras Date: Mon, 2 Feb 2026 18:03:26 +0200 Subject: [PATCH] Statusas port script --- uv_app/contracts/bukle_by_sutarties_kodas.sql | 13 + uv_app/contracts/port_bukle_to_rivile.py | 429 ++++++++++++++++++ 2 files changed, 442 insertions(+) create mode 100644 uv_app/contracts/bukle_by_sutarties_kodas.sql create mode 100644 uv_app/contracts/port_bukle_to_rivile.py diff --git a/uv_app/contracts/bukle_by_sutarties_kodas.sql b/uv_app/contracts/bukle_by_sutarties_kodas.sql new file mode 100644 index 0000000..c2ed96f --- /dev/null +++ b/uv_app/contracts/bukle_by_sutarties_kodas.sql @@ -0,0 +1,13 @@ +SELECT + b.bukleid, + b.sutartisid, + s.sutartiesid AS sutarties_kodas, + b.bukle, + b.data, + b.tscreated, + b.ucreated, + b.comments +FROM public.bukle b +JOIN public.sutartis s ON s.sutartisid = b.sutartisid +WHERE s.sutartiesid = %s +ORDER BY b.data, b.bukleid; diff --git a/uv_app/contracts/port_bukle_to_rivile.py b/uv_app/contracts/port_bukle_to_rivile.py new file mode 100644 index 0000000..29cebc5 --- /dev/null +++ b/uv_app/contracts/port_bukle_to_rivile.py @@ -0,0 +1,429 @@ +from __future__ import annotations + +import os +from datetime import date +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 +SERVICE_CODE = "STATUSAS" +MIN_YEAR = 2000 +OPEN_END_DATE = date(3999, 1, 1) + +DOTENV_PATH = Path(__file__).resolve().parents[1] / ".env" +load_dotenv(DOTENV_PATH, override=True) + +QUERY_PATH = Path(__file__).with_name("bukle_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_appendices( + conn: "pyodbc.Connection", + contract_code: str, +) -> list[dict[str, object]]: + 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_KAINA, + N52_PASTABOS, + N52_TERM1, + N52_TERM2, + N52_TERM3, + N52_TERM4, + N52_TERM5, + N52_TERM6, + N52_POZ_TERM, + N52_POZ_TERM_KS, + N52_POZ_DATE, + N52_POZ_KS + 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] + + +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 _create_contract( + api_key: str, + contract_code: str, + client_code: str, + start: date, + contract_type: str, +) -> None: + if start < date(MIN_YEAR, 1, 1): + start = date.today() + 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, + } + }, + } + _post(api_key, payload) + + +def _update_contract( + api_key: str, + contract_code: str, + client_code: str, + contract_type: str, +) -> 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, + } + }, + } + _post(api_key, payload) + + +def _delete_appendix( + api_key: str, + contract_code: str, + kodas_k0: str, + service_code: str, + dok_nr: str, +) -> None: + payload = { + "method": "EDIT_N52", + "params": { + "oper": "D", + "user": api_key.split(".", 1)[0], + "fld": "N52_KODAS_KT,N52_KODAS_K0,N52_KODAS,N52_DOK_NR", + "val": f"{contract_code},{kodas_k0},{service_code},{dok_nr}", + }, + "data": { + "N52": { + "N52_KODAS_KT": contract_code, + "N52_KODAS_K0": kodas_k0, + "N52_KODAS": service_code, + "N52_DOK_NR": dok_nr, + } + }, + } + _post(api_key, payload) + + +def _create_appendix( + api_key: str, + contract_code: str, + kodas_k0: str, + dok_nr: str, + start_date: date, + end_date: date, + active: bool, + comment: Optional[str], +) -> None: + n52 = { + "N52_KODAS_KT": contract_code, + "N52_KODAS_K0": kodas_k0, + "N52_DOK_NR": dok_nr, + "N52_RUSIS": "2", + "N52_KODAS": SERVICE_CODE, + "N52_KIEKIS": "1", + "N52_KAINA": "0", + "N52_OP_DATA": date.today().isoformat(), + "N52_BEG_DATE": start_date.isoformat(), + "N52_END_DATE": end_date.isoformat(), + "N52_TERM1": "1", + "N52_TERM2": "0", + "N52_TERM3": "0", + "N52_TERM4": "0", + "N52_TERM5": "0", + "N52_TERM6": "0", + "N52_POZ_TERM": "1", + "N52_POZ_TERM_KS": "1", + "N52_POZ_DATE": "1", + "N52_POZ_KS": "0", + "N52_VISKAS": "1" if active else "0", + } + if comment: + n52["N52_PASTABOS"] = comment + + payload = { + "method": "EDIT_N52", + "params": { + "oper": "I", + "user": api_key.split(".", 1)[0], + }, + "data": {"N52": n52}, + } + _post(api_key, payload) + + +def _is_desired_state( + existing: list[dict[str, object]], + periods: list[dict[str, object]], +) -> 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: + 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 + + +def _normalize_date(value: Optional[date]) -> date: + if value is None or value < date(MIN_YEAR, 1, 1): + return date(MIN_YEAR, 1, 1) + return value + + +def _build_periods(rows: list[dict[str, object]]) -> list[dict[str, object]]: + normalized = [] + for row in rows: + start = _normalize_date(row.get("data")) + normalized.append( + { + "start": start, + "bukle": int(row["bukle"]), + "comment": row.get("comments"), + } + ) + + normalized.sort(key=lambda item: item["start"]) + periods = [] + for idx, item in enumerate(normalized): + start_date = item["start"] + if idx == len(normalized) - 1: + end_date = OPEN_END_DATE + else: + next_start = normalized[idx + 1]["start"] + end_date = next_start - date.resolution + bukle = item["bukle"] + active = bukle in {2, 3} + periods.append( + { + "start": start_date, + "end": end_date, + "active": active, + "comment": item["comment"], + } + ) + return periods + + +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 bukle rows found.") + return + + contract_code = f"SUT-{sutarties_kodas}" + client_code = str(rows[0].get("sutarties_kodas") or "").strip() + if not client_code: + raise RuntimeError("Missing client code for sutartis.") + + 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.") + + periods = _build_periods(rows) + if not periods: + print("No valid periods found.") + return + + if not _contract_exists(mssql_conn, contract_code): + first = periods[0]["start"] + _create_contract(api_key, contract_code, sutarties_kodas, first, contract_type) + print(f"Created contract {contract_code}") + else: + _update_contract(api_key, contract_code, sutarties_kodas, contract_type) + print(f"Updated contract {contract_code}") + + existing = _fetch_existing_appendices(mssql_conn, contract_code) + + max_desired_start = max(p["start"] for p in periods) + newer_existing = [ + row for row in existing if row.get("N52_BEG_DATE") and row["N52_BEG_DATE"] > max_desired_start + ] + if newer_existing: + print("Found newer MSSQL periods not in PGSQL; skipping changes.") + for row in newer_existing: + print( + f" Existing period: {row.get('N52_BEG_DATE')} - {row.get('N52_END_DATE')}" + ) + return + + if _is_desired_state(existing, periods): + print("Already in desired state; no changes.") + return + + for row in existing: + dok_nr = str(row.get("N52_DOK_NR") or "").strip() + kodas_k0 = str(row.get("N52_KODAS_K0") or "").strip() + if dok_nr and kodas_k0: + _delete_appendix( + api_key, + contract_code, + kodas_k0, + SERVICE_CODE, + dok_nr, + ) + print(f"Deleted appendix {kodas_k0} ({dok_nr})") + + next_number = 1 + for period in periods: + start_date = period["start"] + end_date = period["end"] + active = period["active"] + comment = period["comment"] + + dok_nr = str(next_number) + kodas_k0 = f"{SERVICE_CODE}-{dok_nr}" + _create_appendix( + api_key, + contract_code, + kodas_k0, + dok_nr, + start_date, + end_date, + active, + comment, + ) + print(f"Created appendix for {start_date} - {end_date}") + next_number += 1 + finally: + mssql_conn.close() + + +if __name__ == "__main__": + main()