Compare commits

..

20 Commits

Author SHA1 Message Date
e5b425ba87 update: template 2026-06-03 16:38:54 +08:00
044cf2503a fix: 'common' version 2026-05-26 11:58:12 +08:00
3b9af6e57e update: bump 'common' version to 0.1.15 2026-05-22 15:51:49 +08:00
9779d1d407 fix: 'common' version 2026-04-29 17:27:48 +08:00
1d6d7482b4 fix: 'common' version 2026-04-29 15:26:39 +08:00
423213759d update: disabled BiDi 2026-04-29 11:41:58 +08:00
7abaa1b059 fix 2026-04-17 11:23:08 +08:00
8665a8dd1d fix: bump 'common' version to 0.1.9 2026-04-17 11:21:48 +08:00
875aa5e761 add: 'common' as dependency 2026-04-14 15:14:35 +08:00
6e46b8faf5 fix: crash 2026-04-01 15:12:22 +08:00
714818bb15 pivot to selenium 2026-03-31 18:00:39 +08:00
ba548e8b13 update: restored grid color 2025-07-28 14:07:21 +08:00
73ffc1a937 update: removed backgrounds 2025-07-21 11:30:30 +08:00
5e98510252 update: redesign 2025-07-19 16:49:15 +08:00
Break27
2fde41b0b3 Update README.md 2025-07-08 10:52:49 +08:00
da87d22bcb add: communication means 2025-07-07 18:24:14 +08:00
Break27
61a64bed41 Update README.md 2025-06-19 17:20:03 +08:00
4846b33985 update: margin copying 2025-06-19 17:08:15 +08:00
45d576e229 update: README.md 2025-06-17 16:14:42 +08:00
e0d62262f3 replace Department with Status 2025-06-17 16:00:35 +08:00
9 changed files with 281 additions and 54 deletions

3
.gitignore vendored
View File

@@ -1 +1,2 @@
.workbooks/ /.vscode
/venv

View File

