update: bump 'common' version to 0.1.15
This commit is contained in:
264
index.html
264
index.html
@@ -128,157 +128,151 @@
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="module">
|
||||||
const $ = (selectors) => {
|
import { default as $, Rpc2, LogRecord } from '/';
|
||||||
return document.querySelector(selectors);
|
if (performance.getEntriesByType('navigation')[0].type === 'reload') await Rpc2.invoke('exit');
|
||||||
};
|
|
||||||
|
|
||||||
const $$ = (selectors) => {
|
let error = null;
|
||||||
return document.querySelectorAll(selectors);
|
let status = null;
|
||||||
};
|
let { profiles, parameters } = await Rpc2.invoke('context');
|
||||||
|
|
||||||
const $$$ = (selectors, event, listener) => {
|
let date = new Date();
|
||||||
$(selectors).addEventListener(event, listener);
|
let day = date.getDay() || 7;
|
||||||
};
|
$('#datefrom').valueAsNumber = date.setHours(-24 * (day - 1)) - date.getTimezoneOffset() * 60 * 1000;
|
||||||
|
$('#dateto').valueAsNumber = date.setHours(24 * 7) + date.getTimezoneOffset() * 60 * 1000;
|
||||||
|
$('#all').dispatchEvent(new Event('change'));
|
||||||
|
|
||||||
function main(profiles, args, status=null, error=null) {
|
$.set('#begin', 'click', async () => {
|
||||||
$$$('#begin', 'click', async () => {
|
switch (status) {
|
||||||
switch (status) {
|
case 'READY':
|
||||||
case 'READY':
|
let name = $('#name').value;
|
||||||
let name = $('#name').value;
|
let options = { profile: name };
|
||||||
let options = { profile: name };
|
|
||||||
|
for (let element of $.all("input[type='checkbox']:not(.local-only)")) {
|
||||||
for (let element of $$("input[type='checkbox']:not(.local-only)")) {
|
options[element.id] = element.checked;
|
||||||
options[element.id] = element.checked;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for (let element of $$("input[type='date']")) {
|
for (let element of $.all("input[type='date']")) {
|
||||||
options[element.id] = element.value;
|
options[element.id] = element.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let element of $$("input[type='number']")) {
|
for (let element of $.all("input[type='number']")) {
|
||||||
args[element.id] = element.valueAsNumber;
|
parameters[element.id] = element.valueAsNumber;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Rpc2.invoke('begin', options, args);
|
await Rpc2.invoke('begin', options, parameters);
|
||||||
break;
|
break;
|
||||||
case 'RUNNING':
|
case 'RUNNING':
|
||||||
await Rpc2.invoke('pause');
|
await Rpc2.invoke('pause');
|
||||||
break;
|
break;
|
||||||
case 'STANDBY':
|
case 'STANDBY':
|
||||||
await Rpc2.invoke('resume');
|
await Rpc2.invoke('resume');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$$$('#begin', 'click', () => {
|
$.set('#begin', 'click', () => {
|
||||||
$('#begin > span.icon').removeAttribute('hidden');
|
$('#begin > span.icon').removeAttribute('hidden');
|
||||||
$('#begin > span.text').innerText = '';
|
$('#begin > span.text').innerText = '';
|
||||||
$('#begin').classList.remove('pulse');
|
$('#begin').classList.remove('pulse');
|
||||||
$('#begin').disabled = true;
|
$('#begin').disabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
$$$('#cancel', 'click', async () => {
|
$.set('#cancel', 'click', async () => {
|
||||||
$('#cancel').disabled = true;
|
$('#cancel').disabled = true;
|
||||||
await Rpc2.invoke('cancel');
|
await Rpc2.invoke('cancel');
|
||||||
});
|
});
|
||||||
|
|
||||||
$$$('#skip', 'click', async () => {
|
$.set('#skip', 'click', async () => {
|
||||||
$('#skip').disabled = true;
|
$('#skip').disabled = true;
|
||||||
await Rpcs.invoke('skip');
|
await Rpcs.invoke('skip');
|
||||||
});
|
});
|
||||||
|
|
||||||
$$$('#logs', 'change', (e) => {
|
$.set('#logs', 'change', (e) => {
|
||||||
if (e.target.checked) $('#messages').removeAttribute('hidden');
|
if (e.target.checked) $('#messages').removeAttribute('hidden');
|
||||||
else $('#messages').setAttribute('hidden', '');
|
else $('#messages').setAttribute('hidden', '');
|
||||||
});
|
});
|
||||||
|
|
||||||
$$$('#name', 'change', (e) => {
|
$.set('#name', 'change', (e) => {
|
||||||
let p = profiles.find(o => o.name === e.target.value);
|
let p = profiles.find(o => o.name === e.target.value);
|
||||||
for (let k of Object.keys(p)) $(`#${k}`)?.setAttribute('value', p[k] ?? '');
|
for (let k of Object.keys(p)) $(`#${k}`)?.setAttribute('value', p[k] ?? '');
|
||||||
});
|
});
|
||||||
|
|
||||||
$$$('#all', 'change', (e) => {
|
$.set('#all', 'change', (e) => {
|
||||||
$('#name').disabled = e.target.checked;
|
$('#name').disabled = e.target.checked;
|
||||||
});
|
});
|
||||||
|
|
||||||
for (let item of profiles) {
|
if (parameters['account'] && parameters['password']) {
|
||||||
$('#name').add(new Option(item.name, item.name));
|
let account = new String(parameters['account']);
|
||||||
$('#name').dispatchEvent(new Event('change'));
|
let name = account.split('@', 1).pop();
|
||||||
|
name = name.charAt(0).toLocaleUpperCase() + name.slice(1);
|
||||||
|
document.title += ` (${name})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let item of profiles) {
|
||||||
|
$('#name').add(new Option(item.name, item.name));
|
||||||
|
$('#name').dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let item of $.all("input[type='number']")) {
|
||||||
|
item.value = parameters[item.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
while (await new Promise(o => setTimeout(o, 1000, true))) {
|
||||||
|
let history = await Rpc2.invoke('history').catch(() => []);
|
||||||
|
let logs = Array.from(history);
|
||||||
|
|
||||||
|
for (let record of logs) {
|
||||||
|
if ($('#messages').childNodes.length >= 500) $('#messages').childNodes.item(0)?.remove();
|
||||||
|
if (record.levelno >= 40) error = record;
|
||||||
|
let message = LogRecord.format(record);
|
||||||
|
let node = document.createTextNode(new String(message).concat('\n'));
|
||||||
|
$('#messages').appendChild(node);
|
||||||
|
$('#messages').scrollTop = $('#messages').scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let item of $$("input[type='number']")) {
|
status = await Rpc2.invoke('status').catch(() => null);
|
||||||
item.value = args[item.id];
|
$('#statusLabel').innerText = status ? status.charAt(0).toUpperCase() + status.slice(1).toLowerCase() : '';
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case 'IDLE':
|
||||||
|
continue;
|
||||||
|
case 'READY':
|
||||||
|
$('#begin > span.text').innerText = 'Begin';
|
||||||
|
$('#begin').classList.remove('pulse');
|
||||||
|
$('#progressLabel').innerText = '';
|
||||||
|
$('#remainingLabel').innerText = '';
|
||||||
|
break;
|
||||||
|
case 'RUNNING':
|
||||||
|
$('#begin > span.text').innerText = 'Pause';
|
||||||
|
$('#begin').classList.add('pulse');
|
||||||
|
|
||||||
|
let progress = await Rpc2.invoke('progress').catch(() => new Object());
|
||||||
|
let { task, number, index, limit } = progress;
|
||||||
|
$('#numberLabel').innerText = number ?? '';
|
||||||
|
$('#progressLabel').innerText = limit ? `${task}, ${parseFloat((index / limit * 100).toFixed(2))}% (${index}/${limit})` : task;
|
||||||
|
|
||||||
|
let [t1, t2] = await Rpc2.invoke('uptime').catch(() => []);
|
||||||
|
$('#uptimeLabel').innerText = Temporal.Duration.from({ seconds: t1 ?? 0 }).round({ largestUnit: 'hours' }).toLocaleString('en', { style: 'digital' });
|
||||||
|
|
||||||
|
let remaining = index && limit && t2 ? Math.floor((limit - index) / (index / t2)) : 0;
|
||||||
|
$('#remainingLabel').innerHTML = remaining ? Temporal.Duration.from({ seconds: remaining }).round({ largestUnit: 'hours' }).toLocaleString('en') : '';
|
||||||
|
break;
|
||||||
|
case 'STANDBY':
|
||||||
|
if (error !== null) {
|
||||||
|
alert(`(${error.levelname}) ${error.msg}\n${error.exc_text ?? ''}`);
|
||||||
|
error = null;
|
||||||
|
}
|
||||||
|
$('#begin > span.text').innerText = 'Resume';
|
||||||
|
$('#begin').classList.remove('pulse');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
$('#begin > span.icon').setAttribute('hidden', '');
|
||||||
|
$('#begin').disabled = false;
|
||||||
|
|
||||||
let date = new Date();
|
let actions = await Rpc2.invoke('actions').catch(() => new Object());
|
||||||
let day = date.getDay() || 7;
|
$('#cancel').disabled = !actions['Cancel'];
|
||||||
$('#datefrom').valueAsNumber = date.setHours(-24 * (day - 1)) - date.getTimezoneOffset() * 60 * 1000;
|
$('#skip').disabled = !actions['Skip'];
|
||||||
$('#dateto').valueAsNumber = date.setHours(24 * 7) + date.getTimezoneOffset() * 60 * 1000;
|
|
||||||
$('#all').dispatchEvent(new Event('change'));
|
|
||||||
|
|
||||||
if (args['account'] && args['password']) {
|
|
||||||
let account = new String(args['account']);
|
|
||||||
let name = account.split('@', 1).pop();
|
|
||||||
name = name.charAt(0).toLocaleUpperCase() + name.slice(1);
|
|
||||||
document.title += ` (${name})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
setInterval(async () => {
|
|
||||||
let history = await Rpc2.invoke('history');
|
|
||||||
let logs = Array.from(history);
|
|
||||||
|
|
||||||
for (let record of logs) {
|
|
||||||
if (record.levelno >= 40) error = record;
|
|
||||||
let message = LogRecord.format(record);
|
|
||||||
let node = document.createTextNode(new String(message).concat('\n'));
|
|
||||||
$('#messages').appendChild(node);
|
|
||||||
$('#messages').scrollTop = $('#messages').scrollHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
status = await Rpc2.invoke('status');
|
|
||||||
$('#statusLabel').innerText = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
|
|
||||||
|
|
||||||
switch (status) {
|
|
||||||
case 'IDLE':
|
|
||||||
return;
|
|
||||||
case 'READY':
|
|
||||||
$('#begin > span.text').innerText = 'Begin';
|
|
||||||
$('#begin').classList.remove('pulse');
|
|
||||||
$('#progressLabel').innerText = '';
|
|
||||||
$('#remainingLabel').innerText = '';
|
|
||||||
break;
|
|
||||||
case 'RUNNING':
|
|
||||||
$('#begin > span.text').innerText = 'Pause';
|
|
||||||
$('#begin').classList.add('pulse');
|
|
||||||
|
|
||||||
let progress = await Rpc2.invoke('progress');
|
|
||||||
let { task, number, index, limit } = progress;
|
|
||||||
$('#numberLabel').innerText = number ?? '';
|
|
||||||
$('#progressLabel').innerText = limit ? `${task}, ${parseFloat((index / limit * 100).toFixed(2))}% (${index}/${limit})` : task;
|
|
||||||
|
|
||||||
let [t1, t2] = await Rpc2.invoke('uptime').catch(() => []);
|
|
||||||
$('#uptimeLabel').innerText = Temporal.Duration.from({ seconds: t1 ?? 0 }).round({ largestUnit: 'hours' }).toLocaleString('en', { style: 'digital' });
|
|
||||||
|
|
||||||
let remaining = index && limit && t2 ? Math.floor((limit - index) / (index / t2)) : 0;
|
|
||||||
$('#remainingLabel').innerHTML = remaining ? Temporal.Duration.from({ seconds: remaining }).round({ largestUnit: 'hours' }).toLocaleString('en') : '';
|
|
||||||
break;
|
|
||||||
case 'STANDBY':
|
|
||||||
if (error !== null) {
|
|
||||||
alert(`(${error.levelname}) ${error.msg}\n${error.exc_text ?? ''}`);
|
|
||||||
error = null;
|
|
||||||
}
|
|
||||||
$('#begin > span.text').innerText = 'Resume';
|
|
||||||
$('#begin').classList.remove('pulse');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$('#begin > span.icon').setAttribute('hidden', '');
|
|
||||||
$('#begin').disabled = false;
|
|
||||||
|
|
||||||
let actions = await Rpc2.invoke('actions');
|
|
||||||
$('#cancel').disabled = !actions['Cancel'];
|
|
||||||
$('#skip').disabled = !actions['Skip'];
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -357,4 +351,4 @@ label {
|
|||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
141
main.py
141
main.py
@@ -11,8 +11,9 @@ from selenium.common.exceptions import TimeoutException, NoSuchElementException
|
|||||||
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.keys import Keys
|
||||||
|
|
||||||
from common import jsonrpc2
|
|
||||||
from common.utils import *
|
from common.utils import *
|
||||||
|
from common.timer import Timer
|
||||||
|
from common.jsonrpc2 import ServiceProvider
|
||||||
from common.actionflow import Action, ActionFlow
|
from common.actionflow import Action, ActionFlow
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@@ -37,8 +38,9 @@ WEBURL = "https://crm.xiaoman.cn/%s"
|
|||||||
APIURL = "https://%s.vosfactures.fr"
|
APIURL = "https://%s.vosfactures.fr"
|
||||||
|
|
||||||
def main(driver: WebDriver, logger = logging.getLogger('main')):
|
def main(driver: WebDriver, logger = logging.getLogger('main')):
|
||||||
http = PoolManager()
|
|
||||||
parameters = vars(args)
|
parameters = vars(args)
|
||||||
|
http = PoolManager()
|
||||||
|
sp = ServiceProvider.default()
|
||||||
|
|
||||||
class Status(Enum):
|
class Status(Enum):
|
||||||
IDLE = 0
|
IDLE = 0
|
||||||
@@ -86,41 +88,41 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
t1.start()
|
t1.start()
|
||||||
t2.start()
|
t2.start()
|
||||||
|
|
||||||
jsonrpc2.define('begin', begin)
|
sp.set('begin', begin)
|
||||||
jsonrpc2.define('pause', pause)
|
sp.set('pause', pause)
|
||||||
jsonrpc2.define('resume', resume)
|
sp.set('resume', resume)
|
||||||
jsonrpc2.define('status', lambda: status.name)
|
sp.set('status', lambda: status.name)
|
||||||
jsonrpc2.define('uptime', lambda: [t1.delta()])
|
sp.set('uptime', lambda: [t1.delta()])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
profiles = [
|
profiles = [
|
||||||
Profile(**{ k.lower().strip(): v.strip() for k, v in map(lambda o: str.split(o, '=', 2), p) })
|
Profile(**{ k.lower().strip(): v.strip() for k, v in map(lambda o: str.split(o, '=', 2), p) })
|
||||||
for p in parameters.get('profile')
|
for p in parameters['profile']
|
||||||
]
|
]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical('Unable to load profiles', exc_info=e)
|
logger.critical('Unable to load profiles', exc_info=e)
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
try:
|
try:
|
||||||
driver.get(str(Path('index.html').resolve()))
|
sp.set('context', lambda: { 'profiles': list(map(vars, profiles)), 'parameters': parameters })
|
||||||
driver.execute_script(jsonrpc2.prelude(), list(map(vars, profiles)), parameters)
|
driver.get(sp.run())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical('Unable to load starup page', exc_info=e)
|
logger.critical('Unable to load starup page', exc_info=e)
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
try:
|
try:
|
||||||
driver.switch_to.new_window('tab')
|
driver.switch_to.new_window('tab')
|
||||||
driver.set_page_load_timeout(parameters.get('timeout'))
|
driver.set_page_load_timeout(parameters['timeout'])
|
||||||
driver.get(WEBURL % 'product')
|
driver.get(WEBURL % 'product')
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
logger.warning('Timeout')
|
logger.warning('Timeout')
|
||||||
driver.execute_script("window.stop();")
|
driver.execute_script("window.stop();")
|
||||||
|
|
||||||
setup(driver, parameters.get('attempts'), parameters.get('timeout'), parameters.get('interval'))
|
setup(driver, parameters)
|
||||||
until(lambda x: 'loginProgress' in x.find_element(By.TAG_NAME, "body").get_attribute('class'), watch=False)
|
until(lambda x: 'loginProgress' in x.find_element(By.TAG_NAME, "body").get_attribute('class'), watch=False)
|
||||||
logger.info('Waiting for authentication to complete...')
|
logger.info('Waiting for authentication to complete...')
|
||||||
|
|
||||||
if (account := parameters.get('account')) and (password := parameters.get('password')):
|
if (account := parameters['account']) and (password := parameters['password']):
|
||||||
try:
|
try:
|
||||||
logger.info('Logging in as %s (%s)', str.split(account, '@', 1).pop(0).capitalize(), account)
|
logger.info('Logging in as %s (%s)', str.split(account, '@', 1).pop(0).capitalize(), account)
|
||||||
locate("input.account").send_keys(account)
|
locate("input.account").send_keys(account)
|
||||||
@@ -137,7 +139,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
logger.info('Done')
|
logger.info('Done')
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
sleep(parameters.get('interval'))
|
sleep(parameters['interval'])
|
||||||
|
|
||||||
class ProductInfo:
|
class ProductInfo:
|
||||||
def __init__(self, file):
|
def __init__(self, file):
|
||||||
@@ -164,7 +166,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
logger.info('Downloading product list...')
|
logger.info('Downloading product list...')
|
||||||
click("header .okki-space .okki-space-item:nth-child(1) button")
|
click("header .okki-space .okki-space-item:nth-child(1) button")
|
||||||
click(".okki-dropdown button")
|
click(".okki-dropdown button")
|
||||||
sleep(parameters.get('interval'))
|
sleep(parameters['interval'])
|
||||||
click(".okki-modal.product-export-wrap .mm-selector-rendered")
|
click(".okki-modal.product-export-wrap .mm-selector-rendered")
|
||||||
click(".mm-outside.ui-field-selector-popper .selector-area:nth-child(1) button")
|
click(".mm-outside.ui-field-selector-popper .selector-area:nth-child(1) button")
|
||||||
click(".okki-modal.product-export-wrap .okki-modal-footer button.okki-btn-primary")
|
click(".okki-modal.product-export-wrap .okki-modal-footer button.okki-btn-primary")
|
||||||
@@ -172,14 +174,14 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
click(".okki-modal.product-export-wrap .okki-modal-footer button.okki-btn-primary")
|
click(".okki-modal.product-export-wrap .okki-modal-footer button.okki-btn-primary")
|
||||||
sleep(parameters.get('interval'))
|
sleep(parameters['interval'])
|
||||||
click(".okki-modal.product-export-wrap .okki-modal-body .virtual-list-wrap .vue-recycle-scroller__item-wrapper > div:nth-child(1) button.okki-btn-link", wait=False)
|
click(".okki-modal.product-export-wrap .okki-modal-body .virtual-list-wrap .vue-recycle-scroller__item-wrapper > div:nth-child(1) button.okki-btn-link", wait=False)
|
||||||
filename = locate(".okki-modal.product-export-wrap .okki-modal-body .virtual-list-wrap .vue-recycle-scroller__item-wrapper > div:nth-child(1) > div > div:nth-child(1) span").get_attribute('title')
|
filename = locate(".okki-modal.product-export-wrap .okki-modal-body .virtual-list-wrap .vue-recycle-scroller__item-wrapper > div:nth-child(1) > div > div:nth-child(1) span").get_attribute('title')
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
sleep(parameters.get('interval'))
|
sleep(parameters['interval'])
|
||||||
|
|
||||||
file = Path(parameters.get('directory')).joinpath(filename)
|
file = Path(parameters['directory']).joinpath(filename)
|
||||||
until(lambda _: file.exists(), watch=False)
|
until(lambda _: file.exists(), watch=False)
|
||||||
p = ProductInfo(file)
|
p = ProductInfo(file)
|
||||||
driver.close()
|
driver.close()
|
||||||
@@ -190,12 +192,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
logger.critical('Unable to load products', exc_info=e)
|
logger.critical('Unable to load products', exc_info=e)
|
||||||
return 4
|
return 4
|
||||||
|
|
||||||
def wait(seconds: float):
|
def fetch(url: str, method = 'GET', retry = parameters['attempts']):
|
||||||
while not sleep(seconds):
|
|
||||||
if status == Status.RUNNING:
|
|
||||||
break
|
|
||||||
|
|
||||||
def fetch(url: str, method = 'GET', retry = parameters.get('attempts')):
|
|
||||||
for attempt in range(1, retry + 1):
|
for attempt in range(1, retry + 1):
|
||||||
try:
|
try:
|
||||||
response = http.request(method, url)
|
response = http.request(method, url)
|
||||||
@@ -212,8 +209,18 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def perform(cls):
|
def perform(cls):
|
||||||
wait(1)
|
if status == Status.RUNNING: return False
|
||||||
|
sleep(0.2); return True
|
||||||
|
|
||||||
|
class Sleep(Action):
|
||||||
|
@classmethod
|
||||||
|
def prepare(cls):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def perform(cls):
|
||||||
|
sleep(parameters['interval'])
|
||||||
|
return False
|
||||||
|
|
||||||
class Cancel(Action):
|
class Cancel(Action):
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -241,8 +248,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
|
|
||||||
flow = ActionFlow()
|
flow = ActionFlow()
|
||||||
flow.append(Wait)
|
flow.append(Wait)
|
||||||
flow.allow(Wait)
|
flow.append(Sleep)
|
||||||
flow.do(Wait)
|
|
||||||
flow.append(Cancel)
|
flow.append(Cancel)
|
||||||
flow.append(Skip)
|
flow.append(Skip)
|
||||||
|
|
||||||
@@ -250,13 +256,18 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
progress = { 'task': '' }
|
progress = { 'task': '' }
|
||||||
selection = 0
|
selection = 0
|
||||||
|
|
||||||
jsonrpc2.define('actions', lambda: flow.capabilities())
|
sp.set('actions', lambda: flow.capabilities())
|
||||||
jsonrpc2.define('cancel', lambda: flow.do(Cancel))
|
sp.set('cancel', lambda: flow.do(Cancel))
|
||||||
jsonrpc2.define('skip', lambda: flow.do(Skip))
|
sp.set('skip', lambda: flow.do(Skip))
|
||||||
jsonrpc2.define('progress', lambda: progress)
|
sp.set('progress', lambda: progress)
|
||||||
|
|
||||||
while not wait(1):
|
while True:
|
||||||
try:
|
try:
|
||||||
|
flow.allow(Wait)
|
||||||
|
flow.allow(Sleep)
|
||||||
|
flow.do(Wait)
|
||||||
|
flow.react()
|
||||||
|
|
||||||
for i in range(len(driver.window_handles), 1, -1):
|
for i in range(len(driver.window_handles), 1, -1):
|
||||||
driver.switch_to.window(driver.window_handles[i-1])
|
driver.switch_to.window(driver.window_handles[i-1])
|
||||||
driver.close()
|
driver.close()
|
||||||
@@ -284,8 +295,8 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
progress['task'] = 'Task 1 of 4'
|
progress['task'] = 'Task 1 of 4'
|
||||||
t2.clear()
|
t2.clear()
|
||||||
t2.start()
|
t2.start()
|
||||||
jsonrpc2.remove('uptime')
|
sp.pop('uptime')
|
||||||
jsonrpc2.define('uptime', lambda: [t1.delta(), t2.delta()])
|
sp.set('uptime', lambda: [t1.delta(), t2.delta()])
|
||||||
|
|
||||||
base = APIURL % profile.subdomain
|
base = APIURL % profile.subdomain
|
||||||
data = list()
|
data = list()
|
||||||
@@ -308,8 +319,9 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
|
|
||||||
logger.info('Downloading invoices (%d)', page)
|
logger.info('Downloading invoices (%d)', page)
|
||||||
data.extend(result)
|
data.extend(result)
|
||||||
|
flow.do(Wait)
|
||||||
|
flow.do(Sleep)
|
||||||
flow.react()
|
flow.react()
|
||||||
wait(parameters.get('interval'))
|
|
||||||
except Skip:
|
except Skip:
|
||||||
pass
|
pass
|
||||||
except Cancel:
|
except Cancel:
|
||||||
@@ -373,6 +385,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
for i, item in enumerate(data, 1):
|
for i, item in enumerate(data, 1):
|
||||||
|
flow.do(Wait)
|
||||||
flow.react()
|
flow.react()
|
||||||
number: str = item['number']
|
number: str = item['number']
|
||||||
logger.info('[%d/%d] Preprocessing data for %s', i, len(data), number)
|
logger.info('[%d/%d] Preprocessing data for %s', i, len(data), number)
|
||||||
@@ -447,8 +460,9 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
filename = f'Order-Import-{profile.name}-{datetime.now().strftime('%Y%m%d-%H%M%S-%f')}.xlsx'
|
filename = f'Order-Import-{profile.name}-{datetime.now().strftime('%Y%m%d-%H%M%S-%f')}.xlsx'
|
||||||
file = Path(parameters.get('directory')).joinpath(filename)
|
file = Path(parameters['directory']).joinpath(filename)
|
||||||
logger.info('Saving excel file to %s', str(file))
|
logger.info('Saving excel file to %s', str(file))
|
||||||
|
flow.do(Wait)
|
||||||
flow.react()
|
flow.react()
|
||||||
workbook.save(file)
|
workbook.save(file)
|
||||||
except Skip:
|
except Skip:
|
||||||
@@ -473,6 +487,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
click(".product-import-img-box .mm-selector-rendered")
|
click(".product-import-img-box .mm-selector-rendered")
|
||||||
click(".mm-outside.mm-select-dropdown ul li:nth-child(%d) span" % (1 if options.get('draft') else 6))
|
click(".mm-outside.mm-select-dropdown ul li:nth-child(%d) span" % (1 if options.get('draft') else 6))
|
||||||
|
|
||||||
|
flow.do(Wait)
|
||||||
flow.react()
|
flow.react()
|
||||||
locate(".big-file-upload input", wait=False).send_keys(str(file))
|
locate(".big-file-upload input", wait=False).send_keys(str(file))
|
||||||
click(".product-import-img-footer button")
|
click(".product-import-img-footer button")
|
||||||
@@ -483,7 +498,8 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
innerText = driver.find_element(By.CSS_SELECTOR, ".img-box-result-title").get_attribute('innerText')
|
innerText = driver.find_element(By.CSS_SELECTOR, ".img-box-result-title").get_attribute('innerText')
|
||||||
if innerText == '导入完成': break
|
if innerText == '导入完成': break
|
||||||
except:
|
except:
|
||||||
wait(parameters.get('interval'))
|
flow.do(Sleep)
|
||||||
|
flow.react()
|
||||||
|
|
||||||
click(".mm-notification-container .mm-icon-close")
|
click(".mm-notification-container .mm-icon-close")
|
||||||
click(".product-import-img-footer button.mm-button__primary")
|
click(".product-import-img-footer button.mm-button__primary")
|
||||||
@@ -492,10 +508,12 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
if err.get_attribute('disabled') is None:
|
if err.get_attribute('disabled') is None:
|
||||||
logger.warning('Incomplete import detected; downloaded 1 related document')
|
logger.warning('Incomplete import detected; downloaded 1 related document')
|
||||||
err.click()
|
err.click()
|
||||||
wait(1)
|
flow.do(Sleep)
|
||||||
|
flow.react()
|
||||||
|
|
||||||
click(".mm-tbody table tbody tr:nth-child(1) td:nth-child(3) a")
|
click(".mm-tbody table tbody tr:nth-child(1) td:nth-child(3) a")
|
||||||
wait(parameters.get('interval'))
|
flow.do(Sleep)
|
||||||
|
flow.react()
|
||||||
logger.info('Done')
|
logger.info('Done')
|
||||||
except Skip:
|
except Skip:
|
||||||
pass
|
pass
|
||||||
@@ -551,7 +569,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
attempts = 0
|
attempts = 0
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if attempts > parameters.get('attempts'):
|
if attempts > parameters['attempts']:
|
||||||
logger.warning('Exhausted all allowed attempts; skipping %s', number)
|
logger.warning('Exhausted all allowed attempts; skipping %s', number)
|
||||||
index += 1
|
index += 1
|
||||||
attempts = 0
|
attempts = 0
|
||||||
@@ -561,6 +579,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
progress['index'] = index
|
progress['index'] = index
|
||||||
flow.allow(Cancel)
|
flow.allow(Cancel)
|
||||||
flow.allow(Skip)
|
flow.allow(Skip)
|
||||||
|
flow.do(Wait)
|
||||||
flow.react()
|
flow.react()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -579,17 +598,20 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
'array_flag': 0
|
'array_flag': 0
|
||||||
}])
|
}])
|
||||||
driver.get(url.encode())
|
driver.get(url.encode())
|
||||||
wait(parameters.get('interval'))
|
flow.do(Wait)
|
||||||
|
flow.do(Sleep)
|
||||||
flow.react()
|
flow.react()
|
||||||
url = Parse(driver.current_url)
|
url = Parse(driver.current_url)
|
||||||
if url.get('page') != page: raise Exception(number)
|
if url.get('page') != page: raise Exception(number)
|
||||||
|
|
||||||
wait(parameters.get('interval'))
|
flow.do(Sleep)
|
||||||
|
flow.react()
|
||||||
link = locate(".virtual-list-wrap .vue-recycle-scroller .vue-recycle-scroller__item-wrapper > div:nth-child(1) .cell[data-cci='1'] a", wait=False)
|
link = locate(".virtual-list-wrap .vue-recycle-scroller .vue-recycle-scroller__item-wrapper > div:nth-child(1) .cell[data-cci='1'] a", wait=False)
|
||||||
if link.text != number: continue
|
if link.text != number: continue
|
||||||
logger.info('[%d/%d] Processing %s...', index+1, len(data), number)
|
logger.info('[%d/%d] Processing %s...', index+1, len(data), number)
|
||||||
link.click()
|
link.click()
|
||||||
driver.switch_to.window(driver.window_handles[3])
|
driver.switch_to.window(driver.window_handles[3])
|
||||||
|
flow.do(Wait)
|
||||||
flow.react()
|
flow.react()
|
||||||
click(".sticky .okki-space-item:nth-child(1) button")
|
click(".sticky .okki-space-item:nth-child(1) button")
|
||||||
break
|
break
|
||||||
@@ -614,7 +636,8 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
driver.switch_to.new_window('tab')
|
driver.switch_to.new_window('tab')
|
||||||
url = Parse(WEBURL % 'crm/business/list')
|
url = Parse(WEBURL % 'crm/business/list')
|
||||||
driver.get(url.encode({ 'mode': 'list' }))
|
driver.get(url.encode({ 'mode': 'list' }))
|
||||||
wait(parameters.get('interval'))
|
flow.do(Wait)
|
||||||
|
flow.do(Sleep)
|
||||||
flow.react()
|
flow.react()
|
||||||
|
|
||||||
try: click(".new-wrapper .paas-next-invoice-list-filter-line-wrapper .okki-btn-background-ghost", wait=False)
|
try: click(".new-wrapper .paas-next-invoice-list-filter-line-wrapper .okki-btn-background-ghost", wait=False)
|
||||||
@@ -627,12 +650,14 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
url.set('curPage', page)
|
url.set('curPage', page)
|
||||||
url.set('pageSize', 1)
|
url.set('pageSize', 1)
|
||||||
driver.get(url.encode({ 'keyword': match, 'search_field': 'serial_keyword' }))
|
driver.get(url.encode({ 'keyword': match, 'search_field': 'serial_keyword' }))
|
||||||
wait(parameters.get('interval'))
|
flow.do(Wait)
|
||||||
|
flow.do(Sleep)
|
||||||
flow.react()
|
flow.react()
|
||||||
url = Parse(driver.current_url)
|
url = Parse(driver.current_url)
|
||||||
if url.get('curPage') != page: raise Exception(match)
|
if url.get('curPage') != page: raise Exception(match)
|
||||||
|
|
||||||
wait(parameters.get('interval'))
|
flow.do(Sleep)
|
||||||
|
flow.react()
|
||||||
cell = locate(".virtual-list-wrap .vue-recycle-scroller .row-item > .cell:nth-child(3) .ow-serial-read-pretty_ellipsis", wait=False)
|
cell = locate(".virtual-list-wrap .vue-recycle-scroller .row-item > .cell:nth-child(3) .ow-serial-read-pretty_ellipsis", wait=False)
|
||||||
if cell.text != match: continue
|
if cell.text != match: continue
|
||||||
link = locate(".virtual-list-wrap .vue-recycle-scroller .row-item > .cell:nth-child(6) a", wait=False)
|
link = locate(".virtual-list-wrap .vue-recycle-scroller .row-item > .cell:nth-child(6) a", wait=False)
|
||||||
@@ -656,6 +681,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
|
|
||||||
if opportunity is not None:
|
if opportunity is not None:
|
||||||
try:
|
try:
|
||||||
|
flow.do(Wait)
|
||||||
flow.react()
|
flow.react()
|
||||||
dropdown = locate("#rc_select_1")
|
dropdown = locate("#rc_select_1")
|
||||||
dropdown.clear()
|
dropdown.clear()
|
||||||
@@ -690,9 +716,10 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
for page in count(1):
|
for page in count(1):
|
||||||
hits = 0
|
hits = 0
|
||||||
iteration = 0
|
iteration = 0
|
||||||
|
flow.do(Wait)
|
||||||
flow.react()
|
flow.react()
|
||||||
|
|
||||||
while hits < pagination and iteration < parameters.get('attempts'):
|
while hits < pagination and iteration < parameters['attempts']:
|
||||||
iteration += 1
|
iteration += 1
|
||||||
height = int(wrapper.get_attribute('clientHeight')) if iteration > 1 else 0
|
height = int(wrapper.get_attribute('clientHeight')) if iteration > 1 else 0
|
||||||
driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", wrapper)
|
driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", wrapper)
|
||||||
@@ -700,6 +727,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
rows = wrapper.find_elements(By.CSS_SELECTOR, ".row-item")
|
rows = wrapper.find_elements(By.CSS_SELECTOR, ".row-item")
|
||||||
|
|
||||||
for row in reversed(rows) if iteration > 1 else rows:
|
for row in reversed(rows) if iteration > 1 else rows:
|
||||||
|
flow.do(Wait)
|
||||||
flow.react()
|
flow.react()
|
||||||
driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", wrapper)
|
driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", wrapper)
|
||||||
driver.execute_script("arguments[0].scrollTo(0, arguments[1]);", wrapper, height)
|
driver.execute_script("arguments[0].scrollTo(0, arguments[1]);", wrapper, height)
|
||||||
@@ -712,7 +740,8 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
driver.execute_script("arguments[0].scroll(400, 0);", wrapper)
|
driver.execute_script("arguments[0].scroll(400, 0);", wrapper)
|
||||||
driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", row)
|
driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", row)
|
||||||
driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", wrapper)
|
driver.execute_script("arguments[0].scrollIntoView({ block: 'center' });", wrapper)
|
||||||
wait(parameters.get('interval'))
|
flow.do(Sleep)
|
||||||
|
flow.react()
|
||||||
target = row.find_element(By.CSS_SELECTOR, ".cell[data-cci='6'] input")
|
target = row.find_element(By.CSS_SELECTOR, ".cell[data-cci='6'] input")
|
||||||
|
|
||||||
if (target.get_attribute('value') == '0'):
|
if (target.get_attribute('value') == '0'):
|
||||||
@@ -726,6 +755,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
button = locate(".text-right li.okki-pagination-next button", condition=None)
|
button = locate(".text-right li.okki-pagination-next button", condition=None)
|
||||||
if button.get_attribute('disabled') is not None and len(ids) < len(positions):
|
if button.get_attribute('disabled') is not None and len(ids) < len(positions):
|
||||||
raise Exception('Product list imcomplete; expected %d, got %d' % (len(positions), len(ids)))
|
raise Exception('Product list imcomplete; expected %d, got %d' % (len(positions), len(ids)))
|
||||||
|
flow.do(Wait)
|
||||||
flow.react()
|
flow.react()
|
||||||
click(button)
|
click(button)
|
||||||
except Skip:
|
except Skip:
|
||||||
@@ -741,16 +771,19 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
click(".ow-box button.okki-btn-round", wait=False)
|
click(".ow-box button.okki-btn-round", wait=False)
|
||||||
wait(parameters.get('interval'))
|
flow.do(Sleep)
|
||||||
|
flow.react()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning('Unable to unset additional fees; skipping', exc_info=e)
|
logger.warning('Unable to unset additional fees; skipping', exc_info=e)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
flow.do(Wait)
|
||||||
flow.react()
|
flow.react()
|
||||||
flow.allow(Cancel, False)
|
flow.allow(Cancel, False)
|
||||||
flow.allow(Skip, False)
|
flow.allow(Skip, False)
|
||||||
click(".sticky.bottom-0 button.okki-btn-primary", condition=None)
|
click(".sticky.bottom-0 button.okki-btn-primary", condition=None)
|
||||||
wait(parameters.get('interval'))
|
flow.do(Sleep)
|
||||||
|
flow.react()
|
||||||
except Skip:
|
except Skip:
|
||||||
pass
|
pass
|
||||||
except Cancel:
|
except Cancel:
|
||||||
@@ -775,14 +808,6 @@ if __name__ == '__main__':
|
|||||||
level = logging.getLevelNamesMapping().get(args.log_level, 'INFO')
|
level = logging.getLevelNamesMapping().get(args.log_level, 'INFO')
|
||||||
logger.setLevel(level)
|
logger.setLevel(level)
|
||||||
|
|
||||||
logger.info('Initializing...')
|
|
||||||
history = jsonrpc2.History()
|
|
||||||
logger.addHandler(history)
|
|
||||||
opts = jsonrpc2.Options()
|
|
||||||
jsonrpc2.define('history', lambda: history.truncate())
|
|
||||||
jsonrpc2.run(opts)
|
|
||||||
|
|
||||||
logger.info('Creating automation instance')
|
|
||||||
opts = ChromeOptions()
|
opts = ChromeOptions()
|
||||||
opts.enable_downloads = True
|
opts.enable_downloads = True
|
||||||
opts.add_argument('--deny-permission-prompts')
|
opts.add_argument('--deny-permission-prompts')
|
||||||
@@ -791,6 +816,8 @@ if __name__ == '__main__':
|
|||||||
with keep.presenting():
|
with keep.presenting():
|
||||||
driver = Chrome(options=opts)
|
driver = Chrome(options=opts)
|
||||||
status = main(driver)
|
status = main(driver)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
status = 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical('Fatal error', exc_info=e)
|
logger.critical('Fatal error', exc_info=e)
|
||||||
status = 1
|
status = 1
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
Reference in New Issue
Block a user