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"> <script type="module">
import { default as $, Rpc2, LogRecord } from '/'; 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 { profiles, parameters } = await Rpc2.invoke('context');
let status = null;
let date = new Date(); let date = new Date();
let day = date.getDay() || 7; let day = date.getDay() || 7;
$('#datefrom').valueAsNumber = date.setHours(-24 * (day - 1)) - date.getTimezoneOffset() * 60 * 1000; $('#datefrom').valueAsNumber = date.setHours(-24 * (day - 1)) - date.getTimezoneOffset() * 60 * 1000;
@@ -160,7 +158,7 @@ $.set('#begin', 'click', async () => {
parameters[element.id] = element.valueAsNumber; parameters[element.id] = element.valueAsNumber;
} }
await Rpc2.invoke('begin', options, parameters); await Rpc2.invoke('begin', { params: [options, parameters] });
break; break;
case 'RUNNING': case 'RUNNING':
await Rpc2.invoke('pause'); await Rpc2.invoke('pause');
@@ -180,12 +178,12 @@ $.set('#begin', 'click', () => {
$.set('#cancel', 'click', async () => { $.set('#cancel', 'click', async () => {
$('#cancel').disabled = true; $('#cancel').disabled = true;
await Rpc2.invoke('cancel'); await Rpc2.notify('cancel');
}); });
$.set('#skip', 'click', async () => { $.set('#skip', 'click', async () => {
$('#skip').disabled = true; $('#skip').disabled = true;
await Rpcs.invoke('skip'); await Rpc2.notify('skip');
}); });
$.set('#logs', 'change', (e) => { $.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))) { 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); let logs = Array.from(history);
for (let record of logs) { for (let record of logs) {
if ($('#messages').childNodes.length >= 500) $('#messages').childNodes.item(0)?.remove(); 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 message = LogRecord.format(record);
let node = document.createTextNode(new String(message).concat('\n')); let node = document.createTextNode(new String(message).concat('\n'));
$('#messages').appendChild(node); $('#messages').appendChild(node);
@@ -247,22 +245,17 @@ while (await new Promise(o => setTimeout(o, 1000, true))) {
$('#begin > span.text').innerText = 'Pause'; $('#begin > span.text').innerText = 'Pause';
$('#begin').classList.add('pulse'); $('#begin').classList.add('pulse');
let progress = await Rpc2.invoke('progress').catch(() => new Object()); let { task, number, index, limit } = await Rpc2.invoke('progress').catch(() => new Object());
let { task, number, index, limit } = progress;
$('#numberLabel').innerText = number ?? ''; $('#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(() => []); let [t1, t2] = await Rpc2.invoke('uptime').catch(() => []);
$('#uptimeLabel').innerText = Temporal.Duration.from({ seconds: t1 ?? 0 }).round({ largestUnit: 'hours' }).toLocaleString('en', { style: 'digital' }); $('#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; 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; break;
case 'STANDBY': case 'STANDBY':
if (error !== null) {
alert(`(${error.levelname}) ${error.msg}\n${error.exc_text ?? ''}`);
error = null;
}
$('#begin > span.text').innerText = 'Resume'; $('#begin > span.text').innerText = 'Resume';
$('#begin').classList.remove('pulse'); $('#begin').classList.remove('pulse');
break; break;
+76 -106
View File
@@ -1,4 +1,3 @@
import unicodedata
import argparse import argparse
import openpyxl import openpyxl
import logging import logging
@@ -20,7 +19,7 @@ from enum import Enum
from wakepy import keep from wakepy import keep
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
from itertools import repeat, count from itertools import count
from urllib3 import PoolManager from urllib3 import PoolManager
parser = argparse.ArgumentParser(description="Order Import") parser = argparse.ArgumentParser(description="Order Import")
@@ -45,8 +44,8 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
class Status(Enum): class Status(Enum):
IDLE = 0 IDLE = 0
READY = 1 READY = 1
RUNNING = 3 RUNNING = 2
STANDBY = 4 STANDBY = 3
class Profile: class Profile:
def __init__(self, name, subdomain, token, remise, person=None, prefix=None, suffix=None): 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() t1.start()
t2.start() t2.start()
sp.set('begin', begin) sp.add(begin, pause, resume)
sp.set('pause', pause)
sp.set('resume', resume)
sp.set('status', lambda: status.name) sp.set('status', lambda: status.name)
sp.set('uptime', lambda: [t1.delta()]) sp.set('uptime', lambda: [t1.delta()])
@@ -120,7 +117,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
setup(driver, parameters) 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...')
if (account := parameters['account']) and (password := parameters['password']): if (account := parameters['account']) and (password := parameters['password']):
try: try:
@@ -186,8 +183,8 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
p = ProductInfo(file) p = ProductInfo(file)
driver.close() driver.close()
driver.switch_to.window(driver.window_handles[0]) driver.switch_to.window(driver.window_handles[0])
status = Status.READY
logger.info('Done') logger.info('Done')
status = Status.READY
except Exception as e: except Exception as e:
logger.critical('Unable to load products', exc_info=e) logger.critical('Unable to load products', exc_info=e)
return 4 return 4
@@ -200,7 +197,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
return result return result
except Exception as e: except Exception as e:
logger.warning('Error while fetching data from %s, retrying... (%d)', url, attempt, exc_info=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): class Wait(Action):
@classmethod @classmethod
@@ -247,18 +244,13 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
raise cls raise cls
flow = ActionFlow() flow = ActionFlow()
flow.append(Wait) flow.stage(Wait, Sleep, Cancel, Skip)
flow.append(Sleep)
flow.append(Cancel)
flow.append(Skip)
profile = None profile = None
progress = { 'task': '' } progress = { 'task': '' }
selection = 0 selection = 0
sp.add(pairs=[ (k.lower(), v) for k, v in flow ])
sp.set('actions', lambda: flow.capabilities()) 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) sp.set('progress', lambda: progress)
while not flow.react(Wait): 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('Profile selected: %s', profile.name)
logger.info('Date from %s to %s', df, dt) logger.info('Date from %s to %s', df, dt)
flow.allow(Cancel) flow.allow(Cancel)
flow.allow(Skip, False) flow.deter(Skip)
for page in count(1): for page in count(1):
try: 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}')): if 'error' in (category := fetch(f'{base}/categories/{o}.json?api_token={profile.token}')):
error = category['error'] error = category['error']
code = category['code'] 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 continue
if (client := clients.get(o := item['client_id'])) is None: if (client := clients.get(o := item['client_id'])) is None:
if 'error' in (client := fetch(f'{base}/clients/{o}.json?api_token={profile.token}')): if 'error' in (client := fetch(f'{base}/clients/{o}.json?api_token={profile.token}')):
error = client['error'] error = client['error']
code = client['code'] 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 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'] date: str = item['issue_date']
kind: str = item['kind'] kind: str = item['kind']
total = float(item['price_net']) total = float(item['price_net'])
@@ -453,7 +445,7 @@ 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['directory']).joinpath(filename) 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) flow.react(Wait)
workbook.save(file) workbook.save(file)
except Skip: except Skip:
@@ -461,12 +453,12 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
except Cancel: except Cancel:
continue continue
except Exception as e: 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 status = Status.STANDBY
continue continue
try: try:
logger.info('Uploading...') logger.info('Uploading data...')
progress.clear() progress.clear()
progress['task'] = 'Task 3 of 4' progress['task'] = 'Task 3 of 4'
t2.clear() 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 .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(".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.react(Wait)
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")
click(".product-import-img-footer button.mm-button__primary")
while True: while not flow.react(Wait, Sleep):
try: try:
innerText = driver.find_element(By.CSS_SELECTOR, ".img-box-result-title").get_attribute('innerText') err = locate(".mm-tbody table tbody tr:nth-child(1) td:nth-child(5) .okki-space-item:nth-child(1) button", wait=False)
if innerText == '导入完成': break ok = locate(".mm-tbody table tbody tr:nth-child(1) td:nth-child(3) a", wait=False)
except: break
flow.react(Sleep) except: pass
try: click(".product-import-img-footer button.mm-button__primary", wait=False)
click(".mm-notification-container .mm-icon-close") except: pass
click(".product-import-img-footer button.mm-button__primary") try: click(".list-header-wrap button", wait=False)
err = locate(".mm-tbody table tbody tr:nth-child(1) td:nth-child(5) .okki-space-item:nth-child(1) button") except: pass
if err.get_attribute('disabled') is None: if err.get_attribute('disabled') is None:
logger.warning('Incomplete import detected; downloaded 1 related document') click(err, condition=None)
err.click() logger.warning('Incomplete import detected; downloading 1 related document')
flow.react(Sleep) flow.react(Sleep)
click(".mm-tbody table tbody tr:nth-child(1) td:nth-child(3) a") click(ok, condition=None)
flow.react(Sleep) flow.react(Sleep)
logger.info('Done') logger.info('Done')
except Skip: except Skip:
@@ -507,7 +496,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
except Cancel: except Cancel:
continue continue
except Exception as e: 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 status = Status.STANDBY
continue continue
@@ -542,6 +531,7 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
while index < len(data): while index < len(data):
try: try:
attempts += 1 attempts += 1
if len(driver.window_handles) > 3: driver.close()
driver.switch_to.window(driver.window_handles[2]) driver.switch_to.window(driver.window_handles[2])
item = data[index] item = data[index]
kind = item['kind'] kind = item['kind']
@@ -551,21 +541,16 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
opportunity = None opportunity = None
if kind != 'vat': if kind != 'vat':
logger.info('[%d/%d] Skipping %s', index+1, len(data), number) logger.info('[%d/%d] Undesired invoice type; skipping %s', index+1, len(data), number)
index += 1 raise Skip()
attempts = 0
continue
if attempts > parameters['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 raise Skip()
attempts = 0
continue
progress['number'] = number progress['number'] = number
progress['index'] = index progress['index'] = index
flow.allow(Cancel) flow.allow(Skip, Cancel)
flow.allow(Skip)
flow.react(Wait) flow.react(Wait)
try: try:
@@ -597,17 +582,11 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
flow.react(Wait) flow.react(Wait)
click(".sticky .okki-space-item:nth-child(1) button") click(".sticky .okki-space-item:nth-child(1) button")
break break
except Skip:
index += 1
attempts = 0
continue
except Cancel:
break
except NoSuchElementException: except NoSuchElementException:
logger.warning("Could not find invoice '%s'; skipping", number) logger.warning("Could not find invoice '%s'; skipping", number)
index += 1 raise Skip()
attempts = 0 except Action as e:
continue raise e
except Exception as e: except Exception as e:
logger.error("Error while looking up invoice '%s'", number, exc_info=e) logger.error("Error while looking up invoice '%s'", number, exc_info=e)
status = Status.STANDBY 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) 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)
opportunity = unicodedata.normalize('NFKD', link.get_attribute('title')) opportunity = link.text
break break
except Skip:
index += 1
attempts = 0
continue
except Cancel:
break
except NoSuchElementException: 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: except Exception as e:
logger.error("Error while looking up opportunity '%s'", match, exc_info=e) logger.error("Error while looking up opportunity '%s'", match, exc_info=e)
status = Status.STANDBY status = Status.STANDBY
@@ -659,24 +634,29 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
if opportunity is not None: if opportunity is not None:
try: try:
flow.react(Wait) flow.react(Wait)
label = locate("label.paas-form-item-label")
dropdown = locate("#rc_select_1") dropdown = locate("#rc_select_1")
dropdown.clear()
dropdown.send_keys(opportunity)
menu = locate(".okki-select-dropdown") for iteration in count(1):
menuitems = menu.find_elements(By.CSS_SELECTOR, ".rc-virtual-list-holder-inner > div") 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: try:
if menuitem.get_attribute('label').strip().startswith(opportunity): for menuitem in menuitems:
click(menuitem) if menuitem.get_attribute('label') == opportunity:
except Skip: click(menuitem)
index += 1 raise StopIteration()
attempts = 0 except StopIteration: break
continue except: pass
except Cancel: assert iteration < parameters['attempts'], "Exceeded maximum retry attempts"
break click(label, condition=None)
except Action as e:
raise e
except Exception as 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: try:
pagination = 10 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))) raise Exception('Product list imcomplete; expected %d, got %d' % (len(positions), len(ids)))
flow.react(Wait) flow.react(Wait)
click(button) click(button)
except Skip: except Action as e:
index += 1 raise e
attempts = 0
continue
except Cancel:
break
except Exception as e: except Exception as e:
logger.error('Error while modifying invoice', exc_info=e) logger.error('Error while modifying invoice', exc_info=e)
status = Status.STANDBY status = Status.STANDBY
@@ -745,30 +721,24 @@ def main(driver: WebDriver, logger = logging.getLogger('main')):
click(".ow-box button.okki-btn-round", wait=False) click(".ow-box button.okki-btn-round", wait=False)
flow.react(Sleep) flow.react(Sleep)
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', exc_info=e)
try: flow.react(Wait)
flow.react(Wait) flow.deter(Skip, Cancel)
flow.allow(Cancel, False) click(".sticky.bottom-0 button.okki-btn-primary", condition=None)
flow.allow(Skip, False) flow.react(Sleep)
click(".sticky.bottom-0 button.okki-btn-primary", condition=None) driver.close()
flow.react(Sleep) except Skip:
except Skip: pass
pass except Cancel:
except Cancel: break
break
except Exception as e:
logger.error('Error while saving document', exc_info=e)
status = Status.STANDBY
continue
finally:
driver.close()
index += 1
attempts = 0
except Exception as e: except Exception as e:
logger.error('Unexpected error', exc_info=e) logger.error('Unexpected error', exc_info=e)
status = Status.STANDBY status = Status.STANDBY
continue
index += 1
attempts = 0
if __name__ == '__main__': if __name__ == '__main__':
try: try:
BIN
View File
Binary file not shown.