update: bump 'common' version to 0.1.18

This commit is contained in:
2026-07-16 11:59:53 +08:00
parent 0150468161
commit 8ba58f4749
3 changed files with 94 additions and 131 deletions
+10 -17
View File
@@ -130,12 +130,10 @@
<script type="module">
import { default as $, Rpc2, LogRecord } from '/';
if (performance.getEntriesByType('navigation')[0].type === 'reload') await Rpc2.invoke('exit');
if (performance.getEntriesByType('navigation')[0].type === 'reload') await Rpc2.notify('exit');
let error = null;
let status = null;
let { profiles, parameters } = await Rpc2.invoke('context');
let status = null;
let date = new Date();
let day = date.getDay() || 7;
$('#datefrom').valueAsNumber = date.setHours(-24 * (day - 1)) - date.getTimezoneOffset() * 60 * 1000;
@@ -160,7 +158,7 @@ $.set('#begin', 'click', async () => {
parameters[element.id] = element.valueAsNumber;
}
await Rpc2.invoke('begin', options, parameters);
await Rpc2.invoke('begin', { params: [options, parameters] });
break;
case 'RUNNING':
await Rpc2.invoke('pause');
@@ -180,12 +178,12 @@ $.set('#begin', 'click', () => {
$.set('#cancel', 'click', async () => {
$('#cancel').disabled = true;
await Rpc2.invoke('cancel');
await Rpc2.notify('cancel');
});
$.set('#skip', 'click', async () => {
$('#skip').disabled = true;
await Rpcs.invoke('skip');
await Rpc2.notify('skip');
});
$.set('#logs', 'change', (e) => {
@@ -219,12 +217,12 @@ for (let item of $.all("input[type='number']")) {
}
while (await new Promise(o => setTimeout(o, 1000, true))) {
let history = await Rpc2.invoke('history').catch(() => []);
let history = await Rpc2.invoke('logs').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;
if (record.levelno >= 40) alert(`(${record.levelname}) ` + [record.msg, record.exc_text].filter(Boolean).join('\n'));
let message = LogRecord.format(record);
let node = document.createTextNode(new String(message).concat('\n'));
$('#messages').appendChild(node);
@@ -247,22 +245,17 @@ while (await new Promise(o => setTimeout(o, 1000, true))) {
$('#begin > span.text').innerText = 'Pause';
$('#begin').classList.add('pulse');
let progress = await Rpc2.invoke('progress').catch(() => new Object());
let { task, number, index, limit } = progress;
let { task, number, index, limit } = await Rpc2.invoke('progress').catch(() => new Object());
$('#numberLabel').innerText = number ?? '';
$('#progressLabel').innerText = limit ? `${task}, ${parseFloat((index / limit * 100).toFixed(2))}% (${index}/${limit})` : task;
$('#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') : '';
$('#remainingLabel').innerText = remaining > 0 ? 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;
+84 -114
View File
@@ -1,4 +1,3 @@
import unicodedata
import argparse
import openpyxl
import logging
@@ -20,7 +19,7 @@ from enum import Enum
from wakepy import keep
from pathlib import Path
from datetime import datetime
from itertools import repeat, count
from itertools import count
from urllib3 import PoolManager
parser = argparse.ArgumentParser(description="Order Import")
@@ -45,8 +44,8 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
class Status(Enum):
IDLE = 0
READY = 1
RUNNING = 3
STANDBY = 4
RUNNING = 2
STANDBY = 3
class Profile:
def __init__(self, name, subdomain, token, remise, person=None, prefix=None, suffix=None):
@@ -88,9 +87,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
t1.start()
t2.start()
sp.set('begin', begin)
sp.set('pause', pause)
sp.set('resume', resume)
sp.add(begin, pause, resume)
sp.set('status', lambda: status.name)
sp.set('uptime', lambda: [t1.delta()])
@@ -109,7 +106,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
except Exception as e:
logger.critical('Unable to load starup page', exc_info=e)
return 3
try:
driver.switch_to.new_window('tab')
driver.set_page_load_timeout(parameters['timeout'])
@@ -117,10 +114,10 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
except TimeoutException:
logger.warning('Timeout')
driver.execute_script("window.stop();")
setup(driver, parameters)
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...')
if (account := parameters['account']) and (password := parameters['password']):
try:
@@ -158,7 +155,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
if self.wb.active.cell(row, a+1).value == needle:
b = row
break
if b is None: return dict()
return { k: self.wb.active.cell(b, c+1).value for c, k in enumerate(self.headers) }
@@ -186,8 +183,8 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
p = ProductInfo(file)
driver.close()
driver.switch_to.window(driver.window_handles[0])
status = Status.READY
logger.info('Done')
status = Status.READY
except Exception as e:
logger.critical('Unable to load products', exc_info=e)
return 4
@@ -200,13 +197,13 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
return result
except Exception as e:
logger.warning('Error while fetching data from %s, retrying... (%d)', url, attempt, exc_info=e)
assert attempt < retry
assert attempt < retry, "Exceeded maximum retry attempts"
class Wait(Action):
@classmethod
def prepare(cls):
return True
@classmethod
def perform(cls):
if status == Status.RUNNING: return False
@@ -216,7 +213,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
@classmethod
def prepare(cls):
return True
@classmethod
def perform(cls):
sleep(parameters['interval'])
@@ -228,7 +225,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
nonlocal status
status = Status.RUNNING
return True
@classmethod
def perform(cls):
nonlocal status
@@ -240,25 +237,20 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
@classmethod
def prepare(cls):
return True
@classmethod
def perform(cls):
driver.switch_to.window(driver.current_window_handle)
raise cls
flow = ActionFlow()
flow.append(Wait)
flow.append(Sleep)
flow.append(Cancel)
flow.append(Skip)
flow.stage(Wait, Sleep, Cancel, Skip)
profile = None
progress = { 'task': '' }
selection = 0
sp.add(pairs=[ (k.lower(), v) for k, v in flow ])
sp.set('actions', lambda: flow.capabilities())
sp.set('cancel', lambda: flow.queue(Cancel))
sp.set('skip', lambda: flow.queue(Skip))
sp.set('progress', lambda: progress)
while not flow.react(Wait):
@@ -303,7 +295,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
logger.info('Profile selected: %s', profile.name)
logger.info('Date from %s to %s', df, dt)
flow.allow(Cancel)
flow.allow(Skip, False)
flow.deter(Skip)
for page in count(1):
try:
@@ -388,17 +380,17 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
if 'error' in (category := fetch(f'{base}/categories/{o}.json?api_token={profile.token}')):
error = category['error']
code = category['code']
logger.warning("Error while fetching 'category' (code: %s, message: %s); skipping", code, error)
logger.warning("Error while fetching field 'category' (code: %s, message: %s); skipping", code, error)
continue
if (client := clients.get(o := item['client_id'])) is None:
if 'error' in (client := fetch(f'{base}/clients/{o}.json?api_token={profile.token}')):
error = client['error']
code = client['code']
logger.warning("Error while fetching 'client' (code: %s, message: %s); skipping", code, error)
logger.warning("Error while fetching field 'client' (code: %s, message: %s); skipping", code, error)
continue
identity = client['external_id'] if client['company'] else profile.person
identity = client['shortcut'] if client['company'] else profile.person
date: str = item['issue_date']
kind: str = item['kind']
total = float(item['price_net'])
@@ -453,7 +445,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
try:
filename = f'Order-Import-{profile.name}-{datetime.now().strftime('%Y%m%d-%H%M%S-%f')}.xlsx'
file = Path(parameters['directory']).joinpath(filename)
logger.info('Saving excel file to %s', str(file))
logger.info('Saving document at %s', str(file))
flow.react(Wait)
workbook.save(file)
except Skip:
@@ -461,12 +453,12 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
except Cancel:
continue
except Exception as e:
logger.error('Error while saving excel file', exc_info=e)
logger.error('Error while saving document', exc_info=e)
status = Status.STANDBY
continue
try:
logger.info('Uploading...')
logger.info('Uploading data...')
progress.clear()
progress['task'] = 'Task 3 of 4'
t2.clear()
@@ -477,29 +469,26 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
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(".mm-outside.mm-select-dropdown ul li:nth-child(%d) span" % (1 if options.get('draft') else 6))
flow.react(Wait)
locate(".big-file-upload input", wait=False).send_keys(str(file))
click(".product-import-img-footer button")
click(".product-import-img-footer button.mm-button__primary")
while True:
while not flow.react(Wait, Sleep):
try:
innerText = driver.find_element(By.CSS_SELECTOR, ".img-box-result-title").get_attribute('innerText')
if innerText == '导入完成': break
except:
flow.react(Sleep)
click(".mm-notification-container .mm-icon-close")
click(".product-import-img-footer button.mm-button__primary")
err = locate(".mm-tbody table tbody tr:nth-child(1) td:nth-child(5) .okki-space-item:nth-child(1) button")
err = locate(".mm-tbody table tbody tr:nth-child(1) td:nth-child(5) .okki-space-item:nth-child(1) button", wait=False)
ok = locate(".mm-tbody table tbody tr:nth-child(1) td:nth-child(3) a", wait=False)
break
except: pass
try: click(".product-import-img-footer button.mm-button__primary", wait=False)
except: pass
try: click(".list-header-wrap button", wait=False)
except: pass
if err.get_attribute('disabled') is None:
logger.warning('Incomplete import detected; downloaded 1 related document')
err.click()
click(err, condition=None)
logger.warning('Incomplete import detected; downloading 1 related document')
flow.react(Sleep)
click(".mm-tbody table tbody tr:nth-child(1) td:nth-child(3) a")
click(ok, condition=None)
flow.react(Sleep)
logger.info('Done')
except Skip:
@@ -507,7 +496,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
except Cancel:
continue
except Exception as e:
logger.error('Error while uploading excel file', exc_info=e)
logger.error('Error while uploading document', exc_info=e)
status = Status.STANDBY
continue
@@ -528,7 +517,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
def get(self, key: str):
return self.query[key]
def set(self, key: str, value):
self.query[key] = value
@@ -542,6 +531,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
while index < len(data):
try:
attempts += 1
if len(driver.window_handles) > 3: driver.close()
driver.switch_to.window(driver.window_handles[2])
item = data[index]
kind = item['kind']
@@ -551,21 +541,16 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
opportunity = None
if kind != 'vat':
logger.info('[%d/%d] Skipping %s', index+1, len(data), number)
index += 1
attempts = 0
continue
logger.info('[%d/%d] Undesired invoice type; skipping %s', index+1, len(data), number)
raise Skip()
if attempts > parameters['attempts']:
logger.warning('Exhausted all allowed attempts; skipping %s', number)
index += 1
attempts = 0
continue
raise Skip()
progress['number'] = number
progress['index'] = index
flow.allow(Cancel)
flow.allow(Skip)
flow.allow(Skip, Cancel)
flow.react(Wait)
try:
@@ -597,17 +582,11 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
flow.react(Wait)
click(".sticky .okki-space-item:nth-child(1) button")
break
except Skip:
index += 1
attempts = 0
continue
except Cancel:
break
except NoSuchElementException:
logger.warning("Could not find invoice '%s'; skipping", number)
index += 1
attempts = 0
continue
raise Skip()
except Action as e:
raise e
except Exception as e:
logger.error("Error while looking up invoice '%s'", number, exc_info=e)
status = Status.STANDBY
@@ -638,16 +617,12 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
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
link = locate(".virtual-list-wrap .vue-recycle-scroller .row-item > .cell:nth-child(6) a", wait=False)
opportunity = unicodedata.normalize('NFKD', link.get_attribute('title'))
opportunity = link.text
break
except Skip:
index += 1
attempts = 0
continue
except Cancel:
break
except NoSuchElementException:
logger.warning("Could not find opportunity '%s'; skipping", match)
logger.warning("Could not find opportunity '%s'", match)
except Action as e:
raise e
except Exception as e:
logger.error("Error while looking up opportunity '%s'", match, exc_info=e)
status = Status.STANDBY
@@ -659,24 +634,29 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
if opportunity is not None:
try:
flow.react(Wait)
label = locate("label.paas-form-item-label")
dropdown = locate("#rc_select_1")
dropdown.clear()
dropdown.send_keys(opportunity)
menu = locate(".okki-select-dropdown")
menuitems = menu.find_elements(By.CSS_SELECTOR, ".rc-virtual-list-holder-inner > div")
for iteration in count(1):
dropdown.click()
dropdown.clear()
dropdown.send_keys(opportunity)
menu = locate(".okki-select-dropdown")
menuitems = menu.find_elements(By.CSS_SELECTOR, ".rc-virtual-list-holder-inner > div")
for menuitem in menuitems:
if menuitem.get_attribute('label').strip().startswith(opportunity):
click(menuitem)
except Skip:
index += 1
attempts = 0
continue
except Cancel:
break
try:
for menuitem in menuitems:
if menuitem.get_attribute('label') == opportunity:
click(menuitem)
raise StopIteration()
except StopIteration: break
except: pass
assert iteration < parameters['attempts'], "Exceeded maximum retry attempts"
click(label, condition=None)
except Action as e:
raise e
except Exception as e:
logger.warning('Could not select opportunity; skipping', exc_info=e)
logger.warning("Could not select opportunity '%s'", opportunity, exc_info=e)
try:
pagination = 10
@@ -730,12 +710,8 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
raise Exception('Product list imcomplete; expected %d, got %d' % (len(positions), len(ids)))
flow.react(Wait)
click(button)
except Skip:
index += 1
attempts = 0
continue
except Cancel:
break
except Action as e:
raise e
except Exception as e:
logger.error('Error while modifying invoice', exc_info=e)
status = Status.STANDBY
@@ -745,30 +721,24 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
click(".ow-box button.okki-btn-round", wait=False)
flow.react(Sleep)
except Exception as e:
logger.warning('Unable to unset additional fees; skipping', exc_info=e)
try:
flow.react(Wait)
flow.allow(Cancel, False)
flow.allow(Skip, False)
click(".sticky.bottom-0 button.okki-btn-primary", condition=None)
flow.react(Sleep)
except Skip:
pass
except Cancel:
break
except Exception as e:
logger.error('Error while saving document', exc_info=e)
status = Status.STANDBY
continue
finally:
driver.close()
logger.warning('Unable to unset additional fees', exc_info=e)
index += 1
attempts = 0
flow.react(Wait)
flow.deter(Skip, Cancel)
click(".sticky.bottom-0 button.okki-btn-primary", condition=None)
flow.react(Sleep)
driver.close()
except Skip:
pass
except Cancel:
break
except Exception as e:
logger.error('Unexpected error', exc_info=e)
status = Status.STANDBY
continue
index += 1
attempts = 0
if __name__ == '__main__':
try:
BIN
View File
Binary file not shown.