fix: CCA claim submission grid targeting, retry, and dashboard claim-number lookup
- Fix NameError (col_idx vs col_expr) that silently corrupted grid-fill logs - Poll for new grid rows to render before clicking, instead of failing immediately - Abort submission with a clear error if code/tooth/billed amount fail to fill, instead of silently submitting an incomplete claim - Compute grid column positions live from header metadata instead of stale hardcoded indices; billed amount was landing on a hidden helper column (TAB_FROM_CODE_CELL) via td[last()] instead of the real Billed Amt column - Read claim/encounter number from the top (newest) dashboard row instead of the bottom row
This commit is contained in:
@@ -446,14 +446,13 @@ class AutomationCCAClaimSubmit:
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
print(f"[CCA Claim step4] Capturing dashboard — URL: {self.driver.current_url}")
|
print(f"[CCA Claim step4] Capturing dashboard — URL: {self.driver.current_url}")
|
||||||
|
|
||||||
# Extract Encounter ID from the LAST row (newest claim is at the bottom)
|
# Extract Encounter ID from the FIRST row (newest claim is at the top)
|
||||||
claim_number = None
|
claim_number = None
|
||||||
|
|
||||||
# Try table cells first (Encounter ID is the first column)
|
# Try table cells first (Encounter ID is the first column)
|
||||||
for sel in [
|
for sel in [
|
||||||
(By.XPATH, "//table//tbody//tr[last()]/td[1]"),
|
(By.XPATH, "//table//tbody//tr[1]/td[1]"),
|
||||||
(By.XPATH, "//*[contains(@class,'wbx-table-results')]//tr[last()]/td[1]"),
|
(By.XPATH, "//*[contains(@class,'wbx-table-results')]//tr[1]/td[1]"),
|
||||||
(By.XPATH, "//table//tbody//tr[last()-0]/td[1]"),
|
|
||||||
]:
|
]:
|
||||||
try:
|
try:
|
||||||
cell = WebDriverWait(self.driver, 8).until(
|
cell = WebDriverWait(self.driver, 8).until(
|
||||||
@@ -461,7 +460,7 @@ class AutomationCCAClaimSubmit:
|
|||||||
text = cell.text.strip()
|
text = cell.text.strip()
|
||||||
if re.match(r'\d{10,}', text):
|
if re.match(r'\d{10,}', text):
|
||||||
claim_number = text
|
claim_number = text
|
||||||
print(f"[CCA Claim step4] Encounter ID from last row: {claim_number}")
|
print(f"[CCA Claim step4] Encounter ID from top row: {claim_number}")
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
@@ -591,14 +590,12 @@ class AutomationCCAClaimSubmit:
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
def _build_col_map(self):
|
def _build_col_map(self):
|
||||||
"""
|
"""
|
||||||
Map column names to 1-based td positions using ej-mappingname divs.
|
Map column names to 1-based td positions using ej-mappingname divs,
|
||||||
Falls back to hardcoded positions based on the known PDF column order.
|
in the order they actually appear in the live grid header (1-based
|
||||||
|
index = td position). Falls back to hardcoded positions (stale —
|
||||||
|
do not trust for BILLED_AMOUNT) only if the live scan fails.
|
||||||
"""
|
"""
|
||||||
# Hardcoded based on PDF column order:
|
fallback_col_map = {
|
||||||
# rownum(1), Code(2), Desc(3), Tooth(4),
|
|
||||||
# Surf1-5(5-9), OralCavity1-4(10-13), DiagPtr1-4(14-17),
|
|
||||||
# EPSDT(18), Qty(19), Auth(20), ServiceDate(21), BilledAmt(22)
|
|
||||||
col_map = {
|
|
||||||
"CODE": 2,
|
"CODE": 2,
|
||||||
"TOOTH": 4,
|
"TOOTH": 4,
|
||||||
"SURF1": 5, "S1": 5,
|
"SURF1": 5, "S1": 5,
|
||||||
@@ -608,10 +605,9 @@ class AutomationCCAClaimSubmit:
|
|||||||
"SURF5": 9, "S5": 9,
|
"SURF5": 9, "S5": 9,
|
||||||
"QUANTITY": 19, "QTY": 19,
|
"QUANTITY": 19, "QTY": 19,
|
||||||
"DOS": 21, "SERVICE DATE": 21, "SERVICEDATE": 21,
|
"DOS": 21, "SERVICE DATE": 21, "SERVICEDATE": 21,
|
||||||
"BILLED_AMOUNT": 22, "BILLED AMT": 22, "BILLED A": 22,
|
"BILLED_AMOUNT": 19, "BILLED AMT": 19, "BILLED A": 19,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Try to confirm/override positions using ej-mappingname divs in header
|
|
||||||
try:
|
try:
|
||||||
divs = self.driver.find_elements(
|
divs = self.driver.find_elements(
|
||||||
By.XPATH,
|
By.XPATH,
|
||||||
@@ -619,14 +615,29 @@ class AutomationCCAClaimSubmit:
|
|||||||
"//div[@id='ServicesGrid']//div[@ej-mappingname]"
|
"//div[@id='ServicesGrid']//div[@ej-mappingname]"
|
||||||
)
|
)
|
||||||
print(f"[CCA Claim grid] Found {len(divs)} ej-mappingname header divs")
|
print(f"[CCA Claim grid] Found {len(divs)} ej-mappingname header divs")
|
||||||
for div in divs:
|
col_map = {}
|
||||||
mapping = div.get_attribute("ej-mappingname") or ""
|
for i, div in enumerate(divs):
|
||||||
|
mapping = (div.get_attribute("ej-mappingname") or "").strip().upper()
|
||||||
text = div.text.strip()
|
text = div.text.strip()
|
||||||
print(f"[CCA Claim grid] Header: mapping={mapping!r} text={text!r}")
|
print(f"[CCA Claim grid] Header[{i+1}]: mapping={mapping!r} text={text!r}")
|
||||||
|
if mapping:
|
||||||
|
col_map[mapping] = i + 1
|
||||||
|
|
||||||
|
# Aliases for the surface columns, which have empty mapping
|
||||||
|
# names in some grid states — fill in from SURFACE1..5 if present.
|
||||||
|
for n in range(1, 6):
|
||||||
|
key = f"SURFACE{n}"
|
||||||
|
if key in col_map:
|
||||||
|
col_map[f"SURF{n}"] = col_map[key]
|
||||||
|
col_map[f"S{n}"] = col_map[key]
|
||||||
|
|
||||||
|
if col_map:
|
||||||
|
return col_map
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[CCA Claim grid] Header scan error: {e}")
|
print(f"[CCA Claim grid] Header scan error: {e}")
|
||||||
|
|
||||||
return col_map
|
print("[CCA Claim grid] Falling back to hardcoded column map")
|
||||||
|
return fallback_col_map
|
||||||
|
|
||||||
def _dbl_click_col(self, row_num, col_idx):
|
def _dbl_click_col(self, row_num, col_idx):
|
||||||
"""Double-click the cell at (row_num, col_idx) — both 1-based.
|
"""Double-click the cell at (row_num, col_idx) — both 1-based.
|
||||||
@@ -733,38 +744,51 @@ class AutomationCCAClaimSubmit:
|
|||||||
cell_clicked = False
|
cell_clicked = False
|
||||||
|
|
||||||
# --- Single-click cell to activate (yellow), then fill input ---
|
# --- Single-click cell to activate (yellow), then fill input ---
|
||||||
def click_cell_and_fill(col_expr, input_id, value):
|
def click_cell_and_fill(col_expr, input_id, value, row_timeout=6):
|
||||||
"""
|
"""
|
||||||
Click the cell at (row_num, col_expr) to activate it (turns yellow),
|
Click the cell at (row_num, col_expr) to activate it (turns yellow),
|
||||||
then fill the pre-rendered input by ID.
|
then fill the pre-rendered input by ID.
|
||||||
col_expr can be a number like 2 or an XPath expression like 'last()' or 'last()-1'.
|
col_expr can be a number like 2 or an XPath expression like 'last()' or 'last()-1'.
|
||||||
|
Polls for up to row_timeout seconds since a new row is rendered
|
||||||
|
asynchronously by the grid after the previous row commits.
|
||||||
"""
|
"""
|
||||||
# Find the cell in the grid CONTENT rows (not headers)
|
xpaths = [
|
||||||
cell = None
|
|
||||||
for xpath in [
|
|
||||||
f"(//div[contains(@class,'e-gridcontent')]//tr)[{row_num}]/td[{col_expr}]",
|
f"(//div[contains(@class,'e-gridcontent')]//tr)[{row_num}]/td[{col_expr}]",
|
||||||
f"(//div[contains(@class,'e-content')]//tr)[{row_num}]/td[{col_expr}]",
|
f"(//div[contains(@class,'e-content')]//tr)[{row_num}]/td[{col_expr}]",
|
||||||
f"(//div[@id='Services']//div[contains(@class,'e-content')]//tr)[{row_num}]/td[{col_expr}]",
|
f"(//div[@id='Services']//div[contains(@class,'e-content')]//tr)[{row_num}]/td[{col_expr}]",
|
||||||
f"(//tr[@aria-rowindex='{row_num-1}'])/td[{col_expr}]",
|
f"(//tr[@aria-rowindex='{row_num-1}'])/td[{col_expr}]",
|
||||||
]:
|
]
|
||||||
try:
|
|
||||||
cell = self.driver.find_element(By.XPATH, xpath)
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if cell:
|
deadline = time.monotonic() + row_timeout
|
||||||
try:
|
clicked = False
|
||||||
self.driver.execute_script(
|
while time.monotonic() < deadline and not clicked:
|
||||||
"arguments[0].scrollIntoView({block:'nearest',inline:'nearest'});",
|
cell = None
|
||||||
cell)
|
for xpath in xpaths:
|
||||||
cell.click()
|
try:
|
||||||
|
cell = self.driver.find_element(By.XPATH, xpath)
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if cell:
|
||||||
|
try:
|
||||||
|
self.driver.execute_script(
|
||||||
|
"arguments[0].scrollIntoView({block:'nearest',inline:'nearest'});",
|
||||||
|
cell)
|
||||||
|
cell.click()
|
||||||
|
clicked = True
|
||||||
|
time.sleep(0.3)
|
||||||
|
print(f"[CCA Claim grid] Clicked row={row_num} col={col_expr}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[CCA Claim grid] Cell click failed, retrying: {e}")
|
||||||
|
time.sleep(0.3)
|
||||||
|
else:
|
||||||
|
print(f"[CCA Claim grid] Cell not found yet row={row_num} col={col_expr}, retrying")
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
print(f"[CCA Claim grid] Clicked row={row_num} col={col_idx}")
|
|
||||||
except Exception as e:
|
if not clicked:
|
||||||
print(f"[CCA Claim grid] Cell click failed: {e}")
|
print(f"[CCA Claim grid] Gave up clicking row={row_num} col={col_expr} after {row_timeout}s")
|
||||||
else:
|
return False
|
||||||
print(f"[CCA Claim grid] Cell not found row={row_num} col={col_idx}")
|
|
||||||
|
|
||||||
# Fill the pre-rendered input
|
# Fill the pre-rendered input
|
||||||
try:
|
try:
|
||||||
@@ -785,23 +809,31 @@ class AutomationCCAClaimSubmit:
|
|||||||
print(f"[CCA Claim grid] Fill {input_id} failed: {e}")
|
print(f"[CCA Claim grid] Fill {input_id} failed: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# CODE (col 2)
|
# CODE
|
||||||
click_cell_and_fill(2, "ServicesCODE", code)
|
code_ok = click_cell_and_fill(col_map["CODE"], "ServicesCODE", code)
|
||||||
|
|
||||||
# TOOTH (col 4) — skip if empty
|
# TOOTH — skip if empty
|
||||||
|
tooth_ok = True
|
||||||
if tooth:
|
if tooth:
|
||||||
click_cell_and_fill(4, "ServicesTOOTH", tooth)
|
tooth_ok = click_cell_and_fill(col_map["TOOTH"], "ServicesTOOTH", tooth)
|
||||||
|
|
||||||
# Surfaces (col 5-9) — skip if empty
|
# Surfaces — skip if empty
|
||||||
if surface_chars:
|
if surface_chars:
|
||||||
|
surf_ids = ["ServicesSURF1","ServicesSURF2","ServicesSURF3",
|
||||||
|
"ServicesSURF4","ServicesSURF5"]
|
||||||
for si, char in enumerate(surface_chars[:5]):
|
for si, char in enumerate(surface_chars[:5]):
|
||||||
surf_ids = ["ServicesSURF1","ServicesSURF2","ServicesSURF3",
|
click_cell_and_fill(col_map[f"SURF{si+1}"], surf_ids[si], char)
|
||||||
"ServicesSURF4","ServicesSURF5"]
|
|
||||||
click_cell_and_fill(5 + si, surf_ids[si], char)
|
|
||||||
|
|
||||||
# Billed Amount — last column
|
# Billed Amount
|
||||||
# QTY and Service Date auto-fill after CODE is entered and this cell is clicked
|
# QTY and Service Date auto-fill after CODE is entered and this cell is clicked
|
||||||
click_cell_and_fill("last()", "ServicesBILLED_AMOUNT", billed_str)
|
billed_ok = click_cell_and_fill(col_map["BILLED_AMOUNT"], "ServicesBILLED_AMOUNT", billed_str)
|
||||||
|
|
||||||
|
if not (code_ok and tooth_ok and billed_ok):
|
||||||
|
missing = [name for name, ok in
|
||||||
|
[("code", code_ok), ("tooth", tooth_ok), ("billed amount", billed_ok)]
|
||||||
|
if not ok]
|
||||||
|
return (f"ERROR: failed to fill {', '.join(missing)} for service line "
|
||||||
|
f"{idx+1} (code={code}) — claim NOT submitted")
|
||||||
|
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user