@@ -1,53 +0,0 @@
# 商机纸质打印操作步骤
1. 导出商机数据
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/yBRq1geYWaXmldv1/img/ac293e62-7aec-42f9-ad40-023cfea9da1e.png)
2. 筛选数据
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/yBRq1geYWaXmldv1/img/aa73c2fe-3790-4524-8c07-29ad6370b488.png)
3. 在“导出设置”中设置导出字段(字段和顺序必须与图中一致)
![15c9c6391dd848f89075d5e62d7756d3.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/yBRq1geYWaXmldv1/img/7e4c90f5-7857-4e32-8ea6-b28f723cac66.png)
4. 导出详细版
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/yBRq1geYWaXmldv1/img/03f57463-5e82-4f7d-9dfc-922e1a03a154.png)
5. 在数据源中全选并复制Ctrl+A, Ctrl+C
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/yBRq1geYWaXmldv1/img/19b435f1-260c-4557-a32d-f91a16ebe10c.png)
6. 打开导出宏启用宏选择Data工作簿全选Ctrl+A并清除然后粘贴Ctrl+V
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/yBRq1geYWaXmldv1/img/bbd16bd3-b18a-47ed-8696-b4f2bfb01d1c.png)
7. 选择Config工作簿输入导出位置注意路径必须以分隔符 “\” 结尾)
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/yBRq1geYWaXmldv1/img/4f0e8d61-11eb-46e3-94dc-228a19fe9228.png)
8. 运行宏
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/yBRq1geYWaXmldv1/img/c0a439b3-68a2-45bc-b1ab-5902caa3c782.png)
9. 宏运行完成Excel会有弹窗提示在指定文件夹找到导出的商机
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/yBRq1geYWaXmldv1/img/6a7b04ed-fcd9-4da6-8789-5d1a8ce67e68.png)
10. 全选,右键,选择显示更多选项,点击打印
![image.png](https://alidocs.oss-cn-zhangjiakou.aliyuncs.com/res/yBRq1geYWaXmldv1/img/35d3c5c5-06ba-4c62-b8ab-a09d83cc336c.png)
11. 完成打印,由当周主持人打印,在日升会的时候发给同事。

48
index.html Normal file
View File

@@ -0,0 +1,48 @@
<link rel="icon" href="https://s3.dualstack.us-east-2.amazonaws.com/pythondotorg-assets/media/files/python-logo-only.svg" type="image/svg+xml">
<title>Opportunity Export</title>
<textarea id="messages" name="messages" rows="45" cols="100" readonly>
</textarea>
<script type="module">
import { default as $, Rpc2, LogRecord } from '/';
let { parameters } = await Rpc2.invoke('context');
let account = new String(parameters['account']);
let name = account.split('@', 1).pop() ?? 'unknown';
name = name.charAt(0).toLocaleUpperCase() + name.slice(1);
document.title += ` (${name})`;
while (await new Promise(o => setTimeout(o, 1000, true))) {
let result = await Rpc2.invoke('history').catch(() => []);
let logs = Array.from(result);
for (let record of logs) {
if ($('#messages').childNodes.length >= 500) $('#messages').childNodes.item(0)?.remove();
let message = LogRecord.format(record);
let node = document.createTextNode(new String(message).concat('\n'));
$('#messages').appendChild(node);
}
$('#messages').scrollTop = messages.scrollHeight;
}
</script>
<style type="text/css">
body {
width: 100%;
height: 100%;
display: inline-flex;
margin: auto;
align-items: center;
justify-content: center;
}
textarea {
resize: none;
white-space: pre-wrap;
}
textarea:focus {
outline: none;
}
</style>

223
main.py Normal file
View File

@@ -0,0 +1,223 @@
import argparse
import openpyxl
import logging
import base64
import csv
from selenium.common.exceptions import TimeoutException, StaleElementReferenceException
from selenium.webdriver.chrome.webdriver import WebDriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from common.jsonrpc2 import ServiceProvider
from common.utils import *
from io import BytesIO
from pathlib import Path
from urllib3 import PoolManager
from itertools import count
from subprocess import Popen
parser = argparse.ArgumentParser(description="Opportunity Export")
parser.add_argument('account', type=str)
parser.add_argument('password', type=str)
parser.add_argument('-p', '--open', action='store_true')
parser.add_argument('-d', '--directory', type=str, default=str(Path.home().joinpath('Downloads')))
parser.add_argument('-e', '--encoding', type=str, default="utf-8-sig")
parser.add_argument('-t', '--timeout', type=int, default=60)
parser.add_argument('-r', '--attempts', type=int, default=3)
parser.add_argument('-l', '--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'])
args = parser.parse_args()
WEBURL = "https://crm.xiaoman.cn/business/export"
APIURL = "https://crm.xiaoman.cn/api/opportunityRead/export"
def main(driver: WebDriver, logger = logging.getLogger('main')):
parameters = vars(args)
http = PoolManager()
sp = ServiceProvider.default()
try:
sp.set('context', lambda: { 'parameters': parameters })
driver.get(sp.run())
except Exception as e:
logger.critical('Unable to load starup page', exc_info=e)
return 2
try:
driver.switch_to.new_window('tab')
driver.set_page_load_timeout(parameters['timeout'])
driver.get(WEBURL)
except TimeoutException:
logger.warning('Timeout')
driver.execute_script("window.stop();")
try:
setup(driver, parameters)
until(lambda x: 'loginProgress' in x.find_element(By.TAG_NAME, "body").get_attribute('class'), watch=False)
account = str(parameters['account'])
password = str(parameters['password'])
logger.info('Logging in as %s (%s)', account.split('@', 1).pop(0).capitalize(), account)
locate("input.account").send_keys(account)
locate("input#password").send_keys(password)
click("input.agree-checkbox")
click("button.login-btn")
except Exception as e:
logger.critical('Unable to login to %s', parameters['url'], exc_info=e)
return 3
logger.info('Waiting for authentication to complete...')
while True:
try:
locate("#container", wait=False)
sidebar = locate(".new-layout-left")
driver.execute_script("arguments[0].remove();", sidebar)
logger.info('Done')
break
except:
sleep(3)
while True:
try:
if buttons := driver.find_elements(By.CSS_SELECTOR, ".export-actions .okki-btn-primary"):
identity = 'EXPORT'
for button in buttons: driver.execute_script("arguments[0].addEventListener('click', () => arguments[0].parentElement.setAttribute(arguments[1], '1'));", button, identity)
break
finally:
sleep(1)
while True:
try:
for index in count(0):
if (driver.execute_script("return arguments[0].parentElement.getAttribute(arguments[1]);", buttons[index], identity)):
driver.execute_script("arguments[0].parentElement.removeAttribute(arguments[1]);", buttons[index], identity)
raise ValueError()
except StaleElementReferenceException:
logger.critical('Error while listening to button events')
return 4
except ValueError:
pass
except:
sleep(1)
continue
try:
locate(".safe-verify-dialog:not([style*='display: none']) .mm-modal-content .mm-modal-body input", wait=False).send_keys(parameters['password'])
click(".mm-modal-footer button.okki-btn-primary")
except:
pass
while True:
try:
sleep(1)
click(".business-export-wrap section:nth-child(3) h2 svg", wait=False)
cell = locate(".business-export-wrap section:nth-child(3) table tbody tr:first-child td:first-child span", wait=False)
if (filename := cell.text) != '--': break
except:
pass
try:
logger.info('New task: %s', filename)
download = locate(".business-export-wrap section:nth-child(3) table tbody tr:first-child td:last-child a")
href = download.get_attribute('href')
response = http.request("GET", href)
text = response.data.decode(parameters['encoding']).splitlines()
data = csv.reader(text)
logger.info('Read %d line(s) total', len(text))
except Exception as e:
logger.critical('Unable to load input data', exc_info=e)
return 5
try:
file = Path('template.xlsx').resolve()
template = openpyxl.load_workbook(file)
except Exception as e:
logger.critical('Unable to load template excel', exc_info=e)
return 6
header = next(data, None)
source = template.active
output = BytesIO()
def preprocess(data: str):
try: return float(data)
except ValueError: pass
try: return int(data)
except ValueError: pass
return data.lstrip("'")
for row in data:
for index, name in enumerate(header):
names = template.defined_names
if name not in names: continue
for _, coord in names[name].destinations:
value = preprocess(row[index])
source[coord] = value
sheet = template.copy_worksheet(source)
sheet.title = 'Copy'
logger.debug('%s', sheet.title)
if not len(template.worksheets) > 1:
logger.error('Invalid input')
continue
template.remove(source)
template.save(output)
data = base64.b64encode(output.getbuffer())
text = data.decode('ascii')
mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
href = f"data:{mime};base64,{text}"
filename = filename.replace('.csv', '.xlsx')
driver.switch_to.new_window('tab')
driver.execute_script(
"""
var link = document.createElement('a');
link.download = arguments[0];
link.href = arguments[1];
link.click();
""",
filename,
href
)
if parameters['open']:
try:
path = Path(parameters['directory']) / filename
until(lambda _: path.exists(), watch=False)
Popen(['start', str(path)], shell=True)
except Exception as e:
logger.warning('Unable to open exported excel file at %s', str(path), exc_info=e)
driver.close()
driver.switch_to.window(driver.window_handles[1])
logger.info('Done')
if __name__ == '__main__':
try:
logging.basicConfig(level=logging.INFO, format="[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s", datefmt="%Y-%m-%d %H:%M")
logger = logging.getLogger()
level = logging.getLevelNamesMapping().get(args.log_level, 'INFO')
logger.setLevel(level)
opts = Options()
opts.enable_downloads = True
opts.add_argument('--deny-permission-prompts')
opts.add_experimental_option('prefs', { 'download.default_directory': args.directory })
driver = WebDriver(options=opts)
status = main(driver)
except KeyboardInterrupt:
status = 0
except Exception as e:
logger.critical('Fatal error', exc_info=e)
status = 1
finally:
driver.quit()
exit(status)

View File

@@ -0,0 +1,4 @@
cd /D .\..\
set SE_PROXY=http://127.0.0.1:10809
.\venv\Scripts\pythonw.exe .\main.py "user@example.com" "example"
@pause

View File

@@ -0,0 +1,4 @@
cd /D .\..\
python -m venv venv
.\venv\Scripts\pip.exe install -r ./requirements.txt
@pause

BIN
requirements.txt Normal file

Binary file not shown.

BIN
template.xlsx Normal file

Binary file not shown.

Binary file not shown.