[MERGE] forward port branch 9.0 up to 7ee09ed

This commit is contained in:
Christophe Simonis
2016-11-17 14:37:19 +01:00
19 changed files with 497 additions and 150 deletions
+1
View File
@@ -152,6 +152,7 @@ class AccountMove(models.Model):
if not move.journal_id.update_posted: 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.')) raise UserError(_('You cannot modify a posted entry of this journal.\nFirst you should set the journal to allow cancelling entries.'))
if self.ids: if self.ids:
self._check_lock_date()
self._cr.execute('UPDATE account_move '\ self._cr.execute('UPDATE account_move '\
'SET state=%s '\ 'SET state=%s '\
'WHERE id IN %s', ('draft', tuple(self.ids),)) 'WHERE id IN %s', ('draft', tuple(self.ids),))
+1 -1
View File
@@ -282,7 +282,7 @@
<field name="code"/> <field name="code"/>
<field name="refund_sequence" attrs="{'invisible': [('type', 'not in', ['sale', 'purchase'])]}" groups="base.group_no_one"/> <field name="refund_sequence" attrs="{'invisible': [('type', 'not in', ['sale', 'purchase'])]}" groups="base.group_no_one"/>
<field name="sequence_id" required="0" attrs="{'readonly': 1}" groups="base.group_no_one"/> <field name="sequence_id" required="0" attrs="{'readonly': 1}" groups="base.group_no_one"/>
<field name="refund_sequence_id" attrs="{'invisible': ['|',('type', 'not in', ['sale', 'purchase']), ('refund_sequence', '!=', True)], 'required': [('type', 'in', ['sale', 'purchase']), ('refund_sequence', '=', True)]}" groups="base.group_no_one"/> <field name="refund_sequence_id" attrs="{'invisible': ['|',('type', 'not in', ['sale', 'purchase']), ('refund_sequence', '!=', True)], 'required': [('type', 'in', ['sale', 'purchase']), ('refund_sequence', '=', True)]}"/>
</group> </group>
<group> <group>
<field name="default_debit_account_id" domain="[('deprecated', '=', False)]" /> <field name="default_debit_account_id" domain="[('deprecated', '=', False)]" />
+25 -18
View File
@@ -14,6 +14,8 @@ import openerp.addons.hw_proxy.controllers.main as hw_proxy
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
DRIVER_NAME = 'fiscal_data_module'
class Blackbox(Thread): class Blackbox(Thread):
def __init__(self): def __init__(self):
Thread.__init__(self) Thread.__init__(self)
@@ -39,26 +41,31 @@ class Blackbox(Thread):
# request. The first device to give an answer that makes sense # request. The first device to give an answer that makes sense
# wins. # wins.
def _find_device_path_by_probing(self): def _find_device_path_by_probing(self):
path = "/dev/serial/by-path/" with hw_proxy.rs232_lock:
probe_message = self._wrap_low_level_message_around("S000") path = "/dev/serial/by-path/"
probe_message = self._wrap_low_level_message_around("S000")
try: try:
devices = listdir(path) devices = listdir(path)
except OSError: except OSError:
_logger.warning(path + " doesn't exist") _logger.warning(path + " doesn't exist")
else: self.set_status("disconnected", ["No RS-232 device (or emulated ones) found"])
for device in listdir(path): else:
path_to_device = path + device for device in devices:
_logger.debug("Probing " + device) 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): 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") _logger.info(device + " will be used as the blackbox")
self.set_status("connected", [device]) self.set_status("connected", [device])
return path_to_device hw_proxy.rs232_devices[device] = DRIVER_NAME
return path_to_device
_logger.warning("Blackbox could not be found") _logger.warning("Blackbox could not be found")
self.set_status("error", ["Couldn't find the Fiscal Data Module"]) self.set_status("disconnected", ["Couldn't find the Fiscal Data Module"])
return "" return ""
def _lrc(self, msg): def _lrc(self, msg):
lrc = 0 lrc = 0
@@ -152,7 +159,7 @@ class Blackbox(Thread):
if isfile("/home/pi/registered_blackbox_be"): if isfile("/home/pi/registered_blackbox_be"):
blackbox_thread = Blackbox() blackbox_thread = Blackbox()
hw_proxy.drivers['fiscal_data_module'] = blackbox_thread hw_proxy.drivers[DRIVER_NAME] = blackbox_thread
class BlackboxDriver(hw_proxy.Proxy): class BlackboxDriver(hw_proxy.Proxy):
@http.route('/hw_proxy/request_blackbox/', type='json', auth='none', cors='*') @http.route('/hw_proxy/request_blackbox/', type='json', auth='none', cors='*')
+6 -2
View File
@@ -104,8 +104,12 @@ class EscposDriver(Thread):
printers = self.connected_usb_devices() printers = self.connected_usb_devices()
if len(printers) > 0: if len(printers) > 0:
self.set_status('connected','Connected to '+printers[0]['name']) print_dev = Usb(printers[0]['vendor'], printers[0]['product'])
return 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: else:
self.set_status('disconnected','Printer Not Found') self.set_status('disconnected','Printer Not Found')
return None return None
+18 -1
View File
@@ -13,7 +13,7 @@ from time import sleep
class Usb(Escpos): class Usb(Escpos):
""" Define USB printer """ """ 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 idVendor : Vendor ID
@param idProduct : Product ID @param idProduct : Product ID
@@ -42,6 +42,23 @@ class Usb(Escpos):
self.device.detach_kernel_driver(self.interface) self.device.detach_kernel_driver(self.interface)
self.device.set_configuration() self.device.set_configuration()
usb.util.claim_interface(self.device, self.interface) 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: except usb.core.USBError as e:
raise HandleDeviceError(e) raise HandleDeviceError(e)
@@ -37,10 +37,10 @@ index_template = """
<body> <body>
<h1>Your PosBox is up and running</h1> <h1>Your PosBox is up and running</h1>
<p> <p>
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 receipt printers and barcode scanners with Odoo's Point of
Sale, <b>version 8.0 or later</b>. You can start an <a href='https://www.odoo.com/start'>online free trial</a>, Sale, <b>version 8.0 or later</b>. You can start an <a href='https://www.odoo.com/start'>online free trial</a>,
or <a href='https://www.odoo.com/start?download'>download and install</a> it yourself. or <a href='https://www.odoo.com/page/download'>download and install</a> it yourself.
</p> </p>
<p> <p>
For more information on how to setup the Point of Sale with 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 <a href='/wifi'>Wi-Fi configuration page</a>. Wi-Fi can be configured by visiting the <a href='/wifi'>Wi-Fi configuration page</a>.
</p> </p>
<p> <p>
The PosBox software installed on this posbox is <b>version 14</b>, If you need to grant remote debugging access to a developer, you can do it <a href='/remote_connect'>here</a>.
</p>
<p>
The PosBox software installed on this posbox is <b>version 15</b>,
the posbox version number is independent from Odoo. You can upgrade the posbox version number is independent from Odoo. You can upgrade
the software on the <a href='/hw_proxy/upgrade/'>upgrade page</a>. the software on the <a href='/hw_proxy/upgrade/'>upgrade page</a>.
</p> </p>
+21 -1
View File
@@ -11,6 +11,8 @@ import subprocess
import json import json
import werkzeug import werkzeug
import werkzeug.wrappers import werkzeug.wrappers
from threading import Lock
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -30,6 +32,11 @@ BANNED_DEVICES = set([
# so that 'status' can return the status of all active drivers # so that 'status' can return the status of all active drivers
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): class Proxy(http.Controller):
def get_status(self): def get_status(self):
@@ -47,7 +54,7 @@ class Proxy(http.Controller):
return True return True
@http.route('/hw_proxy/status', type='http', auth='none', cors='*') @http.route('/hw_proxy/status', type='http', auth='none', cors='*')
def status_http(self): def status_http(self, debug=None, **kwargs):
resp = """ resp = """
<!DOCTYPE HTML> <!DOCTYPE HTML>
<html> <html>
@@ -95,6 +102,8 @@ class Proxy(http.Controller):
<h2>Connected Devices</h2> <h2>Connected Devices</h2>
<p>The list of connected USB devices as seen by the posbox</p> <p>The list of connected USB devices as seen by the posbox</p>
""" """
if debug is None:
resp += """(<a href="/hw_proxy/status?debug">debug version</a>)"""
devices = commands.getoutput("lsusb").split('\n') devices = commands.getoutput("lsusb").split('\n')
count = 0 count = 0
resp += "<div class='devices'>\n" resp += "<div class='devices'>\n"
@@ -110,6 +119,17 @@ class Proxy(http.Controller):
resp += "</div>\n</body>\n</html>\n\n" resp += "</div>\n</body>\n</html>\n\n"
if debug is not None:
resp += """
<h3>Debug version</h3>
<p><tt>lsusb -v</tt> output:</p>
<pre>
%s
</pre>
""" % subprocess.check_output('lsusb -v', shell=True)
return request.make_response(resp,{ return request.make_response(resp,{
'Cache-Control': 'no-cache', 'Cache-Control': 'no-cache',
'Content-Type': 'text/html; charset=utf-8', 'Content-Type': 'text/html; charset=utf-8',
+250 -115
View File
@@ -1,21 +1,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
import os import os
import re
import time 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 import openerp.addons.hw_proxy.controllers.main as hw_proxy
from openerp import http from openerp import http
from openerp.http import request
from openerp.tools.translate import _
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
DRIVER_NAME = 'scale'
try: try:
import serial import serial
except ImportError: except ImportError:
@@ -23,6 +22,99 @@ except ImportError:
serial = None 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): class Scale(Thread):
def __init__(self): def __init__(self):
Thread.__init__(self) Thread.__init__(self)
@@ -33,8 +125,8 @@ class Scale(Thread):
self.weight = 0 self.weight = 0
self.weight_info = 'ok' self.weight_info = 'ok'
self.device = None self.device = None
self.probed_device_paths = []
self.path_to_scale = '' self.path_to_scale = ''
self.protocol = None
def lockedstart(self): def lockedstart(self):
with self.lock: with self.lock:
@@ -42,15 +134,15 @@ class Scale(Thread):
self.daemon = True self.daemon = True
self.start() self.start()
def set_status(self, status, message = None): def set_status(self, status, message=None):
if status == self.status['status']: 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) self.status['messages'].append(message)
if status == 'error' and message: if status == 'error' and message:
_logger.error('Scale Error: '+message) _logger.error('Scale Error: '+ message)
elif status == 'disconnected' and message: elif status == 'disconnected' and message:
_logger.warning('Disconnected Scale: '+message) _logger.warning('Disconnected Scale: '+ message)
else: else:
self.status['status'] = status self.status['status'] = status
if message: if message:
@@ -59,61 +151,105 @@ class Scale(Thread):
self.status['messages'] = [] self.status['messages'] = []
if status == 'error' and message: if status == 'error' and message:
_logger.error('Scale Error: '+message) _logger.error('Scale Error: '+ message)
elif status == 'disconnected' and message: elif status == 'disconnected' and message:
_logger.info('Disconnected Scale: %s', message) _logger.info('Disconnected Scale: %s', message)
def _get_raw_response(self, connection): def _get_raw_response(self, connection):
response = "" answer = []
while True: while True:
byte = connection.read(1) char = connection.read(1) # may return `bytes` or `str`
if not char:
if byte: break
response += byte
else: 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): def get_device(self):
try: if self.device:
if not os.path.exists(self.input_dir): return self.device
self.set_status('disconnected','Scale Not Found')
return None with hw_proxy.rs232_lock:
devices = [ device for device in listdir(self.input_dir)] 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: 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 path = self.input_dir + device
for protocol in SCALE_PROTOCOLS:
# don't keep probing devices that are not a scale, _logger.info('Probing %s with protocol %s', path, protocol)
# 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)
connection = serial.Serial(path, connection = serial.Serial(path,
baudrate = 9600, baudrate=protocol.baudrate,
bytesize = serial.SEVENBITS, bytesize=protocol.bytesize,
stopbits = serial.STOPBITS_ONE, stopbits=protocol.stopbits,
parity = serial.PARITY_EVEN, parity=protocol.parity,
timeout = 1, timeout=1, # longer timeouts for probing
writeTimeout = 1) writeTimeout=1) # longer timeouts for probing
connection.write(protocol.weightCommand + protocol.commandTerminator)
connection.write("W") time.sleep(protocol.commandDelay)
self.probed_device_paths.append(path) answer = self._get_raw_response(connection)
weight, weight_info, status = self._parse_weight_answer(protocol, answer)
if self._get_raw_response(connection): if status:
_logger.debug(path + ' is scale') _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.path_to_scale = path
self.set_status('connected','Connected to '+device) self.protocol = protocol
connection.timeout = 0.02 self.set_status(
connection.writeTimeout = 0.02 '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 return connection
else:
_logger.debug('Already probed: ' + path)
self.set_status('disconnected','Scale Not Found') self.set_status('disconnected', 'No supported RS-232 scale found')
return None except Exception as e:
except Exception as e: _logger.exception('Failed probing for scales')
self.set_status('error',str(e)) self.set_status('error', 'Failed probing for scales: %s' % e)
return None return None
def get_weight(self): def get_weight(self):
@@ -123,109 +259,110 @@ class Scale(Thread):
def get_weight_info(self): def get_weight_info(self):
self.lockedstart() self.lockedstart()
return self.weight_info return self.weight_info
def get_status(self): def get_status(self):
self.lockedstart() self.lockedstart()
return self.status return self.status
def read_weight(self): def read_weight(self):
with self.scalelock: with self.scalelock:
if self.device: p = self.protocol
try: try:
self.device.write('W') self.device.write(p.weightCommand + p.commandTerminator)
time.sleep(0.2) time.sleep(p.commandDelay)
answer = [] answer = self._get_raw_response(self.device)
weight, weight_info, status = self._parse_weight_answer(p, answer)
while True: if status:
char = self.device.read(1) self.set_status('error', status)
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))
self.device = None 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): def set_zero(self):
with self.scalelock: with self.scalelock:
if self.device: if self.device:
try: try:
self.device.write('Z') self.device.write(self.protocol.zeroCommand + self.protocol.commandTerminator)
time.sleep(self.protocol.commandDelay)
except Exception as e: 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 self.device = None
def set_tare(self): def set_tare(self):
with self.scalelock: with self.scalelock:
if self.device: if self.device:
try: try:
self.device.write('T') self.device.write(self.protocol.tareCommand + self.protocol.commandTerminator)
time.sleep(self.protocol.commandDelay)
except Exception as e: 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 self.device = None
def clear_tare(self): def clear_tare(self):
with self.scalelock: with self.scalelock:
if self.device: if self.device:
try: p = self.protocol
self.device.write('C') 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: 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 self.device = None
def run(self): def run(self):
self.device = None self.device = None
while True: while True:
if self.device: if self.device:
old_weight = self.weight
self.read_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: else:
with self.scalelock: with self.scalelock:
self.device = self.get_device() self.device = self.get_device()
if not self.device: if not self.device:
time.sleep(5) # retry later to support "plug and play"
time.sleep(10)
scale_thread = None scale_thread = None
if serial: if serial:
scale_thread = Scale() scale_thread = Scale()
hw_proxy.drivers['scale'] = scale_thread hw_proxy.drivers[DRIVER_NAME] = scale_thread
class ScaleDriver(hw_proxy.Proxy): class ScaleDriver(hw_proxy.Proxy):
@http.route('/hw_proxy/scale_read/', type='json', auth='none', cors='*') @http.route('/hw_proxy/scale_read/', type='json', auth='none', cors='*')
def scale_read(self): def scale_read(self):
if scale_thread: 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 return None
@http.route('/hw_proxy/scale_zero/', type='json', auth='none', cors='*') @http.route('/hw_proxy/scale_zero/', type='json', auth='none', cors='*')
@@ -245,5 +382,3 @@ class ScaleDriver(hw_proxy.Proxy):
if scale_thread: if scale_thread:
scale_thread.clear_tare() scale_thread.clear_tare()
return True return True
@@ -13,6 +13,14 @@ file_exists() {
[[ -f $1 ]]; [[ -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)" __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")" __file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
__base="$(basename ${__file} .sh)" __base="$(basename ${__file} .sh)"
@@ -28,6 +36,7 @@ fi
cp -a *raspbian*.img posbox.img cp -a *raspbian*.img posbox.img
CLONE_DIR="${OVERWRITE_FILES_BEFORE_INIT_DIR}/home/pi/odoo" CLONE_DIR="${OVERWRITE_FILES_BEFORE_INIT_DIR}/home/pi/odoo"
rm -rf "${CLONE_DIR}"
mkdir "${CLONE_DIR}" mkdir "${CLONE_DIR}"
git clone -b 8.0 --no-checkout --depth 1 https://github.com/odoo/odoo.git "${CLONE_DIR}" git clone -b 8.0 --no-checkout --depth 1 https://github.com/odoo/odoo.git "${CLONE_DIR}"
cd "${CLONE_DIR}" cd "${CLONE_DIR}"
@@ -44,7 +53,7 @@ cd "${__dir}"
USR_BIN="${OVERWRITE_FILES_BEFORE_INIT_DIR}/usr/bin/" USR_BIN="${OVERWRITE_FILES_BEFORE_INIT_DIR}/usr/bin/"
mkdir -p "${USR_BIN}" mkdir -p "${USR_BIN}"
cd "/tmp" 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 unzip ngrok.zip
rm ngrok.zip rm ngrok.zip
cd "${__dir}" cd "${__dir}"
@@ -68,6 +77,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=$(kpartx -av posbox.img | tail -n 1 | cut -d ' ' -f 3)
LOOP_MAPPER_PATH="/dev/mapper/${LOOP_MAPPER_PATH}" LOOP_MAPPER_PATH="/dev/mapper/${LOOP_MAPPER_PATH}"
sleep 5
# resize filesystem # resize filesystem
e2fsck -f "${LOOP_MAPPER_PATH}" # resize2fs requires clean fs e2fsck -f "${LOOP_MAPPER_PATH}" # resize2fs requires clean fs
@@ -2,4 +2,4 @@
wget 'https://downloads.raspberrypi.org/raspbian_lite_latest' -O raspbian.img.zip wget 'https://downloads.raspberrypi.org/raspbian_lite_latest' -O raspbian.img.zip
unzip 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
+1 -1
View File
@@ -52,7 +52,7 @@ class SaleOrder(models.Model):
refund is not directly linked to the SO. refund is not directly linked to the SO.
""" """
for order in self: 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 # Search for invoices which have been 'cancelled' (filter_refund = 'modify' in
# 'account.invoice.refund') # 'account.invoice.refund')
# use like as origin may contains multiple references (e.g. 'SO01, SO02') # use like as origin may contains multiple references (e.g. 'SO01, SO02')
+1 -1
View File
@@ -718,7 +718,7 @@ class Picking(models.Model):
# check if the quant is matching the operation details # check if the quant is matching the operation details
if ops.package_id: if ops.package_id:
flag = quant.package_id and bool(QuantPackage.search([('id', 'child_of', [ops.package_id.id])])) or False flag = quant.package_id == ops.package_id
else: else:
flag = not quant.package_id.id flag = not quant.package_id.id
flag = flag and (ops.owner_id.id == quant.owner_id.id) flag = flag and (ops.owner_id.id == quant.owner_id.id)
+1
View File
@@ -37,6 +37,7 @@ class TestStockCommon(common.TransactionCase):
self.productB = self.ProductObj.create({'name': 'Product B', 'type': 'product'}) self.productB = self.ProductObj.create({'name': 'Product B', 'type': 'product'})
self.productC = self.ProductObj.create({'name': 'Product C', 'type': 'product'}) self.productC = self.ProductObj.create({'name': 'Product C', 'type': 'product'})
self.productD = self.ProductObj.create({'name': 'Product D', 'type': 'product'}) self.productD = self.ProductObj.create({'name': 'Product D', 'type': 'product'})
self.productE = self.ProductObj.create({'name': 'Product E', 'type': 'product'})
# Configure unit of measure. # Configure unit of measure.
self.uom_kg = self.UomObj.create({ self.uom_kg = self.UomObj.create({
+126
View File
@@ -1368,3 +1368,129 @@ class TestStockFlow(TestStockCommon):
self.assertEqual(quant.qty, 3.0, 'The quant in customer location for productA has not a quantity of 3.0') self.assertEqual(quant.qty, 3.0, 'The quant in customer location for productA has not a quantity of 3.0')
# Check that the parent package of the quant is the picking_in_package # Check that the parent package of the quant is the picking_in_package
self.assertEqual(quant.package_id.id, picking_in_package.id, 'The quant in customer location is not in its package created in picking in') self.assertEqual(quant.package_id.id, picking_in_package.id, 'The quant in customer location is not in its package created in picking in')
def test_50_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,
'location_id': self.supplier_location,
'location_dest_id': self.stock_location})
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.action_assign()
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,
'location_id': self.stock_location,
'location_dest_id': self.customer_location})
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()
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_60_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,
'location_id': self.supplier_location,
'location_dest_id': self.stock_location})
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.action_assign()
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,
'location_id': self.stock_location,
'location_dest_id': self.customer_location})
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()
#Convert entire packs into taking out of packs
packout0 = picking_out.pack_operation_ids[0]
packout1 = picking_out.pack_operation_ids[1]
packout0.write({
'package_id': pack1.id,
'product_id': self.productE.id,
'product_qty': 120.0,
'product_uom_id': self.productE.uom_id.id,
})
packout1.write({
'package_id': pack2.id,
'product_id': self.productE.id,
'product_qty': 80.0,
'product_uom_id': self.productE.uom_id.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')
@@ -98,7 +98,8 @@ return Widget.extend({
"there is no active filter in the search bar."), "there is no active filter in the search bar."),
})); }));
} else { } else {
this['display_' + this.mode](); var chart = this['display_' + this.mode]();
chart.tooltip.chartContainer(this.$el[0]);
} }
}, },
display_bar: function () { display_bar: function () {
@@ -182,6 +183,8 @@ return Widget.extend({
chart(svg); chart(svg);
this.to_remove = chart.update; this.to_remove = chart.update;
nv.utils.onWindowResize(chart.update); nv.utils.onWindowResize(chart.update);
return chart;
}, },
display_pie: function () { display_pie: function () {
var data = [], var data = [],
@@ -232,6 +235,8 @@ return Widget.extend({
chart(svg); chart(svg);
this.to_remove = chart.update; this.to_remove = chart.update;
nv.utils.onWindowResize(chart.update); nv.utils.onWindowResize(chart.update);
return chart;
}, },
display_line: function () { display_line: function () {
if (this.data.length < 2) { if (this.data.length < 2) {
@@ -309,7 +314,9 @@ return Widget.extend({
chart(svg); chart(svg);
this.to_remove = chart.update; this.to_remove = chart.update;
nv.utils.onWindowResize(chart.update); nv.utils.onWindowResize(chart.update);
return chart;
}, },
destroy: function () { destroy: function () {
nv.utils.offWindowResize(this.to_remove); nv.utils.offWindowResize(this.to_remove);
+5 -1
View File
@@ -10,9 +10,13 @@
} }
} }
.nvtooltip thead {
background-color: transparent;
}
// ------------------------------------------------------------------ // ------------------------------------------------------------------
// Control panel buttons // Control panel buttons
// ------------------------------------------------------------------ // ------------------------------------------------------------------
.o_graph_measures_list { .o_graph_measures_list {
.o-selected-li; .o-selected-li;
} }
@@ -161,6 +161,11 @@ var FieldTextHtmlSimple = widget.extend({
return !this.get('value') || this.get('value') === "<p><br/></p>" || !this.get('value').match(/\S/); return !this.get('value') || this.get('value') === "<p><br/></p>" || !this.get('value').match(/\S/);
}, },
commit_value: function () { 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']) { if (this.options['style-inline']) {
transcoder.class_to_style(this.$content); transcoder.class_to_style(this.$content);
transcoder.font_to_img(this.$content); transcoder.font_to_img(this.$content);
@@ -374,6 +379,11 @@ var FieldTextHtml = widget.extend({
this._dirty_flag = false; this._dirty_flag = false;
return this.editor.save(); return this.editor.save();
} else if (this._dirty_flag && this.editor && this.editor.buildingBlock) { } 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.editor.buildingBlock.clean_for_save();
this.internal_set_value( this.$content.html() ); this.internal_set_value( this.$content.html() );
} }
@@ -539,6 +539,8 @@ function prettify_html(html) {
} }
return result; return result;
} }
// Change Warning: called with dummy arguments to quit code view before saving
$.summernote.pluginEvents.codeview = function (event, editor, layoutInfo) { $.summernote.pluginEvents.codeview = function (event, editor, layoutInfo) {
if (layoutInfo.toolbar) { if (layoutInfo.toolbar) {
return eventHandler.modules.codeview.toggle(layoutInfo); return eventHandler.modules.codeview.toggle(layoutInfo);
@@ -335,9 +335,9 @@ $('.oe_website_sale').each(function () {
$('input.js_variant_change, select.js_variant_change', this).first().trigger('change'); $('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 () { $(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(); var selected_state = select.val();
state_options.detach(); state_options.detach();
var displayed_state = state_options.filter("[data-country_id="+($(this).val() || 0)+"]"); var displayed_state = state_options.filter("[data-country_id="+($(this).val() || 0)+"]");