update: avoir support

This commit is contained in:
2025-07-19 11:42:57 +08:00
parent 0e8503a0d2
commit 0289f722de

View File

@@ -25,12 +25,14 @@ 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=int, nargs='?', default=None)
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'], 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='')
parser.add_argument('-t', '--timeout', type=int, nargs='?', default=60) parser.add_argument('-t', '--timeout', type=int, nargs='?', default=60)
parser.add_argument('-i', '--interval', type=int, nargs='?', default=5) parser.add_argument('-i', '--interval', type=int, nargs='?', default=5)
parser.add_argument('-k', '--kinds', type=str, nargs='*', default=['vat', 'correction'])
parser.add_argument('-r', '--retry', type=int, nargs='?', default=3) parser.add_argument('-r', '--retry', type=int, nargs='?', default=3)
args = parser.parse_args() args = parser.parse_args()
@@ -78,10 +80,15 @@ def main(workbook=None):
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])
if not args.vf_token or not args.vf_api_url:
print('[!!!!] 缺少参数vf_token 或 vf_api_url')
return 11
while True: while True:
try: try:
response = requests.get(f'{args.vf_api_url}/invoices.xml?kind=vat&include_positions=true&per_page=25&page={index}&period=more&date_from={datefrom}&date_to={dateto}&search_date_type=issue_date&api_token={args.vf_token}', timeout=args.timeout) response = requests.get(f'{args.vf_api_url}/invoices.xml?{kinds}&include_positions=true&per_page=25&page={index}&period=more&date_from={datefrom}&date_to={dateto}&search_date_type=issue_date&api_token={args.vf_token}', timeout=args.timeout)
string = response.content.decode(args.encoding) string = response.content.decode(args.encoding)
data = ET.fromstring(string) data = ET.fromstring(string)
except Exception as e: except Exception as e:
@@ -122,13 +129,35 @@ def main(workbook=None):
# 导出发票数据 # 导出发票数据
invoices = root.findall('invoice') invoices = root.findall('invoice')
limit = len(invoices) limit = len(invoices)
data = FieldArray()
print(f'[信息] 已读取发票数据 {limit}') print(f'[信息] 已读取发票数据 {limit}')
# 订单导入字段
# 详情见 <https://crm.xiaoman.cn/order/importOrder>
data = FieldArray(
'订单号',
'商机号',
'订单日期',
'当前处理人',
'业绩归属部门',
'客户编号',
'币种',
'产品名称',
'产品编号',
'产品型号',
'原价',
'折扣率',
'单价',
'数量',
'产品描述',
'AVOIR'
)
for index, invoice in enumerate(invoices): for index, invoice in enumerate(invoices):
rate = index / limit rate = index / limit
kind = text(invoice, 'kind')
number = text(invoice, 'number') number = text(invoice, 'number')
issue_date = text(invoice, 'issue-date') issue_date = text(invoice, 'issue-date')
total = float(text(invoice, 'price-net') or '0')
print(f'[信息] 正在载入 {number} ... {str(round(rate * 100)).rjust(3)} %') print(f'[信息] 正在载入 {number} ... {str(round(rate * 100)).rjust(3)} %')
@@ -144,23 +173,28 @@ def main(workbook=None):
discount = float(text(position, 'discount-percent') or '0') discount = float(text(position, 'discount-percent') or '0')
quantity = float(text(position, 'quantity') or '0') quantity = float(text(position, 'quantity') or '0')
# 订单导入字段 data.append('订单号', args.prefix + number)
# 详情见 <https://crm.xiaoman.cn/order/importOrder>
data.append('订单号', number)
data.append('商机号', relation) data.append('商机号', relation)
data.append('订单日期', issue_date) data.append('订单日期', issue_date)
data.append('当前处理人', category) data.append('当前处理人', category)
data.append('业绩归属部门', args.department) data.append('业绩归属部门', args.department)
data.append('客户编号', client) data.append('客户编号', client)
data.append('币种', args.currency) data.append('币种', args.currency)
match kind:
case 'vat':
data.append('产品名称', product) data.append('产品名称', product)
data.append('产品编号', None)
data.append('产品型号', code) data.append('产品型号', code)
data.append('原价', '%.2f' % price) data.append('原价', '%.2f' % price)
data.append('折扣率', '%g%%' % discount) data.append('折扣率', '%g%%' % discount)
data.append('单价', '%.2f' % (price * (1 - discount / 100))) data.append('单价', '%.2f' % (price * (1 - discount / 100)))
data.append('数量', '%g' % quantity) data.append('数量', '%g' % quantity)
data.append('产品描述', description) data.append('产品描述', description)
case 'correction':
data.append('产品名称', 'REMISE SPECIAL')
data.append('产品编号', '186')
data.append('单价', '0')
data.append('数量', '0')
data.append('AVOIR', '%.2f' % total)
data.newrow() data.newrow()
# 新建导入数据表 # 新建导入数据表
@@ -257,6 +291,10 @@ def main(workbook=None):
wait.until_not(condition) wait.until_not(condition)
return True return True
def keyin(element: WebElement, value):
try: element.send_keys(value)
except: driver.execute_script("arguments[0].value = arguments[1]", element, value)
if bool(args.xm_username) and bool(args.xm_password): if bool(args.xm_username) and bool(args.xm_password):
try: try:
locate("input.account").send_keys(args.xm_username) locate("input.account").send_keys(args.xm_username)
@@ -291,6 +329,8 @@ def main(workbook=None):
status = { 'draft': 1, 'final': 6 } 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 .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({status.get(args.automation)}) span")
@@ -341,8 +381,8 @@ def main(workbook=None):
# 设置分页选项 10 条/页 # 设置分页选项 10 条/页
try: try:
click(".okki-pagination-options-size-changer") click(".paas-invoice-list-frame .okki-pagination-options-size-changer")
click(".okki-select-dropdown .rc-virtual-list-holder-inner div:nth-child(1)") click(".okki-select-dropdown .rc-virtual-list-holder-inner > div:nth-child(1)")
ready(driver) ready(driver)
except: except:
print(f'[警告] 分页选项设置失败') print(f'[警告] 分页选项设置失败')
@@ -354,19 +394,20 @@ def main(workbook=None):
try: try:
click(link) click(link)
driver.switch_to.window(driver.window_handles[3]) driver.switch_to.window(driver.window_handles[3])
click(".component-detail-frame-header .okki-space-item:nth-child(1) button") click(".sticky .okki-space-item:nth-child(1) button")
warn = False warn = False
associative = False associative = False
header = locate(".order-edit-header .edit-order-no span span:nth-child(1)") number = locate("h1.serial-input-title span").get_attribute('innerText')
number = header.text total = lookup(number, 'AVOIR', workbook).map(lambda x: x[0]).ornone()
if number not in modified: if number not in modified and total is None:
# 选择商机 # 选择商机
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()
match = re.match(r'O\d+', str(relation)) needle = str(relation).strip().lstrip('SAV-')
match = re.match(r'O\d+', needle)
if match is not None: if match is not None:
try: try:
@@ -375,24 +416,25 @@ def main(workbook=None):
driver.get(href) driver.get(href)
# 检索商机 # 检索商机
locate(".new-header input").send_keys(relation) locate(".new-header input").send_keys(needle)
click(".new-header .search-btn span") click(".new-header .search-btn span")
ready(driver) ready(driver)
name = locate("#business-board-drag-wrapper .business-card-wrapper > div .name-wrapper a", wait=False) name = locate("#business-board-drag-wrapper .business-card-wrapper > div .name-wrapper a", wait=False)
relation = name.text 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[3])
dropdown = locate(".component-business-select .mm-selector-rendered") dropdown = locate("#rc_select_1")
click(dropdown) dropdown.clear()
dropdown.send_keys(needle)
wait = WebDriverWait(driver, timeout=args.timeout) wait = WebDriverWait(driver, timeout=args.timeout)
menu = wait.until(lambda x: x.find_element(By.CSS_SELECTOR, ".mm-outside.mm-select-dropdown")) menu = wait.until(lambda x: x.find_element(By.CSS_SELECTOR, ".okki-select-dropdown"))
for item in menu.find_elements(By.CSS_SELECTOR, "ul li span"): for item in menu.find_elements(By.CSS_SELECTOR, ".rc-virtual-list-holder-inner > div"):
if item.text.startswith(relation): if item.get_attribute('label').strip().startswith(needle):
click(item) click(item)
associative = True associative = True
raise KeyboardInterrupt() raise KeyboardInterrupt()
@@ -412,14 +454,14 @@ def main(workbook=None):
# 设定分页选项 10 条/页 # 设定分页选项 10 条/页
try: try:
click(".okki-pagination-options-size-changer") click(".okki-pagination-options-size-changer")
click(".okki-select-dropdown .rc-virtual-list-holder-inner div:nth-child(1)") click(".okki-select-dropdown .rc-virtual-list-holder-inner > div:nth-child(1)")
except: except:
print(f'[警告] {number}: 分页选项设置失败') print(f'[警告] {number}: 分页选项设置失败')
pass pass
# 编辑产品 # 编辑产品
try: try:
wrapper = locate(".order-edit-product-wrapper .row-items", condition=None) wrapper = locate(".paas-order-product-list .row-items", condition=None)
positions = lookup(number, '产品型号', workbook).unwrap() positions = lookup(number, '产品型号', workbook).unwrap()
ids = [] ids = []
@@ -449,26 +491,32 @@ 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='4'] input", parent=item, condition=None) original = locate(".cell[data-cci='5'] input", parent=item, condition=None)
if price == '--': price = original.get_attribute('value') if price == '--': price = original.get_attribute('value')
# 填入含税成本价 # 填入含税成本价
target = locate(".cell[data-cci='6'] input", parent=item, condition=None) driver.execute_script("arguments[0].scroll(600, 0);", wrapper)
target = locate(".cell[data-cci='7'] input", parent=item, condition=None)
target.clear()
target.send_keys(price) target.send_keys(price)
ids.append(serial) ids.append(serial)
index += 1 index += 1
# 下一页 # 下一页
click(".summary-pagination-wrapper li.okki-pagination-next button", condition=None) click(".text-right li.okki-pagination-next button", condition=None)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
except Exception as e: except Exception as e:
print(f"[警告] {number}: 编辑产品时发生错误:{e}") print(f"[警告] {number}: 编辑产品时发生错误:{e}")
warn = True warn = True
# 删除 Avoir 费用
try: click(".ow-box button.okki-btn-round")
except: pass
# 保存订单 # 保存订单
ready(driver) ready(driver)
click(".order-edit-footer button.okki-btn-primary", condition=None) click(".sticky.bottom-0 button.okki-btn-primary", condition=None)
ready(driver) ready(driver)
print(f"[信息] {number}: 修改完成") print(f"[信息] {number}: 修改完成")
@@ -488,7 +536,7 @@ def main(workbook=None):
return 85 return 85
try: try:
button = locate(".okki-pagination-next button", wait=False) button = locate(".paas-invoice-list-frame .list-okki-footer-wrap li.okki-pagination-next button", wait=False)
if bool(button.get_attribute('disabled')): raise KeyboardInterrupt() if bool(button.get_attribute('disabled')): raise KeyboardInterrupt()
click(button) click(button)
except: except:
@@ -498,13 +546,13 @@ def main(workbook=None):
return 0 return 0
class FieldArray[K, V]: class FieldArray[K, V]:
def __init__(self): def __init__(self, *args: K):
self.map: dict[K, list[V]] = {} self.map: dict[K, list[V]] = { name: [] for name in args }
self.index = 0 self.index = 0
pass pass
def append(self, key: K, value: V): def append(self, key: K, value: V):
array = self.map.setdefault(key, []) array = self.map.get(key, [])
array.insert(self.index, value) array.insert(self.index, value)
def newrow(self, padding=None): def newrow(self, padding=None):