[MERGE] forward port branch saas-11 up to 3b2f4e2

This commit is contained in:
Christophe Simonis
2016-11-17 14:48:43 +01:00
24 changed files with 516 additions and 158 deletions
+1
View File
@@ -161,6 +161,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),))
+1 -1
View File
@@ -283,7 +283,7 @@
<field name="code"/>
<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="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>
<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__)
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='*')
+6 -2
View File
@@ -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
+18 -1
View File
@@ -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)
@@ -37,10 +37,10 @@ index_template = """
<body>
<h1>Your PosBox is up and running</h1>
<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
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>
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>.
</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 software on the <a href='/hw_proxy/upgrade/'>upgrade page</a>.
</p>
+21 -1
View File
@@ -11,6 +11,8 @@ import subprocess
import json
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):
@@ -47,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 = """
<!DOCTYPE HTML>
<html>
@@ -95,6 +102,8 @@ class Proxy(http.Controller):
<h2>Connected Devices</h2>
<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')
count = 0
resp += "<div class='devices'>\n"
@@ -110,6 +119,17 @@ class Proxy(http.Controller):
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,{
'Cache-Control': 'no-cache',
'Content-Type': 'text/html; charset=utf-8',
+250 -115
View File
@@ -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.info('Disconnected Scale: %s', 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
@@ -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)"
@@ -28,6 +36,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 +53,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 +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="/dev/mapper/${LOOP_MAPPER_PATH}"
sleep 5
# resize filesystem
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
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
@@ -54,7 +54,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')
+1 -1
View File
@@ -725,7 +725,7 @@ class Picking(models.Model):
# check if the quant is matching the operation details
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:
flag = not quant.package_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.productC = self.ProductObj.create({'name': 'Product C', '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.
self.uom_kg = self.UomObj.create({
+126
View File
@@ -1410,3 +1410,129 @@ class TestStockFlow(TestStockCommon):
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
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."),
}));
} else {
this['display_' + this.mode]();
var chart = this['display_' + this.mode]();
chart.tooltip.chartContainer(this.$el[0]);
}
},
display_bar: function () {
@@ -182,6 +183,8 @@ return Widget.extend({
chart(svg);
this.to_remove = chart.update;
nv.utils.onWindowResize(chart.update);
return chart;
},
display_pie: function () {
var data = [],
@@ -232,6 +235,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) {
@@ -309,7 +314,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);
+5 -2
View File
@@ -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
+5 -1
View File
@@ -10,9 +10,13 @@
}
}
.nvtooltip thead {
background-color: transparent;
}
// ------------------------------------------------------------------
// Control panel buttons
// ------------------------------------------------------------------
.o_graph_measures_list {
.o-selected-li;
}
}
@@ -911,9 +911,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 {
@@ -927,7 +927,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() {
@@ -161,6 +161,11 @@ var FieldTextHtmlSimple = widget.extend({
return !this.get('value') || this.get('value') === "<p><br/></p>" || !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() );
}
@@ -538,6 +538,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);
@@ -517,7 +517,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);
}
@@ -349,7 +349,7 @@ odoo.define('website_sale.website_sale', function (require) {
//$("input[name='phone']").attr('placeholder', data.phone_code !== 0 ? '+'+ data.phone_code : '');
// populate states and display
var selectStates = $("select[name='state_id']");
var selectStates = $("select[name='state_id']:visible");
// dont reload state at first loading (done in qweb)
if (selectStates.data('init')===1) {
selectStates.data('init', 0);
+7
View File
@@ -325,6 +325,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 """
+4 -3
View File
@@ -1729,7 +1729,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:
@@ -1803,8 +1804,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 ())