From 2fd4dfca7f496b047a32526e743cc4c4067aea8c Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 4 Nov 2016 01:03:56 +0100 Subject: [PATCH 01/18] [FIX] point_of_sale: update POSBox image creation script - ngrok download URL has changed - default kernel for raspbian image has changed and needs to match - remove pre-existing git checkout to avoid conflicting - wait a few seconds after setting up loop dev with kpartx to access image contents - sometimes required to let the device appear in /dev --- addons/point_of_sale/tools/posbox/posbox_create_image.sh | 4 +++- addons/point_of_sale/tools/posbox/posbox_download_images.sh | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/point_of_sale/tools/posbox/posbox_create_image.sh b/addons/point_of_sale/tools/posbox/posbox_create_image.sh index ba01ef09da5..b08dd20a844 100755 --- a/addons/point_of_sale/tools/posbox/posbox_create_image.sh +++ b/addons/point_of_sale/tools/posbox/posbox_create_image.sh @@ -28,6 +28,7 @@ fi cp -a *raspbian*.img posbox.img CLONE_DIR="${OVERWRITE_FILES_BEFORE_INIT_DIR}/home/pi/odoo" +rm -rf "${CLONE_DIR}" mkdir "${CLONE_DIR}" git clone -b 8.0 --no-checkout --depth 1 https://github.com/odoo/odoo.git "${CLONE_DIR}" cd "${CLONE_DIR}" @@ -44,7 +45,7 @@ cd "${__dir}" USR_BIN="${OVERWRITE_FILES_BEFORE_INIT_DIR}/usr/bin/" mkdir -p "${USR_BIN}" cd "/tmp" -curl 'https://dl.ngrok.com/ngrok_2.0.19_linux_arm.zip' > ngrok.zip +curl 'https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip' > ngrok.zip unzip ngrok.zip rm ngrok.zip cd "${__dir}" @@ -68,6 +69,7 @@ START_OF_ROOT_PARTITION=$(fdisk -l posbox.img | tail -n 1 | awk '{print $2}') LOOP_MAPPER_PATH=$(kpartx -av posbox.img | tail -n 1 | cut -d ' ' -f 3) LOOP_MAPPER_PATH="/dev/mapper/${LOOP_MAPPER_PATH}" +sleep 5 # resize filesystem e2fsck -f "${LOOP_MAPPER_PATH}" # resize2fs requires clean fs diff --git a/addons/point_of_sale/tools/posbox/posbox_download_images.sh b/addons/point_of_sale/tools/posbox/posbox_download_images.sh index b500094058d..09bd79b2103 100755 --- a/addons/point_of_sale/tools/posbox/posbox_download_images.sh +++ b/addons/point_of_sale/tools/posbox/posbox_download_images.sh @@ -2,4 +2,4 @@ wget 'https://downloads.raspberrypi.org/raspbian_lite_latest' -O raspbian.img.zip unzip raspbian.img.zip -wget 'https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/kernel-qemu-4.1.13-jessie' -O kernel-qemu +wget 'https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/kernel-qemu-4.4.13-jessie' -O kernel-qemu From eeadeb0d6e31e33e51e6b4930e5a9ce940ed6935 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 2 Nov 2016 13:45:41 +0100 Subject: [PATCH 02/18] [FIX] hw_proxy,hw_blackbox_be: make RS-232 drivers protect their device RS-232 drivers now need to register any device they are handling in the hw_proxy.rs232_devices map. This will prevent other drivers from probing them, possibly messing up with the device. Any update to rs232_devices must be done while holding the hw_proxy.rs232_lock. The hw_scale driver will be updated to use this mechanism in the next commit, that will also handle a new RS-232 scale protocol. --- addons/hw_blackbox_be/controllers/main.py | 43 +++++++++++++---------- addons/hw_proxy/controllers/main.py | 7 ++++ 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/addons/hw_blackbox_be/controllers/main.py b/addons/hw_blackbox_be/controllers/main.py index 13579ee7b9b..a21ddbc2c80 100644 --- a/addons/hw_blackbox_be/controllers/main.py +++ b/addons/hw_blackbox_be/controllers/main.py @@ -14,6 +14,8 @@ import openerp.addons.hw_proxy.controllers.main as hw_proxy _logger = logging.getLogger(__name__) +DRIVER_NAME = 'fiscal_data_module' + class Blackbox(Thread): def __init__(self): Thread.__init__(self) @@ -39,26 +41,31 @@ class Blackbox(Thread): # request. The first device to give an answer that makes sense # wins. def _find_device_path_by_probing(self): - path = "/dev/serial/by-path/" - probe_message = self._wrap_low_level_message_around("S000") + with hw_proxy.rs232_lock: + path = "/dev/serial/by-path/" + probe_message = self._wrap_low_level_message_around("S000") - try: - devices = listdir(path) - except OSError: - _logger.warning(path + " doesn't exist") - else: - for device in listdir(path): - path_to_device = path + device - _logger.debug("Probing " + device) + try: + devices = listdir(path) + except OSError: + _logger.warning(path + " doesn't exist") + self.set_status("disconnected", ["No RS-232 device (or emulated ones) found"]) + else: + for device in devices: + if device in hw_proxy.rs232_devices: + continue + path_to_device = path + device + _logger.debug("Probing " + device) - if self._send_to_blackbox(probe_message, 21, path_to_device, just_wait_for_ack=True): - _logger.info(device + " will be used as the blackbox") - self.set_status("connected", [device]) - return path_to_device + if self._send_to_blackbox(probe_message, 21, path_to_device, just_wait_for_ack=True): + _logger.info(device + " will be used as the blackbox") + self.set_status("connected", [device]) + hw_proxy.rs232_devices[device] = DRIVER_NAME + return path_to_device - _logger.warning("Blackbox could not be found") - self.set_status("error", ["Couldn't find the Fiscal Data Module"]) - return "" + _logger.warning("Blackbox could not be found") + self.set_status("disconnected", ["Couldn't find the Fiscal Data Module"]) + return "" def _lrc(self, msg): lrc = 0 @@ -152,7 +159,7 @@ class Blackbox(Thread): if isfile("/home/pi/registered_blackbox_be"): blackbox_thread = Blackbox() - hw_proxy.drivers['fiscal_data_module'] = blackbox_thread + hw_proxy.drivers[DRIVER_NAME] = blackbox_thread class BlackboxDriver(hw_proxy.Proxy): @http.route('/hw_proxy/request_blackbox/', type='json', auth='none', cors='*') diff --git a/addons/hw_proxy/controllers/main.py b/addons/hw_proxy/controllers/main.py index d377b5e9a01..e1024772500 100644 --- a/addons/hw_proxy/controllers/main.py +++ b/addons/hw_proxy/controllers/main.py @@ -11,6 +11,8 @@ import subprocess import simplejson import werkzeug import werkzeug.wrappers +from threading import Lock + _logger = logging.getLogger(__name__) @@ -30,6 +32,11 @@ BANNED_DEVICES = set([ # so that 'status' can return the status of all active drivers drivers = {} +# keep a list of RS-232 devices that have been recognized by a driver, +# so other drivers can skip them during probes +rs232_devices = {} # {'/path/to/device': 'driver'} +rs232_lock = Lock() # must be held to update `rs232_devices` + class Proxy(http.Controller): def get_status(self): From f579acf02f4762cea9ecc64d2566b84cecf69ea5 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 1 Nov 2016 18:04:59 +0100 Subject: [PATCH 03/18] [FIX] hw_scale: update driver for rs232_devices map and support AdamAZExtra scale This commit fixes the driver to respect the new device registration mechanism via hw_proxy.rs232_devices. It also refactors the code to more easily support multiple scale protocols, and introduces support for the ADAM Equipment AZExtra scale protocol. (Might be compatible with other ADAM Equipment scales) Support for th AZExtra scale is experimental at the moment, especially given two annoying problems with this model: - they do not support proper probing (stays mute until a non-zero weight is measured), so they have to be probed last and *assumed* to work - the scale beeps when a read attempt is made and the weight is not stable yet, or the weight has been already read previously. This constant beeping during operation is mitigated by extra delays between readings, but might still prove to be a major issue for supporting this scale. --- addons/hw_scale/controllers/main.py | 365 +++++++++++++++++++--------- 1 file changed, 250 insertions(+), 115 deletions(-) diff --git a/addons/hw_scale/controllers/main.py b/addons/hw_scale/controllers/main.py index 730b9bb9c68..1ed1bb57468 100644 --- a/addons/hw_scale/controllers/main.py +++ b/addons/hw_scale/controllers/main.py @@ -1,21 +1,20 @@ # -*- coding: utf-8 -*- import logging import os +import re import time -from os import listdir -from os.path import join -from threading import Thread, Lock -from select import select -from Queue import Queue, Empty -import openerp +from collections import namedtuple +from os import listdir +from threading import Thread, Lock + import openerp.addons.hw_proxy.controllers.main as hw_proxy from openerp import http -from openerp.http import request -from openerp.tools.translate import _ _logger = logging.getLogger(__name__) +DRIVER_NAME = 'scale' + try: import serial except ImportError: @@ -23,6 +22,99 @@ except ImportError: serial = None +def _toledo8217StatusParse(status): + """ Parse a scale's status, returning a `(weight, weight_info)` pair. """ + weight, weight_info = None, None + stat = ord(status[status.index('?') + 1]) + if stat == 0: + weight_info = 'ok' + else: + weight_info = [] + if stat & 1 : + weight_info.append('moving') + if stat & 1 << 1: + weight_info.append('over_capacity') + if stat & 1 << 2: + weight_info.append('negative') + weight = 0.0 + if stat & 1 << 3: + weight_info.append('outside_zero_capture_range') + if stat & 1 << 4: + weight_info.append('center_of_zero') + if stat & 1 << 5: + weight_info.append('net_weight') + return weight, weight_info + +ScaleProtocol = namedtuple( + 'ScaleProtocol', + "name baudrate bytesize stopbits parity timeout writeTimeout weightRegexp statusRegexp " + "statusParse commandTerminator commandDelay weightDelay newWeightDelay " + "weightCommand zeroCommand tareCommand clearCommand emptyAnswerValid autoResetWeight") + +# 8217 Mettler-Toledo (Weight-only) Protocol, as described in the scale's Service Manual. +# e.g. here: https://www.manualslib.com/manual/861274/Mettler-Toledo-Viva.html?page=51#manual +# Our recommended scale, the Mettler-Toledo "Ariva-S", supports this protocol on +# both the USB and RS232 ports, it can be configured in the setup menu as protocol option 3. +# We use the default serial protocol settings, the scale's settings can be configured in the +# scale's menu anyway. +Toledo8217Protocol = ScaleProtocol( + name='Toledo 8217', + baudrate=9600, + bytesize=serial.SEVENBITS, + stopbits=serial.STOPBITS_ONE, + parity=serial.PARITY_EVEN, + timeout=1, + writeTimeout=1, + weightRegexp="\x02\\s*([0-9.]+)N?\\r", + statusRegexp="\x02\\s*(\\?.)\\r", + statusParse=_toledo8217StatusParse, + commandDelay=0.2, + weightDelay=0.5, + newWeightDelay=0.2, + commandTerminator='', + weightCommand='W', + zeroCommand='Z', + tareCommand='T', + clearCommand='C', + emptyAnswerValid=False, + autoResetWeight=False, +) + +# The ADAM scales have their own RS232 protocol, usually documented in the scale's manual +# e.g at https://www.adamequipment.com/media/docs/Print%20Publications/Manuals/PDF/AZEXTRA/AZEXTRA-UM.pdf +# https://www.manualslib.com/manual/879782/Adam-Equipment-Cbd-4.html?page=32#manual +# Only the baudrate and label format seem to be configurable in the AZExtra series. +ADAMEquipmentProtocol = ScaleProtocol( + name='Adam Equipment', + baudrate=4800, + bytesize=serial.EIGHTBITS, + stopbits=serial.STOPBITS_ONE, + parity=serial.PARITY_NONE, + timeout=0.2, + writeTimeout=0.2, + weightRegexp=r"\s*([0-9.]+)kg", # LABEL format 3 + KG in the scale settings, but Label 1/2 should work + statusRegexp=None, + statusParse=None, + commandTerminator="\r\n", + commandDelay=0.2, + weightDelay=0.5, + newWeightDelay=5, # AZExtra beeps every time you ask for a weight that was previously returned! + # Adding an extra delay gives the operator a chance to remove the products + # before the scale starts beeping. Could not find a way to disable the beeps. + weightCommand='P', + zeroCommand='Z', + tareCommand='T', + clearCommand=None, # No clear command -> Tare again + emptyAnswerValid=True, # AZExtra does not answer unless a new non-zero weight has been detected + autoResetWeight=True, # AZExtra will not return 0 after removing products +) + + +SCALE_PROTOCOLS = ( + Toledo8217Protocol, + ADAMEquipmentProtocol, # must be listed last, as it supports no probing! +) + class Scale(Thread): def __init__(self): Thread.__init__(self) @@ -33,8 +125,8 @@ class Scale(Thread): self.weight = 0 self.weight_info = 'ok' self.device = None - self.probed_device_paths = [] self.path_to_scale = '' + self.protocol = None def lockedstart(self): with self.lock: @@ -42,15 +134,15 @@ class Scale(Thread): self.daemon = True self.start() - def set_status(self, status, message = None): + def set_status(self, status, message=None): if status == self.status['status']: - if message != None and message != self.status['messages'][-1]: + if message is not None and message != self.status['messages'][-1]: self.status['messages'].append(message) if status == 'error' and message: - _logger.error('Scale Error: '+message) + _logger.error('Scale Error: '+ message) elif status == 'disconnected' and message: - _logger.warning('Disconnected Scale: '+message) + _logger.warning('Disconnected Scale: '+ message) else: self.status['status'] = status if message: @@ -59,61 +151,105 @@ class Scale(Thread): self.status['messages'] = [] if status == 'error' and message: - _logger.error('Scale Error: '+message) + _logger.error('Scale Error: '+ message) elif status == 'disconnected' and message: _logger.warning('Disconnected Scale: '+message) def _get_raw_response(self, connection): - response = "" + answer = [] while True: - byte = connection.read(1) - - if byte: - response += byte + char = connection.read(1) # may return `bytes` or `str` + if not char: + break else: - return response + answer.append(char) + return ''.join(answer) + + def _parse_weight_answer(self, protocol, answer): + """ Parse a scale's answer to a weighing request, returning + a `(weight, weight_info, status)` pair. + """ + weight, weight_info, status = None, None, None + try: + _logger.debug("Parsing weight [%r]", answer) + if not answer and protocol.emptyAnswerValid: + # Some scales do not return the same value again, but we + # should not clear the weight data, POS may still be reading it + return weight, weight_info, status + + if protocol.statusRegexp and re.search(protocol.statusRegexp, answer): + # parse status to set weight_info - we'll try weighing again later + weight, weight_info = protocol.statusParse(answer) + else: + match = re.search(protocol.weightRegexp, answer) + if match: + weight_text = match.group(1) + try: + weight = float(weight_text) + _logger.info('Weight: %s', weight) + except ValueError: + _logger.exception("Cannot parse weight [%r]", weight_text) + status = 'Invalid weight, please power-cycle the scale' + else: + _logger.error("Cannot parse scale answer [%r]", answer) + status = 'Invalid scale answer, please power-cycle the scale' + except Exception as e: + _logger.exception("Cannot parse scale answer [%r]", answer) + status = ("Could not weigh on scale %s with protocol %s: %s" % + (self.path_to_scale, protocol.name, e)) + return weight, weight_info, status def get_device(self): - try: - if not os.path.exists(self.input_dir): - self.set_status('disconnected','Scale Not Found') - return None - devices = [ device for device in listdir(self.input_dir)] + if self.device: + return self.device + + with hw_proxy.rs232_lock: + try: + if not os.path.exists(self.input_dir): + self.set_status('disconnected', 'No RS-232 device found') + return None + + devices = [device for device in listdir(self.input_dir)] - if len(devices) > 0: for device in devices: + driver = hw_proxy.rs232_devices.get(device) + if driver and driver != DRIVER_NAME: + # belongs to another driver + _logger.info('Ignoring %s, belongs to %s', device, driver) + continue path = self.input_dir + device - - # don't keep probing devices that are not a scale, - # only keep probing if in the past the device was - # confirmed to be a scale - if path not in self.probed_device_paths or path == self.path_to_scale: - _logger.debug('Probing: ' + path) + for protocol in SCALE_PROTOCOLS: + _logger.info('Probing %s with protocol %s', path, protocol) connection = serial.Serial(path, - baudrate = 9600, - bytesize = serial.SEVENBITS, - stopbits = serial.STOPBITS_ONE, - parity = serial.PARITY_EVEN, - timeout = 1, - writeTimeout = 1) - - connection.write("W") - self.probed_device_paths.append(path) - - if self._get_raw_response(connection): - _logger.debug(path + ' is scale') + baudrate=protocol.baudrate, + bytesize=protocol.bytesize, + stopbits=protocol.stopbits, + parity=protocol.parity, + timeout=1, # longer timeouts for probing + writeTimeout=1) # longer timeouts for probing + connection.write(protocol.weightCommand + protocol.commandTerminator) + time.sleep(protocol.commandDelay) + answer = self._get_raw_response(connection) + weight, weight_info, status = self._parse_weight_answer(protocol, answer) + if status: + _logger.info('Probing %s: no valid answer to protocol %s', path, protocol.name) + else: + _logger.info('Probing %s: answer looks ok for protocol %s', path, protocol.name) self.path_to_scale = path - self.set_status('connected','Connected to '+device) - connection.timeout = 0.02 - connection.writeTimeout = 0.02 + self.protocol = protocol + self.set_status( + 'connected', + 'Connected to %s with %s protocol' % (device, protocol.name) + ) + connection.timeout = protocol.timeout + connection.writeTimeout = protocol.writeTimeout + hw_proxy.rs232_devices[path] = DRIVER_NAME return connection - else: - _logger.debug('Already probed: ' + path) - self.set_status('disconnected','Scale Not Found') - return None - except Exception as e: - self.set_status('error',str(e)) + self.set_status('disconnected', 'No supported RS-232 scale found') + except Exception as e: + _logger.exception('Failed probing for scales') + self.set_status('error', 'Failed probing for scales: %s' % e) return None def get_weight(self): @@ -123,109 +259,110 @@ class Scale(Thread): def get_weight_info(self): self.lockedstart() return self.weight_info - + def get_status(self): self.lockedstart() return self.status def read_weight(self): with self.scalelock: - if self.device: - try: - self.device.write('W') - time.sleep(0.2) - answer = [] - - while True: - char = self.device.read(1) - if not char: - break - else: - answer.append(char) - - if '?' in answer: - stat = ord(answer[answer.index('?')+1]) - if stat == 0: - self.weight_info = 'ok' - else: - self.weight_info = [] - if stat & 1 : - self.weight_info.append('moving') - if stat & 1 << 1: - self.weight_info.append('over_capacity') - if stat & 1 << 2: - self.weight_info.append('negative') - self.weight = 0.0 - if stat & 1 << 3: - self.weight_info.append('outside_zero_capture_range') - if stat & 1 << 4: - self.weight_info.append('center_of_zero') - if stat & 1 << 5: - self.weight_info.append('net_weight') - else: - answer = answer[1:-1] - if 'N' in answer: - answer = answer[0:-1] - try: - self.weight = float(''.join(answer)) - except ValueError as v: - self.set_status('error','No data Received, please power-cycle the scale'); - self.device = None - - except Exception as e: - self.set_status('error',str(e)) + p = self.protocol + try: + self.device.write(p.weightCommand + p.commandTerminator) + time.sleep(p.commandDelay) + answer = self._get_raw_response(self.device) + weight, weight_info, status = self._parse_weight_answer(p, answer) + if status: + self.set_status('error', status) self.device = None + else: + if weight is not None: + self.weight = weight + if weight_info is not None: + self.weight_info = weight_info + except Exception as e: + self.set_status( + 'error', + "Could not weigh on scale %s with protocol %s: %s" % + (self.path_to_scale, p.name, e)) + self.device = None def set_zero(self): with self.scalelock: if self.device: - try: - self.device.write('Z') + try: + self.device.write(self.protocol.zeroCommand + self.protocol.commandTerminator) + time.sleep(self.protocol.commandDelay) except Exception as e: - self.set_status('error',str(e)) + self.set_status( + 'error', + "Could not zero scale %s with protocol %s: %s" % + (self.path_to_scale, self.protocol.name, e)) self.device = None def set_tare(self): with self.scalelock: if self.device: - try: - self.device.write('T') + try: + self.device.write(self.protocol.tareCommand + self.protocol.commandTerminator) + time.sleep(self.protocol.commandDelay) except Exception as e: - self.set_status('error',str(e)) + self.set_status( + 'error', + "Could not tare scale %s with protocol %s: %s" % + (self.path_to_scale, self.protocol.name, e)) self.device = None def clear_tare(self): with self.scalelock: if self.device: - try: - self.device.write('C') + p = self.protocol + try: + # if the protocol has no clear, we can just tare again + clearCommand = p.clearCommand or p.tareCommand + self.device.write(clearCommand + p.commandTerminator) + time.sleep(p.commandDelay) except Exception as e: - self.set_status('error',str(e)) + self.set_status( + 'error', + "Could not clear tare on scale %s with protocol %s: %s" % + (self.path_to_scale, p.name, e)) self.device = None def run(self): - self.device = None + self.device = None - while True: + while True: if self.device: + old_weight = self.weight self.read_weight() - time.sleep(0.15) + if self.weight != old_weight: + _logger.info('New Weight: %s, sleeping %ss', self.weight, self.protocol.newWeightDelay) + time.sleep(self.protocol.newWeightDelay) + if self.weight and self.protocol.autoResetWeight: + self.weight = 0 + else: + _logger.info('Weight: %s, sleeping %ss', self.weight, self.protocol.weightDelay) + time.sleep(self.protocol.weightDelay) else: with self.scalelock: self.device = self.get_device() if not self.device: - time.sleep(5) + # retry later to support "plug and play" + time.sleep(10) scale_thread = None if serial: scale_thread = Scale() - hw_proxy.drivers['scale'] = scale_thread + hw_proxy.drivers[DRIVER_NAME] = scale_thread class ScaleDriver(hw_proxy.Proxy): @http.route('/hw_proxy/scale_read/', type='json', auth='none', cors='*') def scale_read(self): if scale_thread: - return {'weight': scale_thread.get_weight(), 'unit':'kg', 'info': scale_thread.get_weight_info()} + return {'weight': scale_thread.get_weight(), + 'unit': 'kg', + 'info': scale_thread.get_weight_info()} return None @http.route('/hw_proxy/scale_zero/', type='json', auth='none', cors='*') @@ -245,5 +382,3 @@ class ScaleDriver(hw_proxy.Proxy): if scale_thread: scale_thread.clear_tare() return True - - From d1b0b9e656fa3e9641a7ac834626ebc61ef23376 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 3 Nov 2016 09:17:06 +0100 Subject: [PATCH 04/18] [FIX] point_of_sale: check required commands before building POSBox image --- addons/point_of_sale/tools/posbox/posbox_create_image.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/addons/point_of_sale/tools/posbox/posbox_create_image.sh b/addons/point_of_sale/tools/posbox/posbox_create_image.sh index b08dd20a844..1e4c9d1277c 100755 --- a/addons/point_of_sale/tools/posbox/posbox_create_image.sh +++ b/addons/point_of_sale/tools/posbox/posbox_create_image.sh @@ -13,6 +13,14 @@ file_exists() { [[ -f $1 ]]; } +require_command () { + type "$1" &> /dev/null || { echo "Command $1 is missing. Install it e.g. with 'apt-get install $1'. Aborting." >&2; exit 1; } +} + +require_command kpartx +require_command qemu-system-arm +require_command zerofree + __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" __file="${__dir}/$(basename "${BASH_SOURCE[0]}")" __base="$(basename ${__file} .sh)" From 265c9f7b06d3ff9d918ea98f28c3bc3449dff075 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Tue, 8 Nov 2016 17:49:35 +0100 Subject: [PATCH 05/18] [IMP] hw_proxy: add lsusb full output to hw status page --- addons/hw_proxy/controllers/main.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/addons/hw_proxy/controllers/main.py b/addons/hw_proxy/controllers/main.py index e1024772500..9b83013f0b4 100644 --- a/addons/hw_proxy/controllers/main.py +++ b/addons/hw_proxy/controllers/main.py @@ -54,7 +54,7 @@ class Proxy(http.Controller): return True @http.route('/hw_proxy/status', type='http', auth='none', cors='*') - def status_http(self): + def status_http(self, debug=None, **kwargs): resp = """ @@ -102,6 +102,8 @@ class Proxy(http.Controller):

