AutoRpa/rpa_rebate/repo.py

368 lines
12 KiB
Python

"""表 4 起草行领取、表 3 只录协议行领取、表 4 写入。"""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from db import RepoUnavailable, connect_from_env, oracle_dsn
from persist import STAMP_CLAIMING, STAMP_CONTRACT, STAMP_DONE, GroupSave
ACTION_CONTRACT = 1
CONTRACT_DRAFT = 1
PROCESS_PENDING = 0
PROCESS_CLAIMING = 3
ENTRY_PROTOCOL_ONLY = 0
@dataclass
class Claimed:
drafts: list[dict] = field(default_factory=list)
protocols: list[dict] = field(default_factory=list)
def _has_contract_no(row: dict) -> bool:
return bool((row.get("contract_no") or "").strip())
def is_draft(row: dict) -> bool:
if int(row.get("action_code") or 0) != ACTION_CONTRACT:
return False
if int(row.get("contract_status") or 0) != CONTRACT_DRAFT:
return False
if not _has_contract_no(row):
return False
stamp = row.get("update_user_name") or STAMP_CONTRACT
return stamp == STAMP_CONTRACT
def is_protocol_only(row: dict) -> bool:
if int(row.get("process_status") or 0) != PROCESS_PENDING:
return False
entry = row.get("contract_entry_status")
if entry is None or int(entry) != ENTRY_PROTOCOL_ONLY:
return False
return _has_contract_no(row)
class MemoryRebateRepo:
def __init__(
self,
table4: list[dict] | None = None,
table3: list[dict] | None = None,
):
self.table4: dict[int, dict] = {
int(row["fid"]): dict(row) for row in (table4 or [])
}
self.table3: dict[int, dict] = {
int(row["fid"]): dict(row) for row in (table3 or [])
}
self._next_fid = (max(self.table4) + 1) if self.table4 else 1
def claim_pending(self, limit: int | None = None) -> Claimed:
drafts = sorted(
(row for row in self.table4.values() if is_draft(row)),
key=lambda row: int(row["fid"]),
)
protocols = sorted(
(row for row in self.table3.values() if is_protocol_only(row)),
key=lambda row: int(row["fid"]),
)
if limit is not None:
if len(drafts) >= limit:
drafts = drafts[:limit]
protocols = []
else:
protocols = protocols[: max(0, limit - len(drafts))]
for row in drafts:
row["update_user_name"] = STAMP_CLAIMING
for row in protocols:
row["process_status"] = PROCESS_CLAIMING
return Claimed(drafts=list(drafts), protocols=list(protocols))
def save_group(self, payload: GroupSave) -> None:
for item in payload.table4_updates:
target = self.table4[int(item["fid"])]
target["contract_status"] = item["contract_status"]
target["update_user_name"] = item["update_user_name"]
for fid in payload.table4_release_fids:
self.table4[int(fid)]["update_user_name"] = STAMP_CONTRACT
for row in payload.table4_inserts:
item = dict(row)
item["fid"] = self._next_fid
self._next_fid += 1
self.table4[item["fid"]] = item
for item in payload.table3_updates:
target = self.table3[int(item["fid"])]
target["process_status"] = item["process_status"]
target["process_result"] = item["process_result"]
def load_contract_goods(self, contract_no: str) -> list[dict]:
number = (contract_no or "").strip()
oms_ids = {
int(row["oms_id"])
for row in self.table4.values()
if (row.get("contract_no") or "").strip() == number and row.get("oms_id") is not None
}
return [
dict(row)
for row in self.table3.values()
if (row.get("contract_no") or "").strip() == number
or int(row.get("oms_id") or 0) in oms_ids
]
_SELECT_DRAFTS = """
SELECT
fid, oms_id, action_code, contract_status, contract_fail_reason,
contract_no, agreement_status, agreement_fail_reason,
agreement_id, agreement_dtl_id, org_code, push_status,
NVL(update_user_name, '合同班') AS update_user_name
FROM rpa_contract_agreement_result
WHERE action_code = 1
AND contract_status = 1
AND contract_no IS NOT NULL
AND LENGTH(TRIM(contract_no)) > 0
AND NVL(update_user_name, '合同班') = '合同班'
ORDER BY fid
"""
_SELECT_DRAFTS_LIMIT = f"""
SELECT * FROM (
{_SELECT_DRAFTS}
) WHERE ROWNUM <= :limit
"""
_CLAIM_DRAFT_SQL = """
UPDATE rpa_contract_agreement_result
SET update_user_name = :stamp,
update_time = SYSTIMESTAMP
WHERE fid = :fid
AND action_code = 1
AND contract_status = 1
AND NVL(update_user_name, :contract_stamp) = :contract_stamp
"""
_SELECT_PROTOCOLS = """
SELECT
fid, oms_id, contract_no, contract_entry_status, process_status, org_code,
NVL(process_result, '') AS process_result
FROM rpa_purchase_contract_data
WHERE process_status = 0
AND NVL(contract_entry_status, -1) = 0
AND contract_no IS NOT NULL
AND LENGTH(TRIM(contract_no)) > 0
ORDER BY fid
"""
_SELECT_PROTOCOLS_LIMIT = f"""
SELECT * FROM (
{_SELECT_PROTOCOLS}
) WHERE ROWNUM <= :limit
"""
_CLAIM_PROTOCOL_SQL = """
UPDATE rpa_purchase_contract_data
SET process_status = 3,
process_time = SYSTIMESTAMP,
update_time = SYSTIMESTAMP
WHERE fid = :fid AND process_status = 0
"""
_UPDATE_DRAFT_SQL = """
UPDATE rpa_contract_agreement_result
SET contract_status = :contract_status,
update_user_id = 0,
update_user_name = :stamp,
update_time = SYSTIMESTAMP
WHERE fid = :fid AND update_user_name = :claim_stamp
"""
_RELEASE_DRAFT_SQL = """
UPDATE rpa_contract_agreement_result
SET update_user_name = :stamp,
update_time = SYSTIMESTAMP
WHERE fid = :fid AND update_user_name = :claim_stamp
"""
_INSERT_TABLE4_SQL = """
INSERT INTO rpa_contract_agreement_result (
oms_id, action_code, contract_status, contract_fail_reason, contract_no,
agreement_status, agreement_fail_reason, agreement_id, agreement_dtl_id,
push_status, create_time, update_time,
create_user_id, create_user_name, update_user_id, update_user_name, org_code
) VALUES (
:oms_id, :action_code, :contract_status, :contract_fail_reason, :contract_no,
:agreement_status, :agreement_fail_reason, :agreement_id, :agreement_dtl_id,
0, SYSTIMESTAMP, SYSTIMESTAMP,
0, '返利班', 0, '返利班', :org_code
)
"""
_COMPLETE_PROTOCOL_SQL = """
UPDATE rpa_purchase_contract_data
SET process_status = :process_status,
process_result = :process_result,
process_time = SYSTIMESTAMP,
update_time = SYSTIMESTAMP
WHERE fid = :fid AND process_status = 3
"""
_TABLE4_BIND_KEYS = (
"oms_id",
"action_code",
"contract_status",
"contract_fail_reason",
"contract_no",
"agreement_status",
"agreement_fail_reason",
"agreement_id",
"agreement_dtl_id",
"org_code",
)
_SELECT_CONTRACT_GOODS = """
SELECT
fid, oms_id, supplier_id, supplier_name, supplier_mdm_code,
planner_id, planner_name, purchaser_name, org_code,
goods_id, qty, unit_price, goods_source_id, purchase_tax_rate,
contract_start_date, contract_end_date, payment_term
FROM rpa_purchase_contract_data
WHERE TRIM(contract_no) = :contract_no
OR oms_id IN (
SELECT oms_id FROM rpa_contract_agreement_result
WHERE TRIM(contract_no) = :contract_no
)
ORDER BY fid
"""
def _records(cur) -> list[dict]:
cols = [item[0].lower() for item in cur.description]
return [dict(zip(cols, rec)) for rec in cur.fetchall()]
class OracleRebateRepo:
def __init__(self, connect: Callable):
self._connect = connect
@classmethod
def from_env(cls) -> "OracleRebateRepo":
oracle_dsn()
return cls(connect=connect_from_env)
def claim_pending(self, limit: int | None = None) -> Claimed:
conn = self._connect()
try:
with conn.cursor() as cur:
drafts = self._claim_drafts(cur, limit)
remaining = None if limit is None else max(0, int(limit) - len(drafts))
protocols: list[dict] = []
if remaining is None or remaining > 0:
protocols = self._claim_protocols(cur, remaining)
conn.commit()
return Claimed(drafts=drafts, protocols=protocols)
except Exception as exc:
try:
conn.rollback()
except Exception:
pass
raise RepoUnavailable(str(exc)) from exc
finally:
conn.close()
def load_contract_goods(self, contract_no: str) -> list[dict]:
number = (contract_no or "").strip()
if not number:
return []
conn = self._connect()
try:
with conn.cursor() as cur:
cur.execute(_SELECT_CONTRACT_GOODS, {"contract_no": number})
return _records(cur)
except Exception as exc:
raise RepoUnavailable(str(exc)) from exc
finally:
conn.close()
def save_group(self, payload: GroupSave) -> None:
conn = self._connect()
try:
with conn.cursor() as cur:
for item in payload.table4_updates:
cur.execute(
_UPDATE_DRAFT_SQL,
{
"fid": int(item["fid"]),
"contract_status": item["contract_status"],
"stamp": STAMP_DONE,
"claim_stamp": STAMP_CLAIMING,
},
)
for fid in payload.table4_release_fids:
cur.execute(
_RELEASE_DRAFT_SQL,
{
"fid": int(fid),
"stamp": STAMP_CONTRACT,
"claim_stamp": STAMP_CLAIMING,
},
)
for row in payload.table4_inserts:
cur.execute(
_INSERT_TABLE4_SQL,
{key: row.get(key) for key in _TABLE4_BIND_KEYS},
)
for item in payload.table3_updates:
cur.execute(
_COMPLETE_PROTOCOL_SQL,
{
"fid": int(item["fid"]),
"process_status": item["process_status"],
"process_result": item["process_result"],
},
)
conn.commit()
except Exception as exc:
try:
conn.rollback()
except Exception:
pass
raise RepoUnavailable(str(exc)) from exc
finally:
conn.close()
def _claim_drafts(self, cur, limit: int | None) -> list[dict]:
if limit is None:
cur.execute(_SELECT_DRAFTS)
else:
cur.execute(_SELECT_DRAFTS_LIMIT, {"limit": int(limit)})
fetched = _records(cur)
claimed: list[dict] = []
for row in fetched:
cur.execute(
_CLAIM_DRAFT_SQL,
{
"fid": row["fid"],
"stamp": STAMP_CLAIMING,
"contract_stamp": STAMP_CONTRACT,
},
)
if cur.rowcount == 1:
row["update_user_name"] = STAMP_CLAIMING
claimed.append(row)
return claimed
def _claim_protocols(self, cur, limit: int | None) -> list[dict]:
if limit is None:
cur.execute(_SELECT_PROTOCOLS)
else:
cur.execute(_SELECT_PROTOCOLS_LIMIT, {"limit": int(limit)})
fetched = _records(cur)
claimed: list[dict] = []
for row in fetched:
cur.execute(_CLAIM_PROTOCOL_SQL, {"fid": row["fid"]})
if cur.rowcount == 1:
row["process_status"] = PROCESS_CLAIMING
claimed.append(row)
return claimed