Compare commits
20 Commits
0f1f04a549
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e5b425ba87 | |||
| 044cf2503a | |||
| 3b9af6e57e | |||
| 9779d1d407 | |||
| 1d6d7482b4 | |||
| 423213759d | |||
| 7abaa1b059 | |||
| 8665a8dd1d | |||
| 875aa5e761 | |||
| 6e46b8faf5 | |||
| 714818bb15 | |||
| ba548e8b13 | |||
| 73ffc1a937 | |||
| 5e98510252 | |||
|
|
2fde41b0b3 | ||
| da87d22bcb | |||
|
|
61a64bed41 | ||
| 4846b33985 | |||
| 45d576e229 | |||
| e0d62262f3 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
.workbooks/
|
||||
/.vscode
|
||||
/venv
|
||||
53
README.md
53
README.md
@@ -1,53 +0,0 @@
|
||||
# 商机纸质打印操作步骤
|
||||
|
||||
1. 导出商机数据
|
||||
|
||||
|
||||

|
||||
|
||||
2. 筛选数据
|
||||
|
||||
|
||||

|
||||
|
||||
3. 在“导出设置”中设置导出字段(字段和顺序必须与图中一致)
|
||||
|
||||
|
||||

|
||||
|
||||
4. 导出详细版
|
||||
|
||||
|
||||

|
||||
|
||||
5. 在数据源中全选并复制(Ctrl+A, Ctrl+C)
|
||||
|
||||
|
||||

|
||||
|
||||
6. 打开导出宏,启用宏,选择Data工作簿,全选(Ctrl+A)并清除,然后粘贴(Ctrl+V)
|
||||
|
||||
|
||||

|
||||
|
||||
7. 选择Config工作簿,输入导出位置(注意:路径必须以分隔符 “\” 结尾)
|
||||
|
||||
|
||||

|
||||
|
||||
8. 运行宏
|
||||
|
||||
|
||||

|
||||
|
||||
9. 宏运行完成,Excel会有弹窗提示,在指定文件夹找到导出的商机
|
||||
|
||||
|
||||

|
||||
|
||||
10. 全选,右键,选择显示更多选项,点击打印
|
||||
|
||||
|
||||

|
||||
|
||||
11. 完成打印,由当周主持人打印,在日升会的时候发给同事。
|
||||
48
index.html
Normal file
48
index.html
Normal 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
223
main.py
Normal 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)
|
||||
4
profiles/profile-example.bat
Normal file
4
profiles/profile-example.bat
Normal 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
|
||||
4
profiles/setup-virtualenv.bat
Normal file
4
profiles/setup-virtualenv.bat
Normal 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
BIN
requirements.txt
Normal file
Binary file not shown.
BIN
template.xlsx
Normal file
BIN
template.xlsx
Normal file
Binary file not shown.
BIN
商机数据导出宏.xlsm
BIN
商机数据导出宏.xlsm
Binary file not shown.
Reference in New Issue
Block a user