Connected Devices

The list of connected USB devices as seen by the posbox

""" + if debug is None: + resp += """(debug version)""" devices = commands.getoutput("lsusb").split('\n') count = 0 resp += "
\n" @@ -117,6 +119,17 @@ class Proxy(http.Controller): resp += "
\n\n\n\n" + if debug is not None: + resp += """ + +

Debug version

+

lsusb -v output:

+
+                %s
+                
+ + """ % subprocess.check_output('lsusb -v', shell=True) + return request.make_response(resp,{ 'Cache-Control': 'no-cache', 'Content-Type': 'text/html; charset=utf-8', From 46ce74cddef2d188a1eb4e483c182e1810efae3e Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 9 Nov 2016 11:16:42 +0100 Subject: [PATCH 06/18] [FIX] hw_posbox_homepage: update links and version Also added a direct link to the remote connect page to grant remote SSH access via ngrok. --- addons/hw_posbox_homepage/controllers/main.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py index 7c9f90dab2e..65b276075d2 100644 --- a/addons/hw_posbox_homepage/controllers/main.py +++ b/addons/hw_posbox_homepage/controllers/main.py @@ -37,10 +37,10 @@ index_template = """

Your PosBox is up and running

- The PosBox is an hardware adapter that allows you to use + The PosBox is a hardware adapter that allows you to use receipt printers and barcode scanners with Odoo's Point of Sale, version 8.0 or later. You can start an online free trial, - or download and install it yourself. + or download and install it yourself.

