update: added override mode

This commit is contained in:
2025-08-01 14:21:45 +08:00
parent 0289f722de
commit bedbc9ec42

View File

@@ -11,6 +11,8 @@ from xml.etree import ElementTree as ET
from selenium import webdriver from selenium import webdriver
from selenium.common.exceptions import StaleElementReferenceException, TimeoutException from selenium.common.exceptions import StaleElementReferenceException, TimeoutException
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.remote.webelement import WebElement
@@ -24,9 +26,9 @@ parser.add_argument('--xm-web-url', type=str, nargs='?', default='https://login.
parser.add_argument('--vf-api-url', type=str, nargs='?', default='') parser.add_argument('--vf-api-url', type=str, nargs='?', default='')
parser.add_argument('-C', '--currency', type=str, nargs='?', default='USD') parser.add_argument('-C', '--currency', type=str, nargs='?', default='USD')
parser.add_argument('-D', '--department', type=str, nargs='?', default='') parser.add_argument('-D', '--department', type=str, nargs='?', default='')
parser.add_argument('-T', '--duration', type=int, nargs='?', default=None) parser.add_argument('-T', '--duration', type=str, nargs='*', default=[])
parser.add_argument('-P', '--prefix', type=str, nargs='?', default='') parser.add_argument('-P', '--prefix', type=str, nargs='?', default='')
parser.add_argument('-a', '--automation', type=str, choices=['none', 'draft', 'final'], nargs='?', default='none') parser.add_argument('-a', '--automation', type=str, choices=['none', 'draft', 'final', 'override'], nargs='?', default='none')
parser.add_argument('-e', '--encoding', type=str, nargs='?', default='utf-8') parser.add_argument('-e', '--encoding', type=str, nargs='?', default='utf-8')
parser.add_argument('-d', '--outdir', type=str, nargs='?', default='.') parser.add_argument('-d', '--outdir', type=str, nargs='?', default='.')
parser.add_argument('-o', '--output', type=str, nargs='?', default='') parser.add_argument('-o', '--output', type=str, nargs='?', default='')
@@ -66,25 +68,34 @@ def main(workbook=None):
else: else:
root = None root = None
index = 1 index = 1
format = "%d/%m/%Y" format = "%Y-%m-%d"
if args.duration is not None: match args.duration:
datefrom = (datetime.today() - timedelta(days=args.duration)) case it if len(it) == 0:
dateto = datetime.today()
print(f'[信息] 正在尝试获取自 {datefrom:%Y-%m-%d}{dateto:%Y-%m-%d} 的发票数据')
else:
yesterday = (datetime.today() - timedelta(days=1)) yesterday = (datetime.today() - timedelta(days=1))
datefrom = yesterday datefrom = yesterday
dateto = yesterday dateto = yesterday
print(f'[信息] 正在尝试获取 {yesterday:%Y-%m-%d} 的发票数据') print(f'[信息] 正在尝试获取 {yesterday.strftime(format)} 的发票数据')
case it if len(it) == 1 and (duration := str(it[0])) and duration.isnumeric():
datefrom = (datetime.today() - timedelta(days=int(duration)))
dateto = datetime.today()
print(f'[信息] 正在尝试获取自 {datefrom.strftime(format)}{dateto.strftime(format)} 的发票数据')
case it if len(it) == 2:
datefrom = datetime.strptime(it[0], format)
dateto = datetime.strptime(it[1], format)
print(f'[信息] 正在尝试获取自 {datefrom.strftime(format)}{dateto.strftime(format)} 的发票数据')
case _:
print(f'[!!!!] 无效参数:{args.duration}')
return 11
format = "%d/%m/%Y"
datefrom = parse.quote(datefrom.strftime(format)) datefrom = parse.quote(datefrom.strftime(format))
dateto = parse.quote(dateto.strftime(format)) dateto = parse.quote(dateto.strftime(format))
kinds = '&'.join([f'kinds%5B%5D={k}' for k in args.kinds]) kinds = '&'.join([f'kinds%5B%5D={k}' for k in args.kinds])
if not args.vf_token or not args.vf_api_url: if not args.vf_token or not args.vf_api_url:
print('[!!!!] 缺少参数vf_token 或 vf_api_url') print('[!!!!] 缺少参数vf_token 或 vf_api_url')
return 11 return 12
while True: while True:
try: try:
@@ -105,8 +116,9 @@ def main(workbook=None):
print('[!!!!] 服务器返回了无效数据') print('[!!!!] 服务器返回了无效数据')
return 21 return 21
def lookup(value, fieldname, excel) -> Result: def lookup(value, fieldname, df: pandas.DataFrame) -> Result:
rows = excel[excel.isin([value]).any(axis=1)] df = df.astype(object).where(df.notna(), None)
rows = df[df.isin([value]).any(axis=1)]
data = rows.to_dict(orient='list') data = rows.to_dict(orient='list')
return Result(data.get(fieldname)) return Result(data.get(fieldname))
@@ -318,22 +330,26 @@ def main(workbook=None):
click(".layout-sidebar ul li.list-none.cpq div div") click(".layout-sidebar ul li.list-none.cpq div div")
ready(driver) ready(driver)
click(".layout-sidebar .layout-second-menu li.order a") click(".layout-sidebar .layout-second-menu li.order a")
except Exception as e:
print(f'[!!!!] 尝试检索订单时发生了错误:{e}')
return 82
# 状态菜单映射
if index := { 'draft': 1, 'final': 6 }.get(args.automation):
try:
click(".list-header-top button.okki-dropdown-trigger") click(".list-header-top button.okki-dropdown-trigger")
click(".okki-dropdown-content button") click(".okki-dropdown-content button")
driver.switch_to.window(driver.window_handles[1]) driver.switch_to.window(driver.window_handles[1])
except Exception as e: except Exception as e:
print(f'[!!!!] 尝试进入录入订单页面时发生了错误:{e}') print(f'[!!!!] 尝试进入录入订单页面时发生了错误:{e}')
return 82 return 83
# 状态菜单映射
status = { 'draft': 1, 'final': 6 }
try: try:
# 选择不导入无对应产品的订单 # 选择不导入无对应产品的订单
click(".product-import-img-box .import-img-radio:nth-child(2) .mm-radio-group > label:nth-child(2) .mm-radio-input", condition=None) click(".product-import-img-box .import-img-radio:nth-child(2) .mm-radio-group > label:nth-child(2) .mm-radio-input", condition=None)
# 变更状态 # 变更状态
click(".product-import-img-box .mm-selector-rendered") click(".product-import-img-box .mm-selector-rendered")
click(f".mm-outside.mm-select-dropdown ul li:nth-child({status.get(args.automation)}) span") click(f".mm-outside.mm-select-dropdown ul li:nth-child({index}) span")
path = os.path.abspath(args.output or args.invoices) path = os.path.abspath(args.output or args.invoices)
# 上传文件 # 上传文件
@@ -379,44 +395,33 @@ def main(workbook=None):
ready(driver) #2 ready(driver) #2
modified = [] modified = []
# 设置分页选项 10 条/页 def modify(link: WebElement, handle=3):
try:
click(".paas-invoice-list-frame .okki-pagination-options-size-changer")
click(".okki-select-dropdown .rc-virtual-list-holder-inner > div:nth-child(1)")
ready(driver)
except:
print(f'[警告] 分页选项设置失败')
pass
while ready(driver):
for link in driver.find_elements(By.CSS_SELECTOR, ".list-frame-table .vue-recycle-scroller__item-wrapper .cell[data-cci='1'] a"):
# 编辑订单
try:
click(link) click(link)
driver.switch_to.window(driver.window_handles[3]) driver.switch_to.window(driver.window_handles[handle])
click(".sticky .okki-space-item:nth-child(1) button") click(".sticky .okki-space-item:nth-child(1) button")
warn = False warn = False
associative = False associative = False
number = locate("h1.serial-input-title span").get_attribute('innerText') relation = None
total = lookup(number, 'AVOIR', workbook).map(lambda x: x[0]).ornone() number = Result(None).map(lambda _: locate("h1.serial-input-title span").get_attribute('innerText')).ornone()
correction = lookup(number, 'AVOIR', workbook).map(lambda x: x[0]).ornone()
if number not in modified and total is None: if number is not None and number not in modified and correction is None:
try:
# 选择商机 # 选择商机
for attempt in range(args.retry): for attempt in range(args.retry):
try: try:
relation = lookup(number, '商机号', workbook).map(lambda x: x[0]).unwrap() relation = lookup(number, '商机号', workbook).map(lambda x: x[0]).unwrap()
needle = str(relation).strip().lstrip('SAV-') needle = str(relation).strip()
match = re.match(r'O\d+', needle)
if match is not None: if match := Result(re.search(r'O\d+', needle)).map(lambda x: x[0]).ornone():
try: try:
href = locate(".layout-sidebar ul li.list-none.opportunity a").get_attribute('href') href = locate(".layout-sidebar ul li.list-none.opportunity a").get_attribute('href')
driver.switch_to.new_window('tab') driver.switch_to.new_window('tab')
driver.get(href) driver.get(href)
# 检索商机 # 检索商机
locate(".new-header input").send_keys(needle) locate(".new-header input").send_keys(match)
click(".new-header .search-btn span") click(".new-header .search-btn span")
ready(driver) ready(driver)
@@ -424,7 +429,7 @@ def main(workbook=None):
needle = name.text.strip() needle = name.text.strip()
finally: finally:
driver.close() driver.close()
driver.switch_to.window(driver.window_handles[3]) driver.switch_to.window(driver.window_handles[handle])
dropdown = locate("#rc_select_1") dropdown = locate("#rc_select_1")
dropdown.clear() dropdown.clear()
@@ -465,25 +470,23 @@ def main(workbook=None):
positions = lookup(number, '产品型号', workbook).unwrap() positions = lookup(number, '产品型号', workbook).unwrap()
ids = [] ids = []
while True: while (index := 1):
driver.execute_script("arguments[0].scrollTo(0, 0);", wrapper) for attempts in range(args.retry):
index = 0 if index >= 10: break
tail = Result(ids).map(lambda x: x[-1:][0]).ornone()
while index < 10: driver.execute_script("arguments[0].scrollIntoView({ block: 'end' });", wrapper)
if len(ids) >= len(positions): raise KeyboardInterrupt() driver.execute_script("arguments[0].scrollTo(arguments[1], arguments[2]);", wrapper, 0, 0)
if index >= 5: driver.execute_script("arguments[0].scrollTo(0, arguments[0].scrollHeight);", wrapper)
for item in wrapper.find_elements(By.CSS_SELECTOR, ".row-item"): for item in wrapper.find_elements(By.CSS_SELECTOR, ".row-item"):
# 订单序号 if len(ids) >= len(positions): raise KeyboardInterrupt()
try: serial = item.find_element(By.CSS_SELECTOR, ".cell[data-cci='2'] .cell-inner div").text serial = Result(item.find_elements(By.CSS_SELECTOR, ".cell[data-cci='2'] .cell-inner div")).map(lambda x: int(x[0].text)).ornone()
except: continue
if not bool(serial): continue
# 转到元素 if serial is not None and serial not in ids:
driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", item) base = (serial - tail) if tail is not None else index
driver.execute_script("arguments[0].scrollIntoView({ block: 'start' });", wrapper) height = int(item.get_attribute('clientHeight'))
offset = (base - 1) * height
driver.execute_script("arguments[0].scrollTo(arguments[1], arguments[2]);", wrapper, 0, offset)
if serial not in ids:
click(".product-info-group-product-info a.jump-link", parent=item) click(".product-info-group-product-info a.jump-link", parent=item)
price = locate(".okki-drawer-body div[title='含税成本价'] + div div.mm-formily-preview-text", condition=None).text price = locate(".okki-drawer-body div[title='含税成本价'] + div div.mm-formily-preview-text", condition=None).text
click(".okki-drawer-body .space-header-after button") click(".okki-drawer-body .space-header-after button")
@@ -491,27 +494,30 @@ def main(workbook=None):
wait = WebDriverWait(driver, timeout=args.timeout) wait = WebDriverWait(driver, timeout=args.timeout)
wait.until_not(lambda x: x.find_element(By.CSS_SELECTOR, ".okki-drawer-body").is_displayed()) wait.until_not(lambda x: x.find_element(By.CSS_SELECTOR, ".okki-drawer-body").is_displayed())
original = locate(".cell[data-cci='5'] input", parent=item, condition=None) original = locate(".cell[data-cci='4'] input", parent=item, condition=None)
if price == '--': price = original.get_attribute('value') if price == '--': price = original.get_attribute('value')
driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", wrapper)
driver.execute_script("arguments[0].scroll(arguments[1], arguments[2]);", wrapper, 400, offset)
# 填入含税成本价 # 填入含税成本价
driver.execute_script("arguments[0].scroll(600, 0);", wrapper) target = locate(".cell[data-cci='6'] input", parent=item, condition=None)
target = locate(".cell[data-cci='7'] input", parent=item, condition=None) length = len(target.get_attribute('value'))
target.clear() target.send_keys(Keys.BACKSPACE * length)
target.send_keys(price) target.send_keys(price)
ids.append(serial) ids.append(serial)
index += 1 index += 1
# 下一页 # 下一页
click(".text-right li.okki-pagination-next button", condition=None) button = locate(".text-right li.okki-pagination-next button", condition=None)
if bool(button.get_attribute('disabled')) and len(ids) < len(positions): raise Exception(f'产品信息不完整。缺少:{', '.join(positions[len(ids):])}')
click(button)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
except Exception as e:
print(f"[警告] {number}: 编辑产品时发生错误:{e}")
warn = True
# 删除 Avoir 费用 # 删除 Avoir 费用
try: click(".ow-box button.okki-btn-round") try: click(".ow-box button.okki-btn-round", wait=False)
except: pass except: pass
# 保存订单 # 保存订单
@@ -524,13 +530,32 @@ def main(workbook=None):
global success global success
success += 1 success += 1
except Exception as e:
print(f"[警告] {number}: 编辑订单时发生了错误:{e}")
warn = True
global warning global warning
if warn: warning += 1 if warn: warning += 1
# 关闭标签页 # 关闭标签页
driver.close() driver.close()
driver.switch_to.window(driver.window_handles[2]) driver.switch_to.window(driver.window_handles[handle-1])
# 设置分页选项 10 条/页
try:
click(".paas-invoice-list-frame .okki-pagination-options-size-changer")
click(".okki-select-dropdown .rc-virtual-list-holder-inner > div:nth-child(1)")
ready(driver)
except:
print(f'[警告] 分页选项设置失败')
pass
match args.automation:
case 'draft', 'final':
while ready(driver):
for link in driver.find_elements(By.CSS_SELECTOR, ".list-frame-table .vue-recycle-scroller__item-wrapper .cell[data-cci='1'] a"):
try:
modify(link)
except Exception as e: except Exception as e:
print(f'[!!!!] 编辑订单时发生了错误:{e}') print(f'[!!!!] 编辑订单时发生了错误:{e}')
return 85 return 85
@@ -542,6 +567,54 @@ def main(workbook=None):
except: except:
print('[信息] 已经是最后一页') print('[信息] 已经是最后一页')
break break
case 'override':
for number in workbook['订单号'].unique():
try:
box = locate(".list-header-wrap div[data-item-name='订单号']", condition=None)
search = locate("input", parent=box, condition=None)
length = len(search.get_attribute('value'))
click(box)
ActionChains(driver).send_keys(Keys.BACKSPACE * length).send_keys(number).send_keys(Keys.ENTER).perform()
except Exception as e:
print(f'[警告] {number}: 检索订单时发生了错误:{e}')
continue
entry = None
department: str = lookup(number, '业绩归属部门', workbook).map(lambda x: x[0]).ornone()
for attempt in range(args.retry):
try:
while ready(driver):
for link in driver.find_elements(By.CSS_SELECTOR, ".list-frame-table .vue-recycle-scroller__item-wrapper .cell[data-cci='1'] a"):
name = link.text.strip()
cell = link.find_element(By.XPATH, '../../../../div[@data-cci="6"]/div/span')
title = cell.get_attribute('title').strip()
if link.is_displayed() and name == number and title == department:
entry = link
raise KeyboardInterrupt()
# 下一页
button = locate(".okki-pagination-next button", condition=None)
if bool(button.get_attribute('disabled')): break
click(button)
except KeyboardInterrupt:
break
except StaleElementReferenceException:
continue
except Exception as e:
print(f'[警告] {number}: 发生错误:{e}')
continue
if entry is None:
print(f'[警告] {number}: 无法找到对应发票')
continue
try:
modify(entry, handle=1)
except Exception as e:
print(f'[!!!!] {number}: 编辑订单时发生了错误:{e}')
continue
return 0 return 0