odoo-driver 0.9.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo_driver/__init__.py +5 -0
- odoo_driver/app.py +29 -0
- odoo_driver/cli.py +121 -0
- odoo_driver/default_cert/server.crt +22 -0
- odoo_driver/default_cert/server.key +28 -0
- odoo_driver/devices/__init__.py +4 -0
- odoo_driver/devices/device_display.py +147 -0
- odoo_driver/devices/device_payment.py +20 -0
- odoo_driver/devices/device_printer.py +25 -0
- odoo_driver/devices/device_scale.py +34 -0
- odoo_driver/devices/models/__init__.py +0 -0
- odoo_driver/devices/models/device_abstract.py +115 -0
- odoo_driver/devices/models/device_abstract_with_queue.py +84 -0
- odoo_driver/devices/models/device_task.py +10 -0
- odoo_driver/devices/scale_dialog06/__init__.py +0 -0
- odoo_driver/devices/scale_dialog06/scale_answer.py +101 -0
- odoo_driver/devices/scale_dialog06/scale_command.py +169 -0
- odoo_driver/devices/scale_dialog06/state_machine.py +93 -0
- odoo_driver/devices/tools/__init__.py +0 -0
- odoo_driver/devices/tools/serial_code.py +12 -0
- odoo_driver/flask_app/__init__.py +8 -0
- odoo_driver/flask_app/flask_app_display.py +20 -0
- odoo_driver/flask_app/flask_app_odoo.py +67 -0
- odoo_driver/flask_app/flask_app_payment.py +23 -0
- odoo_driver/flask_app/flask_app_printer.py +30 -0
- odoo_driver/flask_app/flask_app_scale.py +26 -0
- odoo_driver/flask_app/flask_app_website.py +115 -0
- odoo_driver/interface.py +162 -0
- odoo_driver/static/audio/photo-taken.wav +0 -0
- odoo_driver/static/css/bootstrap.min.css +6 -0
- odoo_driver/static/css/bootstrap.min.css.map +1 -0
- odoo_driver/static/css/odoo_pos_driver.css +3 -0
- odoo_driver/static/devices/display.png +0 -0
- odoo_driver/static/devices/display__aures__ocd_300.png +0 -0
- odoo_driver/static/devices/payment.png +0 -0
- odoo_driver/static/devices/payment__ingenico__move_5000.png +0 -0
- odoo_driver/static/devices/printer.png +0 -0
- odoo_driver/static/devices/printer__epson__tm_t20.png +0 -0
- odoo_driver/static/devices/scale.png +0 -0
- odoo_driver/static/devices/scale__mettler_toledo__ariva_s.png +0 -0
- odoo_driver/static/img/favicon.png +0 -0
- odoo_driver/static/img/odoo.png +0 -0
- odoo_driver/static/js/bootstrap.min.js +7 -0
- odoo_driver/static/js/bootstrap.min.js.map +1 -0
- odoo_driver/static/js/jquery.min.js +2 -0
- odoo_driver/static/js/webcam-easy.js +186 -0
- odoo_driver/static/readme/home_page.png +0 -0
- odoo_driver/static/readme/security_exception.png +0 -0
- odoo_driver/templates/application.html.jinja +24 -0
- odoo_driver/templates/disconnections.html.jinja +25 -0
- odoo_driver/templates/errored_tasks.html.jinja +27 -0
- odoo_driver/templates/home.html.jinja +429 -0
- odoo_driver/templates/layout.html.jinja +52 -0
- odoo_driver/templates/photo_booth.html.jinja +67 -0
- odoo_driver/templates/system.html.jinja +24 -0
- odoo_driver/templates/usb_devices.html.jinja +40 -0
- odoo_driver/translations/babel.cfg +2 -0
- odoo_driver/translations/fr/LC_MESSAGES/messages.mo +0 -0
- odoo_driver/translations/fr/LC_MESSAGES/messages.po +299 -0
- odoo_driver/translations/odoo-pos-driver.pot +294 -0
- odoo_driver/usb_device/__init__.py +0 -0
- odoo_driver/usb_device/usb_device_list.py +63 -0
- odoo_driver/usb_device/usb_device_tools.py +41 -0
- odoo_driver-0.9.6.dist-info/METADATA +306 -0
- odoo_driver-0.9.6.dist-info/RECORD +67 -0
- odoo_driver-0.9.6.dist-info/WHEEL +4 -0
- odoo_driver-0.9.6.dist-info/entry_points.txt +3 -0
odoo_driver/__init__.py
ADDED
odoo_driver/app.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from flask import Flask, request
|
|
2
|
+
from flask_babel import Babel
|
|
3
|
+
from flask_cors import CORS
|
|
4
|
+
|
|
5
|
+
# Initialize Flask Application
|
|
6
|
+
app = Flask(__name__)
|
|
7
|
+
|
|
8
|
+
# Include extra route files
|
|
9
|
+
from . import flask_app # noqa: F401,E402
|
|
10
|
+
|
|
11
|
+
# Handle CORS call
|
|
12
|
+
CORS(app, resources={r"/*": {"origins": "*"}})
|
|
13
|
+
|
|
14
|
+
# Handle translation via babel
|
|
15
|
+
app.config["BABEL_DEFAULT_LOCALE"] = "en"
|
|
16
|
+
babel = Babel(app)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_locale():
|
|
20
|
+
# Use the browser's language preferences to select an available translation
|
|
21
|
+
available_translations = [
|
|
22
|
+
str(translation) for translation in babel.list_translations()
|
|
23
|
+
]
|
|
24
|
+
for lang in request.accept_languages.values():
|
|
25
|
+
if lang[:2] in available_translations:
|
|
26
|
+
return lang[:2]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
babel.init_app(app, locale_selector=get_locale)
|
odoo_driver/cli.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import importlib_resources
|
|
8
|
+
import simplejson
|
|
9
|
+
from click_loglevel import LogLevel
|
|
10
|
+
from gevent import pywsgi
|
|
11
|
+
from geventwebsocket.handler import WebSocketHandler
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
import odoo_driver
|
|
15
|
+
|
|
16
|
+
from .app import app
|
|
17
|
+
from .interface import interface
|
|
18
|
+
|
|
19
|
+
# Define loguru specific level for logging
|
|
20
|
+
# to make click_loglevel working
|
|
21
|
+
logging.addLevelName(5, "TRACE")
|
|
22
|
+
logging.addLevelName(25, "SUCCESS")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@click.command()
|
|
26
|
+
@click.option(
|
|
27
|
+
"-a",
|
|
28
|
+
"--address",
|
|
29
|
+
type=click.STRING,
|
|
30
|
+
default="0.0.0.0",
|
|
31
|
+
show_default=True,
|
|
32
|
+
help="Address on which the web service will be exposed",
|
|
33
|
+
)
|
|
34
|
+
@click.option(
|
|
35
|
+
"-p",
|
|
36
|
+
"--port",
|
|
37
|
+
type=click.INT,
|
|
38
|
+
default=8069,
|
|
39
|
+
show_default=True,
|
|
40
|
+
help="Port on which the web service will be exposed",
|
|
41
|
+
)
|
|
42
|
+
@click.option(
|
|
43
|
+
"--secure/--unsecure",
|
|
44
|
+
default=True,
|
|
45
|
+
show_default=True,
|
|
46
|
+
help="Option 'secure' exposes web service on https."
|
|
47
|
+
" Option 'unsecure' exposes web service on http.",
|
|
48
|
+
)
|
|
49
|
+
@click.option(
|
|
50
|
+
"-r",
|
|
51
|
+
"--refresh-devices-delay",
|
|
52
|
+
type=click.INT,
|
|
53
|
+
default=1,
|
|
54
|
+
show_default=True,
|
|
55
|
+
help="Interval in seconds between two device refreshes",
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"--options",
|
|
59
|
+
type=click.STRING,
|
|
60
|
+
default="{}",
|
|
61
|
+
show_default=True,
|
|
62
|
+
help="""Extra JSON options to pass to devices. Exemple:"
|
|
63
|
+
" --options '{"scale": {"polynomial": "123456"}}'
|
|
64
|
+
""",
|
|
65
|
+
)
|
|
66
|
+
@click.option(
|
|
67
|
+
"-l",
|
|
68
|
+
"--log-level",
|
|
69
|
+
type=LogLevel(extra=["TRACE", "SUCCESS"]),
|
|
70
|
+
default="INFO",
|
|
71
|
+
show_default=True,
|
|
72
|
+
)
|
|
73
|
+
@click.option(
|
|
74
|
+
"-f",
|
|
75
|
+
"--log-folder",
|
|
76
|
+
type=click.Path(
|
|
77
|
+
exists=True, file_okay=False, writable=True, resolve_path=True, path_type=Path
|
|
78
|
+
),
|
|
79
|
+
show_default=True,
|
|
80
|
+
)
|
|
81
|
+
@click.version_option(version=odoo_driver.__version__)
|
|
82
|
+
def main(log_level, address, port, refresh_devices_delay, secure, log_folder, options):
|
|
83
|
+
logger.success("Lauching odoo-driver ...")
|
|
84
|
+
|
|
85
|
+
logger.info(f"Application Path: {os.path.abspath(__file__)}")
|
|
86
|
+
|
|
87
|
+
# Log Configuration
|
|
88
|
+
# remove first default sink that is defined to 'INFO'
|
|
89
|
+
logger.remove()
|
|
90
|
+
|
|
91
|
+
# Add stdout logger
|
|
92
|
+
logger.add(sys.stderr, level=log_level)
|
|
93
|
+
|
|
94
|
+
# Add file logger
|
|
95
|
+
if log_folder:
|
|
96
|
+
log_path = log_folder / "odoo-driver_{time}.log"
|
|
97
|
+
logger.info(f"Log Path: {log_path}")
|
|
98
|
+
logger.add(log_path, level=log_level, rotation="1 week", enqueue=True)
|
|
99
|
+
|
|
100
|
+
# Initialize Interface
|
|
101
|
+
interface.initialize(
|
|
102
|
+
refresh_devices_delay=refresh_devices_delay,
|
|
103
|
+
options=simplejson.loads(options),
|
|
104
|
+
log_folder=log_folder,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
kwargs = {"handler_class": WebSocketHandler}
|
|
108
|
+
|
|
109
|
+
# Handle HTTPS, if required
|
|
110
|
+
if secure:
|
|
111
|
+
cert_folder = importlib_resources.files("odoo_driver") / "default_cert"
|
|
112
|
+
server_key_path = cert_folder / "server.key"
|
|
113
|
+
server_cert_path = cert_folder / "server.crt"
|
|
114
|
+
kwargs.update({"keyfile": server_key_path, "certfile": server_cert_path})
|
|
115
|
+
http_server = pywsgi.WSGIServer((address, port), app, **kwargs)
|
|
116
|
+
prefix = secure and "https" or "http"
|
|
117
|
+
logger.success(
|
|
118
|
+
f"Serving odoo-driver (version: {odoo_driver.__version__})"
|
|
119
|
+
f" on {prefix}://{address}:{port} ..."
|
|
120
|
+
)
|
|
121
|
+
http_server.serve_forever()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIDmTCCAoGgAwIBAgIUSJxXt4mpzmWmDHxjeN9/0l4TBJAwDQYJKoZIhvcNAQEL
|
|
3
|
+
BQAwXDELMAkGA1UEBhMCRlIxHTAbBgNVBAgMFEF1dmVyZ25lIFJob25lLUFscGVz
|
|
4
|
+
MQ0wCwYDVQQHDARMeW9uMQ0wCwYDVQQKDARHUkFQMRAwDgYDVQQLDAdJVCBUZWFt
|
|
5
|
+
MB4XDTI0MDgxMjE0Mzc0OVoXDTI1MDgxMjE0Mzc0OVowXDELMAkGA1UEBhMCRlIx
|
|
6
|
+
HTAbBgNVBAgMFEF1dmVyZ25lIFJob25lLUFscGVzMQ0wCwYDVQQHDARMeW9uMQ0w
|
|
7
|
+
CwYDVQQKDARHUkFQMRAwDgYDVQQLDAdJVCBUZWFtMIIBIjANBgkqhkiG9w0BAQEF
|
|
8
|
+
AAOCAQ8AMIIBCgKCAQEA1vrN9ArLWLgsySqqGuGLiU7sWG3MWpC1YHtiCf9LEOlq
|
|
9
|
+
gsmWQpkeUGb0qni6VsoT9M3sSNFS7TX0kmjEu5pE84IHmuWIvQoferl/PQWIqr7p
|
|
10
|
+
A3YTH9RxHQuII59LxfKzehfuZ+85CPYBOlSuLdovkd1+nm9ZcbyLotSeZ/G1JuTC
|
|
11
|
+
Gjg6JhbvHHh6oC8o1rqY2V+T5HgXTBqjHAogD14CMJVkQ7HbeNEQ3JfhyWgfBT6U
|
|
12
|
+
QYJkXcfZ4vI7TCZ8PiHMheoA4O3kIPzxogBwAxnNSs/T6LjkvXfNHVMLsfFrVY+h
|
|
13
|
+
wBbQz2QNbkSKLVrc+JRFBRqZWkvILqBcDsqKlvD+bwIDAQABo1MwUTAdBgNVHQ4E
|
|
14
|
+
FgQUtjczAx17jhbwMAPDpbGa1dQ8Im8wHwYDVR0jBBgwFoAUtjczAx17jhbwMAPD
|
|
15
|
+
pbGa1dQ8Im8wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAQz3v
|
|
16
|
+
7Y1gOkYTcogihzczC5yXUCDOUDnQluUweTnFiNy4bSlD2DuOMvC8eC2eCZesRUwp
|
|
17
|
+
RPcwFQ9RwhNtVI0j+K1Ta5+R43J1ynjFWNed1UFo3oYv0WqeHon+W82mBQZ+x2Hn
|
|
18
|
+
fD98vRBxh0Kch9L00T2mpL2059uSM/CF2C/gXrFGLin9WXYdwjlvtuX2uTr20XzO
|
|
19
|
+
Xl9Tgumvqxvq3SqA68TBYtvpVRXAIgAkCOJLfH7GBKbqUg0dasrGB6MrOePUCyZn
|
|
20
|
+
xDTosZI9fyf0hNB6SdsbSYCd1c+zfh6/WwjMiUqQpNGecBB2i14nHFgQKg6qy8Fk
|
|
21
|
+
mnOKX0WobzE+IsJmbw==
|
|
22
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
-----BEGIN PRIVATE KEY-----
|
|
2
|
+
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDW+s30CstYuCzJ
|
|
3
|
+
Kqoa4YuJTuxYbcxakLVge2IJ/0sQ6WqCyZZCmR5QZvSqeLpWyhP0zexI0VLtNfSS
|
|
4
|
+
aMS7mkTzggea5Yi9Ch96uX89BYiqvukDdhMf1HEdC4gjn0vF8rN6F+5n7zkI9gE6
|
|
5
|
+
VK4t2i+R3X6eb1lxvIui1J5n8bUm5MIaODomFu8ceHqgLyjWupjZX5PkeBdMGqMc
|
|
6
|
+
CiAPXgIwlWRDsdt40RDcl+HJaB8FPpRBgmRdx9ni8jtMJnw+IcyF6gDg7eQg/PGi
|
|
7
|
+
AHADGc1Kz9PouOS9d80dUwux8WtVj6HAFtDPZA1uRIotWtz4lEUFGplaS8guoFwO
|
|
8
|
+
yoqW8P5vAgMBAAECggEAbUQgte9el4NU0BJtTyhgIbQJkJCOE2IFrjpIBEjLwJbC
|
|
9
|
+
2iLXypXEwJzzskmWfFJNflnDxOcPvR11Z4z0oV2xugrXDrWnoF2qzlfGAh/z7MUH
|
|
10
|
+
7ghvg/gYXejkkyZ9/uhyrJXJSO5gENvol9s+hHOnRf5MQv7ZeWOZnXNv5+kEZ/Vi
|
|
11
|
+
lAszy/oxj6kJOcjbeE3E2vNS1Yn9RC1GrGLl/UYTPli6lFZvKylbFQ3i4HePXej7
|
|
12
|
+
smreMu1BVHn6sr871dFqAyG9tW0hrYIUha+FYdgqU/lDhZLIzuxlKhwdxuuoBVDJ
|
|
13
|
+
dmPBhzeENoHXjHJpNQ3PMmlaajfUDQWWC4Nr17aWEQKBgQD35yBKpitbvX81LZfq
|
|
14
|
+
uU+djjDTiekdFfDjLO7wvhwihH1wJAaPFSmxzyinYie8Yhb0WHLhwfvmek06jlyM
|
|
15
|
+
zcQxt09cIsy+hi1vxWdMt/JI5suJle98tN3rPvp9ZmstY0Ua/q2Q8/PL1y5QMJR/
|
|
16
|
+
tH5b9xMmgPOrXX/5L47/thmkqwKBgQDeAGMVnC/m+ULEdxvxsqx/jbS1jo0sPfuN
|
|
17
|
+
1+JfIm//VTAAcV9PJMx5dM+OpdKQACE7wlZZaqE7xr/odyFEei2OmTU8dV+LL2bN
|
|
18
|
+
KjRvkXLFZH8TaaAWO4+w2OkC1/DM2zahcgrjlDutXqkMLilS7PDhWjPNYtVxlIHM
|
|
19
|
+
QscZO8llTQKBgGqi6joSNq502iZm4WVfp0av5oLoXPVc/czLBFFchb+3XDoD1Spx
|
|
20
|
+
QQkygltWFp+rVhJvhaNlDVV8IIJiwJ702ENGhgWtFoOSNjNRJ/lRWxBO7xjU/TqY
|
|
21
|
+
lqld4uRUTSAuBojxKnoi5ik1+78U1pwEaSJoKr2yVJ7PToBDG/EVRPyXAoGAAVu9
|
|
22
|
+
P3eaL26dAvi7mgn+1jT2wxmhMYEble2ZO/3I86YAyi/nZmOSQLf/QR8wn6LwkR4K
|
|
23
|
+
SzfEzsnTiMeZ04Jv3ux/D+Sj9gUPOvgkI/6e1ihcQrElwp0QrwNUwaiyl35L4kcB
|
|
24
|
+
XA0fJFts618o75H1k1tq77nIFo7TEH3kG/pqrA0CgYAofu2NLjwqyTwWlmv46eyb
|
|
25
|
+
nLV1OXBIxGTRFeP1fqJN4b4iy/PsTt4cclRoO+oAFhVJp2/DyxO7hGToR2foA5w4
|
|
26
|
+
sU/I9zFuaddmG9y3hey8i1gBRdeGRT0F/fEyEv6hZMXr7w4OVTVMsi3VW0m7NalK
|
|
27
|
+
wqDKXF//CF6whZKdmOFmcQ==
|
|
28
|
+
-----END PRIVATE KEY-----
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from flask_babel import gettext as _
|
|
2
|
+
from loguru import logger
|
|
3
|
+
|
|
4
|
+
from .models.device_abstract_with_queue import DeviceAbstractWithQueue
|
|
5
|
+
from .tools.serial_code import FF, US
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DeviceDisplay(DeviceAbstractWithQueue):
|
|
9
|
+
use_serial = True
|
|
10
|
+
device_type = "display"
|
|
11
|
+
|
|
12
|
+
@logger.catch
|
|
13
|
+
def _process_task_serial(self, serial, task):
|
|
14
|
+
lines_ascii = self._convert_task_to_ascii_lines(task)
|
|
15
|
+
|
|
16
|
+
self._clear_screen(serial)
|
|
17
|
+
|
|
18
|
+
for row, text in enumerate(lines_ascii, 1):
|
|
19
|
+
self._position_cursor(serial, 1, row)
|
|
20
|
+
self._send_message(serial, [text])
|
|
21
|
+
|
|
22
|
+
def _convert_task_to_ascii_lines(self, task):
|
|
23
|
+
lines_ascii = []
|
|
24
|
+
|
|
25
|
+
action = task.data["action"]
|
|
26
|
+
|
|
27
|
+
if action == "display_lines":
|
|
28
|
+
for line in task.data["text_lines"]:
|
|
29
|
+
lines_ascii.append(line)
|
|
30
|
+
|
|
31
|
+
elif action in [
|
|
32
|
+
"add_line",
|
|
33
|
+
"update_quantity",
|
|
34
|
+
"update_unit_price",
|
|
35
|
+
"update_discount",
|
|
36
|
+
]:
|
|
37
|
+
# First Line
|
|
38
|
+
left_part = task.data.get("product_name")
|
|
39
|
+
right_part = self._prepare_percent(task.data.get("discount"))
|
|
40
|
+
line_1 = self._prepare_line(left_part, right_part)
|
|
41
|
+
|
|
42
|
+
# Second Line Line
|
|
43
|
+
left_part = (
|
|
44
|
+
self._prepare_qty(task.data.get("quantity"))
|
|
45
|
+
+ "×"
|
|
46
|
+
+ self._prepare_price(task.data.get("unit_price"))
|
|
47
|
+
)
|
|
48
|
+
right_part = self._prepare_price(task.data.get("total"))
|
|
49
|
+
line_2 = self._prepare_line(left_part, right_part)
|
|
50
|
+
|
|
51
|
+
lines_ascii = [line_1, line_2]
|
|
52
|
+
|
|
53
|
+
elif action == "remove_line":
|
|
54
|
+
lines_ascii = [
|
|
55
|
+
_("Deleting Line ..."),
|
|
56
|
+
self._prepare_line(task.data.get("product_name")),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
elif action == "payment_balance":
|
|
60
|
+
total = task.data.get("total")
|
|
61
|
+
total_paid = task.data.get("total_paid")
|
|
62
|
+
total_due = task.data.get("total_due")
|
|
63
|
+
total_to_pay = total - total_paid
|
|
64
|
+
line_1 = self._prepare_line(_("Total:"), self._prepare_price(total))
|
|
65
|
+
line_2 = ""
|
|
66
|
+
if total_paid != 0.0:
|
|
67
|
+
if total_to_pay > 0:
|
|
68
|
+
line_2 = self._prepare_line(
|
|
69
|
+
_("To Pay:"), self._prepare_price(total_to_pay)
|
|
70
|
+
)
|
|
71
|
+
elif total_due < 0:
|
|
72
|
+
line_2 = self._prepare_line(
|
|
73
|
+
_("Change:"), self._prepare_price(-total_due)
|
|
74
|
+
)
|
|
75
|
+
lines_ascii = [line_1, line_2]
|
|
76
|
+
|
|
77
|
+
else:
|
|
78
|
+
raise NotImplementedError(
|
|
79
|
+
f"{action} is not a valid action for the Display device."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return lines_ascii
|
|
83
|
+
|
|
84
|
+
def _clear_screen(self, serial):
|
|
85
|
+
self._send_message(serial, [FF], "Clear Screen")
|
|
86
|
+
|
|
87
|
+
def _position_cursor(self, serial, col=1, row=1):
|
|
88
|
+
self._send_message(
|
|
89
|
+
serial, [US, chr(36), chr(col), chr(row)], f"Move to col {col} / row {row}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _send_message(self, serial, message_list, action_description=""):
|
|
93
|
+
message_list = [x for x in message_list if x]
|
|
94
|
+
if not message_list:
|
|
95
|
+
return
|
|
96
|
+
message = b"".join([x.encode("cp858", errors="ignore") for x in message_list])
|
|
97
|
+
if action_description:
|
|
98
|
+
logger.debug(
|
|
99
|
+
f"Display: {action_description} with the message {message} ..."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
serial.write(message)
|
|
103
|
+
|
|
104
|
+
def _display_float(self, float_repr, reduce_txt=True, suffix=""):
|
|
105
|
+
if reduce_txt:
|
|
106
|
+
while float_repr:
|
|
107
|
+
if float_repr[-1:] == "0":
|
|
108
|
+
float_repr = float_repr[:-1]
|
|
109
|
+
else:
|
|
110
|
+
break
|
|
111
|
+
if float_repr[-1:] == ".":
|
|
112
|
+
float_repr = float_repr[:-1]
|
|
113
|
+
return float_repr.replace(".", ",") + suffix
|
|
114
|
+
|
|
115
|
+
def _prepare_percent(self, percent):
|
|
116
|
+
try:
|
|
117
|
+
percent = float(percent)
|
|
118
|
+
except ValueError:
|
|
119
|
+
return ""
|
|
120
|
+
except TypeError:
|
|
121
|
+
return ""
|
|
122
|
+
if not percent:
|
|
123
|
+
return ""
|
|
124
|
+
return self._display_float("-{:.1f}".format(percent), suffix="%")
|
|
125
|
+
|
|
126
|
+
def _prepare_qty(self, quantity):
|
|
127
|
+
try:
|
|
128
|
+
quantity = float(quantity)
|
|
129
|
+
except ValueError:
|
|
130
|
+
quantity = 0.0
|
|
131
|
+
return self._display_float("{:.3f}".format(quantity))
|
|
132
|
+
|
|
133
|
+
def _prepare_price(self, price):
|
|
134
|
+
try:
|
|
135
|
+
price = float(price)
|
|
136
|
+
except ValueError:
|
|
137
|
+
price = 0.0
|
|
138
|
+
return self._display_float("{:.2f}".format(price), reduce_txt=False, suffix="€")
|
|
139
|
+
|
|
140
|
+
def _prepare_line(self, left_part, right_part=""):
|
|
141
|
+
result = left_part[:20].ljust(20, " ")
|
|
142
|
+
if right_part:
|
|
143
|
+
right_part = right_part[-20:]
|
|
144
|
+
result = result[0 : (20 - 1 - len(right_part))]
|
|
145
|
+
result += " " + right_part
|
|
146
|
+
|
|
147
|
+
return result
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from telium import Telium, TeliumAsk
|
|
2
|
+
|
|
3
|
+
from .models.device_abstract import DeviceAbstract
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class DevicePayment(DeviceAbstract):
|
|
7
|
+
device_type = "payment"
|
|
8
|
+
|
|
9
|
+
def push_amount(self, amount, currency_iso):
|
|
10
|
+
if not self.terminal_file:
|
|
11
|
+
return False
|
|
12
|
+
telium = Telium(self.terminal_file)
|
|
13
|
+
payment = TeliumAsk.new_payment(
|
|
14
|
+
amount,
|
|
15
|
+
payment_mode="debit",
|
|
16
|
+
target_currency=currency_iso,
|
|
17
|
+
)
|
|
18
|
+
result = telium.ask(payment)
|
|
19
|
+
telium.close()
|
|
20
|
+
return result
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
|
|
4
|
+
from escpos.printer import Usb
|
|
5
|
+
from PIL import Image
|
|
6
|
+
|
|
7
|
+
from .models.device_abstract_with_queue import DeviceAbstractWithQueue
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DevicePrinter(DeviceAbstractWithQueue):
|
|
11
|
+
device_type = "printer"
|
|
12
|
+
|
|
13
|
+
def _process_task(self, task):
|
|
14
|
+
printer = Usb(self._usb_device.idVendor, self._usb_device.idProduct, 0)
|
|
15
|
+
(action, value) = task.data
|
|
16
|
+
|
|
17
|
+
if action == "print":
|
|
18
|
+
im = Image.open(BytesIO(base64.b64decode(value)))
|
|
19
|
+
printer.image(im)
|
|
20
|
+
printer.cut()
|
|
21
|
+
|
|
22
|
+
elif action == "open_cashbox":
|
|
23
|
+
printer.cashdraw(2)
|
|
24
|
+
|
|
25
|
+
printer.close()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from .models.device_abstract import DeviceAbstract
|
|
2
|
+
from .scale_dialog06 import state_machine
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DeviceScale(DeviceAbstract):
|
|
6
|
+
device_type = "scale"
|
|
7
|
+
|
|
8
|
+
def __init__(self, interface, options):
|
|
9
|
+
DeviceAbstract.__init__(self, interface, options)
|
|
10
|
+
self.last_weight = 0.0
|
|
11
|
+
|
|
12
|
+
def read_weight(self, unit_price, tare):
|
|
13
|
+
with self._get_serial() as serial:
|
|
14
|
+
sm = state_machine.Dialog06Machine(
|
|
15
|
+
serial,
|
|
16
|
+
self.get_option("polynomial", int, required=True),
|
|
17
|
+
unit_price,
|
|
18
|
+
tare,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
while not sm.current_state.final:
|
|
22
|
+
sm.continue_communication()
|
|
23
|
+
|
|
24
|
+
# In a dialog06 context, if the scale return 21 (no change since the last weight)
|
|
25
|
+
# The scale doesn't provide the weight.
|
|
26
|
+
# in that 'valid use case', we return the last weight
|
|
27
|
+
if sm.last_scale_answer.error_number != "21":
|
|
28
|
+
self.last_weight = sm.last_scale_answer.weight
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
"weight": self.last_weight,
|
|
32
|
+
"error_number": sm.last_scale_answer.error_number,
|
|
33
|
+
"error_description": sm.last_scale_answer.error_description,
|
|
34
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import serial.tools.list_ports
|
|
2
|
+
from flask_babel import gettext as _
|
|
3
|
+
from loguru import logger
|
|
4
|
+
from serial import Serial
|
|
5
|
+
|
|
6
|
+
from ...usb_device import usb_device_tools
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DeviceAbstract:
|
|
10
|
+
has_queue = False
|
|
11
|
+
use_serial = False
|
|
12
|
+
device_type = False
|
|
13
|
+
|
|
14
|
+
def __init__(self, interface, options):
|
|
15
|
+
self._interface = interface
|
|
16
|
+
self._usb_device = False
|
|
17
|
+
self.usb_device_name = False
|
|
18
|
+
self.terminal_file = False
|
|
19
|
+
self._usb_image_name = False
|
|
20
|
+
self._options = options
|
|
21
|
+
|
|
22
|
+
def get_option(self, option_name, option_type, required=False):
|
|
23
|
+
option = self._options.get(option_name, False)
|
|
24
|
+
if required and not option:
|
|
25
|
+
raise Exception(
|
|
26
|
+
f"{option_name} is required for the device {self.device_type}."
|
|
27
|
+
)
|
|
28
|
+
return option_type(option)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def name(self):
|
|
32
|
+
if self.device_type == "display":
|
|
33
|
+
return _("Display")
|
|
34
|
+
elif self.device_type == "printer":
|
|
35
|
+
return _("Printer")
|
|
36
|
+
elif self.device_type == "payment":
|
|
37
|
+
return _("Payment Terminal")
|
|
38
|
+
elif self.device_type == "scale":
|
|
39
|
+
return _("Scale")
|
|
40
|
+
return "N/C"
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def usb_vendor_product_code(self):
|
|
44
|
+
return usb_device_tools.get_device_id_vendor_id_product(self._usb_device)
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def usb_serial_number(self):
|
|
48
|
+
return usb_device_tools.get_device_serial_number(self._usb_device)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def usb_manufacturer(self):
|
|
52
|
+
return usb_device_tools.get_device_manufacturer(self._usb_device)
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def usb_product_name(self):
|
|
56
|
+
return usb_device_tools.get_device_product(self._usb_device)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_connected(self):
|
|
60
|
+
return bool(self._usb_device)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def disconnections(self):
|
|
64
|
+
return self._interface.disconnections.get(self.usb_vendor_product_code, [])
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def disconnections_qty(self):
|
|
68
|
+
return len(self.disconnections)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def usb_image_name(self):
|
|
72
|
+
return self._usb_image_name or f"{self.device_type}.png"
|
|
73
|
+
|
|
74
|
+
# ###########################
|
|
75
|
+
# Device Section
|
|
76
|
+
# ###########################
|
|
77
|
+
def set_usb_device(self, usb_device, extra_info):
|
|
78
|
+
self._usb_device = usb_device
|
|
79
|
+
self.usb_device_name = extra_info.get("name", False)
|
|
80
|
+
self._usb_image_name = extra_info.get("image")
|
|
81
|
+
connected_comports = [x for x in serial.tools.list_ports.comports()]
|
|
82
|
+
for port in connected_comports:
|
|
83
|
+
if (port.vid, port.pid) == (
|
|
84
|
+
usb_device.idVendor,
|
|
85
|
+
usb_device.idProduct,
|
|
86
|
+
):
|
|
87
|
+
self.terminal_file = port.device
|
|
88
|
+
self._logger("DEBUG", f"Terminal File found: {self.terminal_file}.")
|
|
89
|
+
break
|
|
90
|
+
if not self.terminal_file:
|
|
91
|
+
self._logger("DEBUG", "Terminal File not found.")
|
|
92
|
+
|
|
93
|
+
def remove_usb_device(self):
|
|
94
|
+
self._usb_device = False
|
|
95
|
+
self.usb_device_name = False
|
|
96
|
+
self._usb_image_name = False
|
|
97
|
+
self.terminal_file = False
|
|
98
|
+
|
|
99
|
+
def _get_serial(self):
|
|
100
|
+
if not self.terminal_file:
|
|
101
|
+
raise Exception(
|
|
102
|
+
"Unable to open a Serial connexion,"
|
|
103
|
+
" because terminal_file is not defined."
|
|
104
|
+
)
|
|
105
|
+
return Serial(self.terminal_file, 9600, timeout=0.05)
|
|
106
|
+
|
|
107
|
+
# ###########################
|
|
108
|
+
# Loggging Section
|
|
109
|
+
# ###########################
|
|
110
|
+
def _logger(self, level, message):
|
|
111
|
+
if self._usb_device:
|
|
112
|
+
extra_info = f" ({self.usb_device_name} - {self.usb_vendor_product_code})"
|
|
113
|
+
else:
|
|
114
|
+
extra_info = ""
|
|
115
|
+
logger.log(level, f"Device '{self.device_type}'{extra_info}: {message}")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from queue import Queue
|
|
3
|
+
from threading import Thread
|
|
4
|
+
|
|
5
|
+
from .device_abstract import DeviceAbstract
|
|
6
|
+
from .device_task import DeviceTask
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DeviceAbstractWithQueue(DeviceAbstract, Thread):
|
|
10
|
+
has_queue = True
|
|
11
|
+
|
|
12
|
+
def __init__(self, interface, options, delay=0, max_queue_size=0):
|
|
13
|
+
"""Initialize a new Device.
|
|
14
|
+
delay: time between processing two messages, in seconds.
|
|
15
|
+
"""
|
|
16
|
+
DeviceAbstract.__init__(self, interface, options)
|
|
17
|
+
Thread.__init__(self)
|
|
18
|
+
self.errored_tasks = []
|
|
19
|
+
self._queue = Queue(max_queue_size)
|
|
20
|
+
self.delay = delay
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def queue_size(self):
|
|
24
|
+
return self._queue.qsize()
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def max_queue_size(self):
|
|
28
|
+
return self._queue.maxsize
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def errored_tasks_qty(self):
|
|
32
|
+
return len(self.errored_tasks)
|
|
33
|
+
|
|
34
|
+
# ###########################
|
|
35
|
+
# Thread / Task Section
|
|
36
|
+
# ###########################
|
|
37
|
+
def run(self):
|
|
38
|
+
while True:
|
|
39
|
+
if self._usb_device:
|
|
40
|
+
current_size = self._queue.qsize()
|
|
41
|
+
if current_size:
|
|
42
|
+
self._logger("INFO", f"Process one of the {current_size} task(s).")
|
|
43
|
+
else:
|
|
44
|
+
self._logger("INFO", "Waiting for message ...")
|
|
45
|
+
task = self._queue.get(True)
|
|
46
|
+
self._process_task_abstract(task)
|
|
47
|
+
time.sleep(self.delay)
|
|
48
|
+
else:
|
|
49
|
+
time.sleep(1)
|
|
50
|
+
|
|
51
|
+
def _process_task_abstract(self, task):
|
|
52
|
+
self._logger("DEBUG", f"Processing task {task.uuid} ...")
|
|
53
|
+
try:
|
|
54
|
+
if self.use_serial:
|
|
55
|
+
with self._get_serial() as serial:
|
|
56
|
+
self._process_task_serial(serial, task)
|
|
57
|
+
else:
|
|
58
|
+
self._process_task(task)
|
|
59
|
+
if task in self.errored_tasks:
|
|
60
|
+
self.errored_tasks.remove(task)
|
|
61
|
+
self._logger("SUCCESS", f"Task processed. UUID: {task.uuid}.")
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
self._logger("ERROR", f"Task {task.uuid}. Error{e}")
|
|
65
|
+
task.error = e
|
|
66
|
+
if task not in self.errored_tasks:
|
|
67
|
+
self._logger("INFO", f"Reenqueing task. UUID: {task.uuid} ...")
|
|
68
|
+
self.errored_tasks.append(task)
|
|
69
|
+
self._queue.put(task)
|
|
70
|
+
if self._usb_device:
|
|
71
|
+
self._interface._hook_usb_device_removed(self._usb_device, e)
|
|
72
|
+
|
|
73
|
+
def add_task(self, data):
|
|
74
|
+
if self.max_queue_size:
|
|
75
|
+
while self._queue.qsize() >= self._queue.maxsize:
|
|
76
|
+
self._logger("WARNING", "Removing obsolete task in queue.")
|
|
77
|
+
self._queue.get_nowait()
|
|
78
|
+
task = DeviceTask(data)
|
|
79
|
+
self._queue.put(task)
|
|
80
|
+
self._logger(
|
|
81
|
+
"INFO",
|
|
82
|
+
f"Task Enqueued ({self._queue.qsize()}/{self._queue.maxsize})."
|
|
83
|
+
f" UUID: {task.uuid}",
|
|
84
|
+
)
|
|
File without changes
|