For more information on how to setup the Point of Sale with @@ -55,7 +55,10 @@ index_template = """ Wi-Fi can be configured by visiting the Wi-Fi configuration page.

- The PosBox software installed on this posbox is version 14, + If you need to grant remote debugging access to a developer, you can do it here. +

+

+ The PosBox software installed on this posbox is version 15, the posbox version number is independent from Odoo. You can upgrade the software on the upgrade page.

From 9f172a61e0f2c87ff97128c44f0ad3b5f6e869b6 Mon Sep 17 00:00:00 2001 From: Kumar Aberer Date: Fri, 4 Nov 2016 12:00:33 +0100 Subject: [PATCH 07/18] [FIX] Check accounting lock date before cancel account moves Error message is shown when trying to cancel account moves that are prior to the accounting lock date. opw:693083 --- addons/account/models/account_move.py | 1 + 1 file changed, 1 insertion(+) diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 51f7ebd181f..18dafe61161 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -154,6 +154,7 @@ class AccountMove(models.Model): if not move.journal_id.update_posted: raise UserError(_('You cannot modify a posted entry of this journal.\nFirst you should set the journal to allow cancelling entries.')) if self.ids: + self._check_lock_date() self._cr.execute('UPDATE account_move '\ 'SET state=%s '\ 'WHERE id IN %s', ('draft', tuple(self.ids),)) From 51680426a6811ea5f227e295fdfa43ff599f3011 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 15 Nov 2016 14:45:46 +0100 Subject: [PATCH 08/18] [FIX] fields: convert_to_cache() on cached values for relational fields (#14328) --- openerp/addons/base/tests/test_api.py | 7 +++++++ openerp/fields.py | 7 ++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/tests/test_api.py b/openerp/addons/base/tests/test_api.py index 645c5d4ad9b..bdd6bed3162 100644 --- a/openerp/addons/base/tests/test_api.py +++ b/openerp/addons/base/tests/test_api.py @@ -323,6 +323,13 @@ class TestAPI(common.TransactionCase): self.assertEqual(set(partner2.child_ids), set(children2)) self.env.check_cache() + # convert from the cache format to the write format + partner = partner1 + partner.country_id, partner.child_ids + data = partner._convert_to_write(partner._cache) + self.assertEqual(data['country_id'], partner.country_id.id) + self.assertEqual(data['child_ids'], [(6, 0, partner.child_ids.ids)]) + @mute_logger('openerp.models') def test_60_prefetch(self): """ Check the record cache prefetching """ diff --git a/openerp/fields.py b/openerp/fields.py index 99345d67148..ed536c82ff6 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -1723,7 +1723,8 @@ class Many2one(_Relational): return process(value._ids) raise ValueError("Wrong value for %s: %r" % (self, value)) elif isinstance(value, tuple): - return process((value[0],)) + # value is either a pair (id, name), or a tuple of ids + return process(value[:1]) elif isinstance(value, dict): return process(record.env[self.comodel_name].new(value)._ids) else: @@ -1797,8 +1798,8 @@ class _RelationalMulti(_Relational): if isinstance(value, BaseModel): if not validate or (value._name == self.comodel_name): return process(value._ids) - elif isinstance(value, list): - # value is a list of record ids or commands + elif isinstance(value, (list, tuple)): + # value is a list/tuple of commands, dicts or record ids comodel = record.env[self.comodel_name] # determine the value ids; by convention empty on new records ids = OrderedSet(record[self.name].ids if record.id else ()) From 1de37862d6559c761fa26ef22715ff640f1f35b1 Mon Sep 17 00:00:00 2001 From: Josse Colpaert Date: Tue, 15 Nov 2016 10:40:57 +0100 Subject: [PATCH 09/18] [FIX] stock: when matching moves with pack operations through quants, it did not match the packages correctly. Courtesy of Pedro Baeza Before, it did a search to see if the package existed, but the only thing it needs to do is see if the package on the pack operation corresponds to that of the quant. (no need to check children also) There is a test added: A picking with 120 pieces incoming, 120 in pack 1, 80 in pack2 When we deliver those in a picking out, with product pack operations: (by taking them out of the pack) 120 from pack 1 and 80 from pack2, we should only have 2 quants and links between moves in the end And before, it generated 3 because it matched the wrong quants and made the wrong links. opw 693760 closes #13836 --- addons/stock/stock.py | 2 +- addons/stock/tests/common.py | 1 + addons/stock/tests/test_stock_flow.py | 127 +++++++++++++++++++++++++- 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 2f9c3c9d41e..7d39758aae3 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1314,7 +1314,7 @@ class stock_picking(osv.osv): #check if the quant is matching the operation details if ops.package_id: - flag = quant.package_id and bool(package_obj.search(cr, uid, [('id', 'child_of', [ops.package_id.id])], context=context)) or False + flag = quant.package_id == ops.package_id else: flag = not quant.package_id.id flag = flag and ((ops.lot_id and ops.lot_id.id == quant.lot_id.id) or not ops.lot_id) diff --git a/addons/stock/tests/common.py b/addons/stock/tests/common.py index 432c1e57f88..c868d8c1b84 100644 --- a/addons/stock/tests/common.py +++ b/addons/stock/tests/common.py @@ -35,6 +35,7 @@ class TestStockCommon(common.TransactionCase): self.productB = self.ProductObj.create({'name': 'Product B'}) self.productC = self.ProductObj.create({'name': 'Product C'}) self.productD = self.ProductObj.create({'name': 'Product D'}) + self.productE = self.ProductObj.create({'name': 'Product E', 'type': 'product'}) # Configure unit of measure. self.uom_kg = self.UomObj.create({ diff --git a/addons/stock/tests/test_stock_flow.py b/addons/stock/tests/test_stock_flow.py index bd3302b5be9..09012465fe9 100644 --- a/addons/stock/tests/test_stock_flow.py +++ b/addons/stock/tests/test_stock_flow.py @@ -1114,6 +1114,9 @@ class TestStockFlow(TestStockCommon): lot1 = lot_obj.create({'name': 'Lot001', 'product_id': lotproduct.id}) move = self.MoveObj.search([('product_id', '=', productKG.id), ('inventory_id', '=', inventory.id)], limit=1) self.assertEqual(len(move), 0, "Partial filter should not create a lines upon prepare") + + + line_vals = [] line_vals += [{'location_id': self.stock_location, 'product_id': packproduct.id, 'product_qty': 10, 'product_uom_id': packproduct.uom_id.id}] @@ -1127,7 +1130,7 @@ class TestStockFlow(TestStockCommon): quants = self.StockQuantObj.search([('product_id', '=', packproduct.id), ('location_id', '=', self.stock_location), ('package_id', '=', pack1.id)]) total_qty = sum([quant.qty for quant in quants]) self.assertEqual(total_qty, 20, 'Expecting 20 units on package 1 of packproduct, but we got %.4f on location stock!' % (total_qty)) - + #Create an inventory that will put the lots without lot to 0 and check that taking without pack will not take it from the pack inventory2 = self.InvObj.create({'name': 'Test Partial Lot and Pack2', 'filter': 'partial', @@ -1146,4 +1149,124 @@ class TestStockFlow(TestStockCommon): self.assertEqual(total_qty, 10, 'Expecting 0 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty)) quants = self.StockQuantObj.search([('product_id', '=', lotproduct.id), ('location_id', '=', self.stock_location), ('lot_id', '=', False)]) total_qty = sum([quant.qty for quant in quants]) - self.assertEqual(total_qty, 0, 'Expecting 0 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty)) \ No newline at end of file + self.assertEqual(total_qty, 0, 'Expecting 0 units lot of lotproduct, but we got %.4f on location stock!' % (total_qty)) + + + def test_30_create_in_out_with_product_pack_lines(self): + picking_in = self.PickingObj.create({ + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_in}) + self.MoveObj.create({ + 'name': self.productE.name, + 'product_id': self.productE.id, + 'product_uom_qty': 10, + 'product_uom': self.productE.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + picking_in.action_confirm() + picking_in.do_prepare_partial() + pack_obj = self.env['stock.quant.package'] + pack1 = pack_obj.create({'name': 'PACKINOUTTEST1'}) + pack2 = pack_obj.create({'name': 'PACKINOUTTEST2'}) + picking_in.pack_operation_ids[0].result_package_id = pack1 + picking_in.pack_operation_ids[0].product_qty = 4 + packop2 = picking_in.pack_operation_ids[0].copy() + packop2.product_qty = 6 + packop2.result_package_id = pack2 + picking_in.do_transfer() + self.assertEqual(sum([x.qty for x in picking_in.move_lines[0].quant_ids]), 10.0, 'Expecting 10 pieces in stock') + #check the quants are in the package + self.assertEqual(sum(x.qty for x in pack1.quant_ids), 4.0, 'Pack 1 should have 4 pieces') + self.assertEqual(sum(x.qty for x in pack2.quant_ids), 6.0, 'Pack 2 should have 6 pieces') + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out}) + self.MoveObj.create({ + 'name': self.productE.name, + 'product_id': self.productE.id, + 'product_uom_qty': 3, + 'product_uom': self.productE.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.action_confirm() + picking_out.action_assign() + picking_out.do_prepare_partial() + packout1 = picking_out.pack_operation_ids[0] + packout2 = picking_out.pack_operation_ids[0].copy() + packout1.product_qty = 2 + packout1.package_id = pack1 + packout2.package_id = pack2 + packout2.product_qty = 1 + picking_out.do_transfer() + #Check there are no negative quants + neg_quants = self.env['stock.quant'].search([('product_id', '=', self.productE.id), ('qty', '<', 0.0)]) + self.assertEqual(len(neg_quants), 0, 'There are negative quants!') + self.assertEqual(len(picking_out.move_lines[0].linked_move_operation_ids), 2, 'We should have 2 links in the matching between the move and the operations') + self.assertEqual(len(picking_out.move_lines[0].quant_ids), 2, 'We should have exactly 2 quants in the end') + + + def test_40_create_in_out_with_product_pack_lines(self): + picking_in = self.PickingObj.create({ + 'partner_id': self.partner_delta_id, + 'picking_type_id': self.picking_type_in}) + self.MoveObj.create({ + 'name': self.productE.name, + 'product_id': self.productE.id, + 'product_uom_qty': 200, + 'product_uom': self.productE.uom_id.id, + 'picking_id': picking_in.id, + 'location_id': self.supplier_location, + 'location_dest_id': self.stock_location}) + + picking_in.action_confirm() + picking_in.do_prepare_partial() + pack_obj = self.env['stock.quant.package'] + pack1 = pack_obj.create({'name': 'PACKINOUTTEST1'}) + pack2 = pack_obj.create({'name': 'PACKINOUTTEST2'}) + picking_in.pack_operation_ids[0].result_package_id = pack1 + picking_in.pack_operation_ids[0].product_qty = 120 + packop2 = picking_in.pack_operation_ids[0].copy() + packop2.product_qty = 80 + packop2.result_package_id = pack2 + picking_in.do_transfer() + self.assertEqual(sum([x.qty for x in picking_in.move_lines[0].quant_ids]), 200.0, 'Expecting 200 pieces in stock') + #check the quants are in the package + self.assertEqual(sum(x.qty for x in pack1.quant_ids), 120, 'Pack 1 should have 120 pieces') + self.assertEqual(sum(x.qty for x in pack2.quant_ids), 80, 'Pack 2 should have 80 pieces') + picking_out = self.PickingObj.create({ + 'partner_id': self.partner_agrolite_id, + 'picking_type_id': self.picking_type_out}) + self.MoveObj.create({ + 'name': self.productE.name, + 'product_id': self.productE.id, + 'product_uom_qty': 200 , + 'product_uom': self.productE.uom_id.id, + 'picking_id': picking_out.id, + 'location_id': self.stock_location, + 'location_dest_id': self.customer_location}) + picking_out.action_confirm() + picking_out.action_assign() + picking_out.do_prepare_partial() + #Convert entire packs into taking out of packs + packout0 = picking_out.pack_operation_ids[0] + packout1 = picking_out.pack_operation_ids[1] + packout0.write({'product_id': self.productE.id, + 'product_qty' : 120.0, + 'product_uom_id' : self.productE.uom_id.id, + 'package_id': pack1.id, + }) + packout1.write({'product_id': self.productE.id, + 'product_qty' : 80.0, + 'product_uom_id' : self.productE.uom_id.id, + 'package_id': pack2.id, + }) + picking_out.do_transfer() + #Check there are no negative quants + neg_quants = self.env['stock.quant'].search([('product_id', '=', self.productE.id), ('qty', '<', 0.0)]) + self.assertEqual(len(neg_quants), 0, 'There are negative quants!') + # We should also make sure that when matching stock moves with pack operations, it takes the correct + self.assertEqual(len(picking_out.move_lines[0].linked_move_operation_ids), 2, 'We should only have 2 links beween the move and the 2 operations') + self.assertEqual(len(picking_out.move_lines[0].quant_ids), 2, 'We should have exactly 2 quants in the end') \ No newline at end of file From 2e0a30001691eb8608b99881c9cee3d2073d9373 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Wed, 16 Nov 2016 13:40:37 +0100 Subject: [PATCH 10/18] [FIX] web_calendar, web_kanban: open form view in edit mode after edit button Some buttons in kanban records or event records allowed to open a form view in edit mode. The feature was lost with commit https://github.com/odoo/odoo/commit/46857d26#diff-fc48ba68abe25af0106d66b6ea2ec73eL116 Indeed the "do_switch_view" method signature changed but some calls still used three arguments instead of two, losing the options param. --- addons/web_calendar/static/src/js/web_calendar.js | 6 +++--- addons/web_kanban/static/src/js/kanban_view.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/web_calendar/static/src/js/web_calendar.js b/addons/web_calendar/static/src/js/web_calendar.js index 549edda4049..4644c00c894 100644 --- a/addons/web_calendar/static/src/js/web_calendar.js +++ b/addons/web_calendar/static/src/js/web_calendar.js @@ -881,9 +881,9 @@ var CalendarView = View.extend({ var index = this.dataset.get_id_index(id); this.dataset.index = index; if (this.write_right) { - this.do_switch_view('form', null, { mode: "edit" }); + this.do_switch_view('form', { mode: "edit" }); } else { - this.do_switch_view('form', null, { mode: "view" }); + this.do_switch_view('form', { mode: "view" }); } } else { @@ -897,7 +897,7 @@ var CalendarView = View.extend({ buttons: [ {text: _t("Edit"), classes: 'btn-primary', close: true, click: function() { self.dataset.index = self.dataset.get_id_index(id); - self.do_switch_view('form', null, { mode: "edit" }); + self.do_switch_view('form', { mode: "edit" }); }}, {text: _t("Delete"), close: true, click: function() { diff --git a/addons/web_kanban/static/src/js/kanban_view.js b/addons/web_kanban/static/src/js/kanban_view.js index e96d354a7e8..6677b322bad 100644 --- a/addons/web_kanban/static/src/js/kanban_view.js +++ b/addons/web_kanban/static/src/js/kanban_view.js @@ -491,7 +491,7 @@ var KanbanView = View.extend({ open_record: function (event, options) { if (this.dataset.select_id(event.data.id)) { - this.do_switch_view('form', null, options); //, null, { mode: "edit" }); + this.do_switch_view('form', options); } else { this.do_warn("Kanban: could not find id#" + event.data.id); } From 897290dd3ff218ddc12dcdc6c936a8725813cefe Mon Sep 17 00:00:00 2001 From: Nicolas Lempereur Date: Wed, 16 Nov 2016 11:01:52 +0100 Subject: [PATCH 11/18] [IMP] web_editor: don't lose modified code view content The scenario when using the code view is the following : - click on code view button to use code view - make changes - click on code view button to get back to wysiwyg view - save from the wysiwyg view If the 3rd step is not done: - for the inline editor, we lose the changes that have been done in code view, - for the iframe editor, we lose the entirety of the content. At one time the user was forced to go back to wysiwyg before being able to save, but this have been removed hence the present usability issue. With this fix, when the editor content is saved, if the user is in code view he is automatically set back to wysiwyg view. closes #14339 opw-693052 --- addons/web_editor/static/src/js/backend.js | 10 ++++++++++ addons/web_editor/static/src/js/rte.summernote.js | 2 ++ 2 files changed, 12 insertions(+) diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js index 0cd9abb5596..6b956c344db 100644 --- a/addons/web_editor/static/src/js/backend.js +++ b/addons/web_editor/static/src/js/backend.js @@ -161,6 +161,11 @@ var FieldTextHtmlSimple = widget.extend({ return !this.get('value') || this.get('value') === "


" || !this.get('value').match(/\S/); }, commit_value: function() { + // leave code view before saving + if (this.$content.prev('textarea').length === 1) { + var layoutInfo = { 'editor': function() { return this.$content; }.bind(this) }; + $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo); + } if (this.options['style-inline']) { transcoder.class_to_style(this.$content); transcoder.font_to_img(this.$content); @@ -374,6 +379,11 @@ var FieldTextHtml = widget.extend({ this._dirty_flag = false; return this.editor.save(); } else if (this._dirty_flag && this.editor && this.editor.buildingBlock) { + // leave code view before saving + if (this.$content.prev('textarea').length === 1) { + var layoutInfo = { 'editor': function() { return this.$content; }.bind(this) }; + $.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo); + } this.editor.buildingBlock.clean_for_save(); this.internal_set_value( this.$content.html() ); } diff --git a/addons/web_editor/static/src/js/rte.summernote.js b/addons/web_editor/static/src/js/rte.summernote.js index 8cbb80583b7..b7d4c191b7e 100644 --- a/addons/web_editor/static/src/js/rte.summernote.js +++ b/addons/web_editor/static/src/js/rte.summernote.js @@ -485,6 +485,8 @@ function prettify_html(html) { } return result; } + +// Change Warning: called with dummy arguments to quit code view before saving $.summernote.pluginEvents.codeview = function (event, editor, layoutInfo) { if (layoutInfo.toolbar) { return eventHandler.modules.codeview.toggle(layoutInfo); From 8e8b7925d233743ad007fa7daf0b1ddb940cdb3a Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 16 Nov 2016 14:52:33 +0100 Subject: [PATCH 12/18] [FIX] hw_escpos: auto-detect endpoint address Instead of hardcoding the IN & OUT endpoint addresses for the ESC/POS printers, we now attempt to auto-detect them. This should increase compatibility with many compatible thermal printers that are simply using a different address, such as the - STMicroelectronics POS58 Printer USB - HU HAI SUNCSW Receipt Printer Co.,Ltd. Gprinter USB Printer Fixes #12890 --- addons/hw_escpos/controllers/main.py | 8 ++++++-- addons/hw_escpos/escpos/printer.py | 19 ++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py index 0b91dc0d37e..23672e12385 100644 --- a/addons/hw_escpos/controllers/main.py +++ b/addons/hw_escpos/controllers/main.py @@ -104,8 +104,12 @@ class EscposDriver(Thread): printers = self.connected_usb_devices() if len(printers) > 0: - self.set_status('connected','Connected to '+printers[0]['name']) - return Usb(printers[0]['vendor'], printers[0]['product']) + print_dev = Usb(printers[0]['vendor'], printers[0]['product']) + self.set_status( + 'connected', + "Connected to %s (in=0x%02x,out=0x%02x)" % (printers[0]['name'], print_dev.in_ep, print_dev.out_ep) + ) + return print_dev else: self.set_status('disconnected','Printer Not Found') return None diff --git a/addons/hw_escpos/escpos/printer.py b/addons/hw_escpos/escpos/printer.py index 1f32875a5cb..d52668b50f9 100644 --- a/addons/hw_escpos/escpos/printer.py +++ b/addons/hw_escpos/escpos/printer.py @@ -13,7 +13,7 @@ from time import sleep class Usb(Escpos): """ Define USB printer """ - def __init__(self, idVendor, idProduct, interface=0, in_ep=0x82, out_ep=0x01): + def __init__(self, idVendor, idProduct, interface=0, in_ep=None, out_ep=None): """ @param idVendor : Vendor ID @param idProduct : Product ID @@ -42,6 +42,23 @@ class Usb(Escpos): self.device.detach_kernel_driver(self.interface) self.device.set_configuration() usb.util.claim_interface(self.device, self.interface) + + cfg = self.device.get_active_configuration() + intf = cfg[(0,0)] # first interface + if self.in_ep is None: + # Attempt to detect IN/OUT endpoint addresses + try: + is_IN = lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_IN + is_OUT = lambda e: usb.util.endpoint_direction(e.bEndpointAddress) == usb.util.ENDPOINT_OUT + endpoint_in = usb.util.find_descriptor(intf, custom_match=is_IN) + endpoint_out = usb.util.find_descriptor(intf, custom_match=is_OUT) + self.in_ep = endpoint_in.bEndpointAddress + self.out_ep = endpoint_out.bEndpointAddress + except usb.core.USBError: + # default values for officially supported printers + self.in_ep = 0x82 + self.out_ep = 0x01 + except usb.core.USBError as e: raise HandleDeviceError(e) From 51882ea70cb599aec8b424b0bde4edc425299c36 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Wed, 16 Nov 2016 16:24:07 +0100 Subject: [PATCH 13/18] [FIX] account: force to choose a refund sequence On an accounting journal, when checking "Dedicated refund sequence", choosing the refund sequence is mandatory. This is the subject of the below revision: d94c449547a2a7a6e270e4602320f588c5b6f896 Since the refund sequence field was available only for with the debug mode, the user was not forced to choose the sequence, while he must be. This is not possible to use the dedicated refund sequence if you do not choose it there. opw-693955 --- addons/account/views/account_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml index c4687b219de..5821eb32b0a 100644 --- a/addons/account/views/account_view.xml +++ b/addons/account/views/account_view.xml @@ -252,7 +252,7 @@ - + From 164b8ed01a96c860cf758514abfd50ba349e990c Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Wed, 16 Nov 2016 14:51:28 +0100 Subject: [PATCH 14/18] [FIX] website_sale: Set the selected state in the visible select tag When there is a shipping address, this is the second select tag which is used(defined in website_same_delivery/views/website_same_delivery.xml) opw:693127 --- addons/website_sale/static/src/js/website_sale.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 1dac377270e..a48483b5349 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -335,9 +335,9 @@ $('.oe_website_sale').each(function () { $('input.js_variant_change, select.js_variant_change', this).first().trigger('change'); }); - var state_options = $("select[name='state_id'] option:not(:first)"); + var state_options = $("select[name='state_id']:visible option:not(:first)"); $(oe_website_sale).on('change', "select[name='country_id']", function () { - var select = $("select[name='state_id']"); + var select = $("select[name='state_id']:visible"); var selected_state = select.val(); state_options.detach(); var displayed_state = state_options.filter("[data-country_id="+($(this).val() || 0)+"]"); From 86252428846607cd5f4b18cef8de49a8cd0b151a Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Wed, 16 Nov 2016 16:40:10 +0100 Subject: [PATCH 15/18] [FIX] web: properly destroy nvtooltips when switching views If the graph view was left while a nvtooltip was shown (thanks to keyboard shortcuts or during loading time), the nvtooltip remained and was only destroyed when leaving the action. Now they are automatically removed with the view as they are now placed inside of them instead of inside the body element. --- addons/web/static/src/css/base.css | 4 ++++ addons/web/static/src/css/base.sass | 3 +++ addons/web/static/src/js/views/graph_widget.js | 11 +++++++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 4a2d5f81bb2..eed068c4931 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -3056,3 +3056,7 @@ body { white-space: nowrap; padding-left: 10px; } + +.nvtooltip thead { + background-color: transparent; +} diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index 7ba9e6bece3..37ac418816d 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -2605,3 +2605,6 @@ body width: 100px white-space: nowrap padding-left: 10px + +.nvtooltip thead + background-color: transparent diff --git a/addons/web/static/src/js/views/graph_widget.js b/addons/web/static/src/js/views/graph_widget.js index d0cd362487d..73b9c6be406 100644 --- a/addons/web/static/src/js/views/graph_widget.js +++ b/addons/web/static/src/js/views/graph_widget.js @@ -97,7 +97,8 @@ return Widget.extend({ "there is no active filter in the search bar."), })); } else { - this['display_' + this.mode](); + var chart = this['display_' + this.mode](); + chart.tooltip.chartContainer(this.$el[0]); } }, display_bar: function () { @@ -181,6 +182,8 @@ return Widget.extend({ chart(svg); this.to_remove = chart.update; nv.utils.onWindowResize(chart.update); + + return chart; }, display_pie: function () { var data = [], @@ -231,6 +234,8 @@ return Widget.extend({ chart(svg); this.to_remove = chart.update; nv.utils.onWindowResize(chart.update); + + return chart; }, display_line: function () { if (this.data.length < 2) { @@ -308,7 +313,9 @@ return Widget.extend({ chart(svg); this.to_remove = chart.update; - nv.utils.onWindowResize(chart.update); + nv.utils.onWindowResize(chart.update); + + return chart; }, destroy: function () { nv.utils.offWindowResize(this.to_remove); From 232b0d18ac7f3f2c799910cb6ac1b160406717e9 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Wed, 16 Nov 2016 17:36:45 +0100 Subject: [PATCH 16/18] [FIX] account: oversight in forward-port. The revision a1d6c2d6317fd3cbc9af0060c14e4281ce63e11f has been ported with an oversight: except_orm is no longer imported in this file, besides, the new way to raise these warnings is to use "UserError" instead. opw-693779 --- addons/account/account_invoice.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index 9159410ed70..458ceee1978 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -810,8 +810,7 @@ class account_invoice(models.Model): if not inv.date_invoice: # FORWARD-PORT UP TO SAAS-6 if inv.currency_id != company_currency and inv.tax_line: - raise except_orm( - _('Warning!'), + raise UserError( _('No invoice date!' '\nThe invoice currency is not the same than the company currency.' ' An invoice date is required to determine the exchange rate to apply. Do not forget to update the taxes!' From 84d5216a73b848f7b100f9c66e81fab1f91bcd2f Mon Sep 17 00:00:00 2001 From: Goffin Simon Date: Wed, 16 Nov 2016 16:13:46 +0100 Subject: [PATCH 17/18] [FIX] sale: vendor bill with a SO reference If the PO and SO have the same sequence, the source document of vendor bills and customer invoices could be the same. opw:693991 --- addons/sale/sale.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 8142a39f416..b2537e98516 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -51,7 +51,7 @@ class SaleOrder(models.Model): refund is not directly linked to the SO. """ for order in self: - invoice_ids = order.order_line.mapped('invoice_lines').mapped('invoice_id') + invoice_ids = order.order_line.mapped('invoice_lines').mapped('invoice_id').filtered(lambda r: r.type in ['out_invoice', 'out_refund']) # Search for invoices which have been 'cancelled' (filter_refund = 'modify' in # 'account.invoice.refund') # use like as origin may contains multiple references (e.g. 'SO01, SO02') From 25888f41952e9b9f78fa493b906dfe458a410210 Mon Sep 17 00:00:00 2001 From: fwi-odoo Date: Thu, 17 Nov 2016 09:54:49 +0100 Subject: [PATCH 18/18] [FIX] web: Deleting items in list view does not reset min to 1 When all the items of the first list view page were deleted, the list view was not reloaded correctly and no records were displayed. --- addons/web/static/src/js/views/list_view.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/views/list_view.js b/addons/web/static/src/js/views/list_view.js index 45fa6b6f1b6..765534da788 100644 --- a/addons/web/static/src/js/views/list_view.js +++ b/addons/web/static/src/js/views/list_view.js @@ -554,8 +554,11 @@ var ListView = View.extend({ if (self.display_nocontent_helper()) { self.no_result(); } else { - // Load previous page if the current one is empty - if (self.records.length === 0 && self.dataset.size() > 0) { + if (self.records.length && self.current_min === 1) { + // Reload the list view if we delete all the records of the first page + self.reload(); + } else if (self.records.length && self.dataset.size() > 0) { + // Load previous page if the current one is empty self.pager.previous(); } // Reload the list view if we are not on the last page