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)
|
||||
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
|
||||
|
||||
# Try table cells first (Encounter ID is the first column)
|
||||
for sel in [
|
||||
(By.XPATH, "//table//tbody//tr[last()]/td[1]"),
|
||||
(By.XPATH, "//*[contains(@class,'wbx-table-results')]//tr[last()]/td[1]"),
|
||||
(By.XPATH, "//table//tbody//tr[last()-0]/td[1]"),
|
||||
(By.XPATH, "//table//tbody//tr[1]/td[1]"),
|
||||
(By.XPATH, "//*[contains(@class,'wbx-table-results')]//tr[1]/td[1]"),
|
||||
]:
|
||||
try:
|
||||
cell = WebDriverWait(self.driver, 8).until(
|
||||
@@ -461,7 +460,7 @@ class AutomationCCAClaimSubmit:
|
||||
text = cell.text.strip()
|
||||
if re.match(r'\d{10,}', 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
|
||||
except Exception:
|
||||
continue
|
||||
@@ -591,14 +590,12 @@ class AutomationCCAClaimSubmit:
|
||||
# ------------------------------------------------------------------ #
|
||||
def _build_col_map(self):
|
||||
"""
|
||||
Map column names to 1-based td positions using ej-mappingname divs.
|
||||
Falls back to hardcoded positions based on the known PDF column order.
|
||||
Map column names to 1-based td positions using ej-mappingname divs,
|
||||
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:
|
||||
# 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 = {
|
||||
fallback_col_map = {
|
||||
"CODE": 2,
|
||||
"TOOTH": 4,
|
||||
"SURF1": 5, "S1": 5,
|
||||
@@ -608,10 +605,9 @@ class AutomationCCAClaimSubmit:
|
||||
"SURF5": 9, "S5": 9,
|
||||
"QUANTITY": 19, "QTY": 19,
|
||||
"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:
|
||||
divs = self.driver.find_elements(
|
||||
By.XPATH,
|
||||
@@ -619,14 +615,29 @@ class AutomationCCAClaimSubmit:
|
||||
"//div[@id='ServicesGrid']//div[@ej-mappingname]"
|
||||
)
|
||||
print(f"[CCA Claim grid] Found {len(divs)} ej-mappingname header divs")
|
||||
for div in divs:
|
||||
mapping = div.get_attribute("ej-mappingname") or ""
|
||||
col_map = {}
|
||||
for i, div in enumerate(divs):
|
||||
mapping = (div.get_attribute("ej-mappingname") or "").strip().upper()
|
||||
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:
|
||||
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):
|
||||
"""Double-click the cell at (row_num, col_idx) — both 1-based.
|
||||
@@ -733,38 +744,51 @@ class AutomationCCAClaimSubmit:
|
||||
cell_clicked = False
|
||||
|
||||
# --- 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),
|
||||
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'.
|
||||
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)
|
||||
cell = None
|
||||
for xpath in [
|
||||
xpaths = [
|
||||
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[@id='Services']//div[contains(@class,'e-content')]//tr)[{row_num}]/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:
|
||||
try:
|
||||
self.driver.execute_script(
|
||||
"arguments[0].scrollIntoView({block:'nearest',inline:'nearest'});",
|
||||
cell)
|
||||
cell.click()
|
||||
deadline = time.monotonic() + row_timeout
|
||||
clicked = False
|
||||
while time.monotonic() < deadline and not clicked:
|
||||
cell = None
|
||||
for xpath in xpaths:
|
||||
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)
|
||||
print(f"[CCA Claim grid] Clicked row={row_num} col={col_idx}")
|
||||
except Exception as e:
|
||||
print(f"[CCA Claim grid] Cell click failed: {e}")
|
||||
else:
|
||||
print(f"[CCA Claim grid] Cell not found row={row_num} col={col_idx}")
|
||||
|
||||
if not clicked:
|
||||
print(f"[CCA Claim grid] Gave up clicking row={row_num} col={col_expr} after {row_timeout}s")
|
||||
return False
|
||||
|
||||
# Fill the pre-rendered input
|
||||
try:
|
||||
@@ -785,23 +809,31 @@ class AutomationCCAClaimSubmit:
|
||||
print(f"[CCA Claim grid] Fill {input_id} failed: {e}")
|
||||
return False
|
||||
|
||||
# CODE (col 2)
|
||||
click_cell_and_fill(2, "ServicesCODE", code)
|
||||
# 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:
|
||||
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:
|
||||
surf_ids = ["ServicesSURF1","ServicesSURF2","ServicesSURF3",
|
||||
"ServicesSURF4","ServicesSURF5"]
|
||||
for si, char in enumerate(surface_chars[:5]):
|
||||
surf_ids = ["ServicesSURF1","ServicesSURF2","ServicesSURF3",
|
||||
"ServicesSURF4","ServicesSURF5"]
|
||||
click_cell_and_fill(5 + si, surf_ids[si], char)
|
||||
click_cell_and_fill(col_map[f"SURF{si+1}"], 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
|
||||
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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user