[MERGE] forward port branch saas-11.3 up to 0c42dcfc22

This commit is contained in:
Christophe Simonis
2018-07-19 15:36:54 +02:00
49 changed files with 640 additions and 149 deletions
@@ -519,7 +519,6 @@ var StatementModel = BasicModel.extend({
},
/**
* Force the partial reconciliation to display the reconciliate button.
* This method should only be called when there is onely one proposition.
*
* @param {string} handle
* @returns {Deferred}
@@ -529,14 +528,17 @@ var StatementModel = BasicModel.extend({
// Retrieve the toggle proposition
var selected;
_.each(line.reconciliation_proposition, function (prop) {
var targetLineAmount = line.st_line.amount;
line.reconciliation_proposition.every(function (prop) {
if (!prop.invalid) {
if (((line.balance.amount < 0 || !line.partial_reconcile) && prop.amount > 0 && line.st_line.amount > 0 && line.st_line.amount < prop.amount) ||
((line.balance.amount > 0 || !line.partial_reconcile) && prop.amount < 0 && line.st_line.amount < 0 && line.st_line.amount > prop.amount)) {
if (((line.balance.amount < 0 || !line.partial_reconcile) && prop.amount > 0 && targetLineAmount > 0 && targetLineAmount < prop.amount) ||
((line.balance.amount > 0 || !line.partial_reconcile) && prop.amount < 0 && targetLineAmount < 0 && targetLineAmount > prop.amount)) {
selected = prop;
return false;
}
targetLineAmount -= prop.amount;
}
return true;
});
// If no toggled proposition found, reject it
@@ -376,18 +376,21 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
// loop state propositions
var props = [];
var nb_debit_props = 0;
var nb_credit_props = 0;
var partialDebitProps = 0;
var partialCreditProps = 0;
_.each(state.reconciliation_proposition, function (prop) {
if (prop.display) {
props.push(prop);
if (prop.amount < 0)
nb_debit_props += 1;
else if (prop.amount > 0)
nb_credit_props += 1;
if (prop.amount > 0 && prop.amount > state.st_line.amount) {
partialDebitProps++;
} else if (prop.amount < 0 && prop.amount < state.st_line.amount) {
partialCreditProps++;
}
}
});
var targetLineAmount = state.st_line.amount;
_.each(props, function (line) {
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state}));
if (!isNaN(line.id)) {
@@ -396,10 +399,10 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
.attr("data-content", qweb.render('reconciliation.line.mv_line.details', {'line': line}));
}
if (line.already_paid === false &&
((state.balance.amount_currency < 0 || line.partial_reconcile) && nb_credit_props == 1
&& line.amount > 0 && state.st_line.amount > 0 && state.st_line.amount < line.amount) ||
((state.balance.amount_currency > 0 || line.partial_reconcile) && nb_debit_props == 1
&& line.amount < 0 && state.st_line.amount < 0 && state.st_line.amount > line.amount)) {
((state.balance.amount_currency < 0 || line.partial_reconcile)
&& line.amount > 0 && state.st_line.amount > 0 && targetLineAmount < line.amount && partialDebitProps <= 1) ||
((state.balance.amount_currency > 0 || line.partial_reconcile)
&& line.amount < 0 && state.st_line.amount < 0 && targetLineAmount > line.amount && partialCreditProps <= 1)) {
var $cell = $line.find(line.amount > 0 ? '.cell_right' : '.cell_left');
var text;
if (line.partial_reconcile) {
@@ -413,6 +416,8 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
.prependTo($cell)
.attr("data-content", text);
}
targetLineAmount -= line.amount;
$props.append($line);
});
@@ -1,6 +1,14 @@
odoo.define('account.reconciliation_tests.data', function () {
"use strict";
/*
* Debug tip:
* To be able to "see" the test in the browser:
* var $body = $('body');
* $body.addClass('debug');
* clientAction.appendTo($body);
*/
var Datas = {};
var db = {
@@ -1518,5 +1526,199 @@ QUnit.module('account', {
clientAction.destroy();
});
QUnit.test('Reconciliation: Payment < inv1 + inv2(partial)', function (assert) {
assert.expect(4);
/*
* One payment: $1175
* Two Invoices
* The first invoice will be fully reconciled $650
* The second invoice will be partially paid with the rest of the payment $999
*/
// modify the second line that is already in db to put it at $999
var indexModif = _.findIndex(this.params.mv_lines['[5,"",0,6]'], function (line) {return line.id === 112});
this.params.mv_lines['[5,"",0,6]'][indexModif] =
{account_type: "receivable", amount_currency_str: "", currency_id: false, date_maturity: "2017-02-07", date: "2017-01-08",
total_amount_str: "$ 999.00", partner_id: 8, account_name: "101200 Account Receivable", name: "INV/2017/0003",
partner_name: "Agrolait", total_amount_currency_str: "", id: 112, credit: 0.0, journal_id: [1, "Customer Invoices"],
amount_str: "$ 999.00", debit: 999.0, account_code: "101200", ref: "", already_paid: false};
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
data: this.params.data,
mockRPC: function (route, args) {
if (args.method === 'process_bank_statement_line') {
assert.deepEqual(args.args,
[
[5], // Id of the bank statement line
[{counterpart_aml_dicts:
[{name:"INV/2017/0002",
debit: 0,
credit: 650,
analytic_tag_ids: [[6, null, []]],
counterpart_aml_id: 109},
{name: "INV/2017/0003",
debit: 0,
credit: 525,
analytic_tag_ids: [[6, null, []]],
counterpart_aml_id: 112}],
payment_aml_ids: [],
partner_id: 8,
new_aml_dicts: []}]
], "should call process_bank_statement_line with partial reconcile values");
}
return this._super(route, args);
},
session: {
currencies: {
3: {
digits: [69, 2],
position: "before",
symbol: "$"
}
}
},
});
clientAction.appendTo($('#qunit-fixture'));
// The first reconciliation "line" is where it happens
var widget = clientAction.widgets[0];
// Add first invoice to reconcile fully
widget.$('.match .cell_account_code:first').trigger('click');
assert.notOk( widget.$('.cell_right .line_info_button').length,
"should not display the partial reconciliation alert");
// Add second invoice to reconcile partially
widget.$('.match .cell_account_code:first').trigger('click');
var $reconciliationAlert = widget.$('.cell_right .line_info_button');
assert.ok($reconciliationAlert.length,
"should display the partial reconciliation alert");
$reconciliationAlert.click();
var $buttonReconcile = widget.$('button.o_reconcile:not(hidden)');
assert.equal($buttonReconcile.length, 1,
'The reconcile button must be visible');
$buttonReconcile.click();
clientAction.destroy();
});
QUnit.test('Reconciliation: payment and 2 partials', function (assert) {
assert.expect(6);
/*
* One payment: $1175
* Two Invoices as Inv1 = 1200; Inv2 = 1200:
* Payment < Inv1 AND Payment < Inv2
* No partial reconcile is possible, as a write-off of 1225 is necessary
*/
// modify the invoice line to have their amount > payment
var indexInv1 = _.findIndex(this.params.mv_lines['[5,"",0,6]'], function (line) {return line.id === 109});
this.params.mv_lines['[5,"",0,6]'][indexInv1] =
{account_type: "receivable", amount_currency_str: "", currency_id: false, date_maturity: "2017-02-07", date: "2017-01-08",
total_amount_str: "$ 1200.00", partner_id: 8, account_name: "101200 Account Receivable", name: "INV/2017/0002", partner_name: "Agrolait",
total_amount_currency_str: "", id: 109, credit: 0.0, journal_id: [1, "Customer Invoices"], amount_str: "$ 1200.00", debit: 1200.0,
account_code: "101200", ref: "", already_paid: false};
var indexInv2 = _.findIndex(this.params.mv_lines['[5,"",0,6]'], function (line) {return line.id === 112});
this.params.mv_lines['[5,"",0,6]'][indexInv2] =
{account_type: "receivable", amount_currency_str: "", currency_id: false, date_maturity: "2017-02-07", date: "2017-01-08",
total_amount_str: "$ 1200.00", partner_id: 8, account_name: "101200 Account Receivable", name: "INV/2017/0003",
partner_name: "Agrolait", total_amount_currency_str: "", id: 112, credit: 0.0, journal_id: [1, "Customer Invoices"],
amount_str: "$ 1200.00", debit: 1200.0, account_code: "101200", ref: "", already_paid: false};
var clientAction = new ReconciliationClientAction.StatementAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
data: this.params.data,
mockRPC: function (route, args) {
if (args.method === 'process_bank_statement_line') {
assert.deepEqual(args.args,
[
[5], // Id of the bank statement line
[{counterpart_aml_dicts:
[{name:"INV/2017/0002",
debit: 0,
credit: 1200,
analytic_tag_ids: [[6, null, []]],
counterpart_aml_id: 109},
{name: "INV/2017/0003",
debit: 0,
credit: 1200,
analytic_tag_ids: [[6, null, []]],
counterpart_aml_id: 112}],
payment_aml_ids: [],
partner_id: 8,
new_aml_dicts: [
{account_id: 282,
credit: 0,
debit: 1225,
analytic_tag_ids: [[6, null, []]],
name: 'SAJ/2014/002 and SAJ/2014/003',
}
]}]
], "should call process_bank_statement_line with new aml dict reconcile values");
}
return this._super(route, args);
},
session: {
currencies: {
3: {
digits: [69, 2],
position: "before",
symbol: "$"
}
}
},
});
clientAction.appendTo($('#qunit-fixture'));
// The first reconciliation "line" is where it happens
var widget = clientAction.widgets[0];
// Add first invoice
// There should be the opportunity to reconcile partially
widget.$('.match .cell_account_code:first').trigger('click');
assert.ok(widget.$('.cell_right .line_info_button').length,
"should display the partial reconciliation alert");
// Add second invoice
widget.$('.match .cell_account_code:first').trigger('click');
assert.notOk(widget.$('.cell_right .line_info_button').length,
"should not display the partial reconciliation alert");
var writeOffCreate = widget.$('div.create');
assert.equal(writeOffCreate.length, 1,
'A write-off creation should be present');
assert.equal(writeOffCreate.find('input[name=amount]').val(), -1225,
'The right amount should be proposed for the write-off');
writeOffCreate.find('.create_account_id input.ui-autocomplete-input').click();
$('ul.ui-autocomplete li a:first').click(); // select first account to do the write off in
var $buttonReconcile = widget.$('button.o_reconcile:not(hidden)');
assert.equal($buttonReconcile.length, 1,
'The reconcile button must be visible');
$buttonReconcile.click();
clientAction.destroy();
});
});
});
+8 -1
View File
@@ -794,12 +794,19 @@ class TestReconciliation(AccountingTestCase):
statement = self.make_payment(invoice, journal, 50)
# The report searches on the create_date to dispatch reconciled lines to report periods
# Also, in this case, there can be only 1 partial_reconcile
statement_partial_id = statement.move_line_ids.mapped(lambda l: l.matched_credit_ids + l.matched_debit_ids)
self.env.cr.execute('UPDATE account_partial_reconcile SET create_date = %(date)s WHERE id = %(partial_id)s',
{'date': report_date_to + ' 00:00:00',
'partial_id': statement_partial_id.id})
# Case 1: The invoice and payment are reconciled: Nothing should appear
report_lines, total, amls = AgedReport._get_partner_move_lines(account_type, report_date_to, 'posted', 30)
partner_lines = [line for line in report_lines if line['partner_id'] == partner.id]
self.assertEqual(partner_lines, [], 'The aged receivable shouldn\'t have lines at this point')
self.assertFalse(partner.id in amls, 'The aged receivable should not have amls either')
self.assertFalse(amls.get(partner.id, False), 'The aged receivable should not have amls either')
# Case 2: The invoice and payment are not reconciled: we should have one line on the report
# and 2 amls
+18 -1
View File
@@ -109,7 +109,7 @@ var BoardController = FormController.extend({
this.do_action({
type: 'ir.actions.act_window',
res_model: event.data.model,
views: [[false, 'form']],
views: [[event.data.formViewID || false, 'form']],
res_id: event.data.res_id,
});
},
@@ -120,6 +120,7 @@ var BoardRenderer = FormRenderer.extend({
do_action: '_onDoAction',
env_updated: '_onEnvUpdated',
update_filters: '_onUpdateFilters',
switch_view: '_onSwitchView',
}),
events: _.extend({}, FormRenderer.prototype.events, {
'click .oe_dashboard_column .oe_fold': '_onFoldClick',
@@ -135,6 +136,7 @@ var BoardRenderer = FormRenderer.extend({
this.noContentHelp = params.noContentHelp;
this.actionsDescr = {};
this._boardSubcontrollers = []; // for board: controllers of subviews
this._boardFormViewIDs = {}; // for board: mapping subview controller to form view id
},
/**
* Call `on_attach_callback` for each subview
@@ -260,6 +262,11 @@ var BoardRenderer = FormRenderer.extend({
hasSelectors: false,
});
return view.getController(self).then(function (controller) {
self._boardFormViewIDs[controller.handle] = _.first(
_.find(action.views, function (descr) {
return descr[1] === 'form';
})
);
self._boardSubcontrollers.push(controller);
return controller.appendTo(params.$node);
});
@@ -401,6 +408,16 @@ var BoardRenderer = FormRenderer.extend({
$action.find('.oe_content').toggle();
this.trigger_up('save_dashboard');
},
/**
* Let FormController know which form view it should display based on the
* window action of the sub controller that is switching view
*
* @private
* @param {OdooEvent} event
*/
_onSwitchView: function (event) {
event.data.formViewID = this._boardFormViewIDs[event.target.handle];
},
/**
* Stops the propagation of 'update_filters' events triggered by the
* controllers instantiated by the dashboard to prevent them from
@@ -275,6 +275,51 @@ QUnit.test('can open a record', function (assert) {
form.destroy();
});
QUnit.test('can open record using action form view', function (assert) {
assert.expect(1);
var form = createView({
View: BoardView,
model: 'board',
data: this.data,
arch: '<form string="My Dashboard">' +
'<board style="2-1">' +
'<column>' +
'<action context="{}" view_mode="list" string="ABC" name="51" domain="[]"></action>' +
'</column>' +
'</board>' +
'</form>',
mockRPC: function (route) {
if (route === '/web/action/load') {
return $.when({
res_model: 'partner',
views: [[4, 'list'], [5, 'form']],
});
}
return this._super.apply(this, arguments);
},
archs: {
'partner,4,list':
'<tree string="Partner"><field name="foo"/></tree>',
'partner,5,form':
'<form string="Partner"><field name="display_name"/></form>',
},
intercepts: {
do_action: function (event) {
assert.deepEqual(event.data.action, {
res_id: 1,
res_model: 'partner',
type: 'ir.actions.act_window',
views: [[5, 'form']],
}, "should do a do_action with correct parameters");
},
},
});
form.$('tr.o_data_row td:contains(yop)').click();
form.destroy();
});
QUnit.test('can drag and drop a view', function (assert) {
assert.expect(4);
+19 -7
View File
@@ -8,6 +8,7 @@ import os
import os.path
import subprocess
import time
import netifaces as ni
import traceback
try:
@@ -29,9 +30,10 @@ except ImportError:
usb = None
from odoo import http, _
from odoo.addons.hw_proxy.controllers import main as hw_proxy
from uuid import getnode as get_mac
_logger = logging.getLogger(__name__)
# workaround https://bugs.launchpad.net/openobject-server/+bug/947231
@@ -196,10 +198,18 @@ class EscposDriver(Thread):
def print_status(self,eprint):
localips = ['0.0.0.0','127.0.0.1','127.0.1.1']
hosting_ap = os.system('pgrep hostapd') == 0
ssid = subprocess.check_output('iwconfig 2>&1 | grep \'ESSID:"\' | sed \'s/.*"\\(.*\\)"/\\1/\'', shell=True).rstrip()
mac = subprocess.check_output('ifconfig | grep -B 1 \'inet addr\' | grep -o \'HWaddr .*\' | sed \'s/HWaddr //\'', shell=True).rstrip()
ips = [ c.split(':')[1].split(' ')[0] for c in subprocess.check_output("/sbin/ifconfig").split('\n') if 'inet addr' in c ]
ips = [ ip for ip in ips if ip not in localips ]
ssid = subprocess.check_output('iwconfig 2>&1 | grep \'ESSID:"\' | sed \'s/.*"\\(.*\\)"/\\1/\'', shell=True).decode('utf-8').rstrip()
mac = get_mac()
h = iter(hex(mac)[2:].zfill(12))
mac = ":".join(i + next(h) for i in h)
interfaces = ni.interfaces()
ips = []
for iface_id in interfaces:
iface_obj = ni.ifaddresses(iface_id)
ifconfigs = iface_obj.get(ni.AF_INET, [])
for conf in ifconfigs:
if conf.get('addr'):
ips.append(conf.get('addr'))
eprint.text('\n\n')
eprint.set(align='center',type='b',height=2,width=2)
eprint.text('PosBox Status\n')
@@ -221,8 +231,10 @@ class EscposDriver(Thread):
eprint.text(ip+'\n')
if len(ips) >= 1:
eprint.text('\nMAC Address:\n' + mac + '\n')
eprint.text('\nHomepage:\nhttp://'+ips[0]+':8069\n')
ips_filtered = [i for i in ips if i != '127.0.0.1']
main_ips = ips_filtered and ips_filtered[0] or '127.0.0.1'
eprint.text('\nMAC Address:\n' + str(mac) + '\n')
eprint.text('\nHomepage:\nhttp://' + main_ips + ':8069\n')
eprint.text('\n\n')
eprint.cut()
+18 -17
View File
@@ -7,6 +7,7 @@ import io
import math
import re
import traceback
import codecs
from hashlib import md5
from PIL import Image
@@ -318,9 +319,9 @@ class Escpos:
else:
image_border = 32 - (size % 32)
if (image_border % 2) == 0:
return (image_border / 2, image_border / 2)
return (int(image_border / 2), int(image_border / 2))
else:
return (image_border / 2, (image_border / 2) + 1)
return (int(image_border / 2), int((image_border / 2) + 1))
def _print_image(self, line, size):
""" Print formatted image """
@@ -330,8 +331,8 @@ class Escpos:
self._raw(S_RASTER_N)
buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0)
self._raw(buffer.decode('hex'))
buffer = b"%02X%02X%02X%02X" % (int((size[0]/size[1])/8), 0, size[1], 0)
self._raw(codecs.decode(buffer, 'hex'))
buffer = ""
while i < len(line):
@@ -340,7 +341,7 @@ class Escpos:
i += 8
cont += 1
if cont % 4 == 0:
self._raw(buffer.decode("hex"))
self._raw(codecs.decode(buffer, "hex"))
buffer = ""
cont = 0
@@ -349,7 +350,7 @@ class Escpos:
i = 0
cont = 0
buffer = ""
raw = ""
raw = b""
def __raw(string):
if output:
@@ -358,8 +359,8 @@ class Escpos:
self._raw(string)
raw += S_RASTER_N
buffer = "%02X%02X%02X%02X" % (((size[0]/size[1])/8), 0, size[1], 0)
raw += buffer.decode('hex')
buffer = "%02X%02X%02X%02X" % (int((size[0]/size[1])/8), 0, size[1], 0)
raw += codecs.decode(buffer, 'hex')
buffer = ""
while i < len(line):
@@ -368,7 +369,7 @@ class Escpos:
i += 8
cont += 1
if cont % 4 == 0:
raw += buffer.decode("hex")
raw += codecs.decode(buffer, 'hex')
buffer = ""
cont = 0
@@ -433,14 +434,14 @@ class Escpos:
print('print_b64_img')
id = md5(img.encode('utf-8')).digest()
id = md5(img).digest()
if id not in self.img_cache:
print('not in cache')
img = img[img.find(',')+1:]
f = io.BytesIO('img')
f.write(base64.decodestring(img))
img = img[img.find(b',')+1:]
f = io.BytesIO(b'img')
f.write(base64.decodebytes(img))
f.seek(0)
img_rgba = Image.open(f)
img = Image.new('RGB', img_rgba.size, (255,255,255))
@@ -677,7 +678,7 @@ class Escpos:
elif elem.tag == 'img':
if 'src' in elem.attrib and 'data:' in elem.attrib['src']:
self.print_base64_image(elem.attrib['src'])
self.print_base64_image(bytes(elem.attrib['src'], 'utf-8'))
elif elem.tag == 'barcode' and 'encoding' in elem.attrib:
serializer.start_block(stylestack)
@@ -797,19 +798,19 @@ class Escpos:
(encoding, _) = remaining.popitem()
else:
encoding = 'cp437'
encoded = '\xb1' # could not encode, output error character
encoded = b'\xb1' # could not encode, output error character
break;
if encoding != self.encoding:
# if the encoding changed, remember it and prefix the character with
# the esc-pos encoding change sequence
self.encoding = encoding
encoded = encodings[encoding] + encoded
encoded = bytes(encodings[encoding], 'utf-8') + encoded
return encoded
def encode_str(txt):
buffer = ''
buffer = b''
for c in txt:
buffer += encode_char(c)
return buffer
@@ -58,7 +58,7 @@ index_template = """
</p>
%s
<p>
The PosBox software installed on this posbox is <b>version 16</b>,
The PosBox software installed on this posbox is <b>version 17 BETA</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>
@@ -161,7 +161,7 @@ class PosboxHomepage(odoo.addons.web.controllers.main.Home):
"""
return wifi_template
@http.route('/wifi_connect', type='http', auth='none', cors='*')
@http.route('/wifi_connect', type='http', auth='none', cors='*', csrf=False)
def connect_to_wifi(self, essid, password, persistent=False):
if persistent:
persistent = "1"
@@ -171,7 +171,7 @@ class PosboxHomepage(odoo.addons.web.controllers.main.Home):
subprocess.call(['/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/connect_to_wifi.sh', essid, password, persistent])
return "connecting to " + essid
@http.route('/wifi_clear', type='http', auth='none', cors='*')
@http.route('/wifi_clear', type='http', auth='none', cors='*', csrf=False)
def clear_wifi_configuration(self):
os.system('/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/clear_wifi_configuration.sh')
return "configuration cleared"
@@ -244,7 +244,7 @@ class PosboxHomepage(odoo.addons.web.controllers.main.Home):
"""
return ngrok_template
@http.route('/enable_ngrok', type='http', auth='none', cors='*')
@http.route('/enable_ngrok', type='http', auth='none', cors='*', csrf=False)
def enable_ngrok(self, auth_token):
if subprocess.call(['pgrep', 'ngrok']) == 1:
subprocess.Popen(['ngrok', 'tcp', '-authtoken', auth_token, '-log', '/tmp/ngrok.log', '22'])
+1 -1
View File
@@ -80,7 +80,7 @@ upgrade_template = """
</p>
<pre>
"""
upgrade_template += subprocess.check_output("git --work-tree=/home/pi/odoo/ --git-dir=/home/pi/odoo/.git log -1", shell=True).replace("\n", "<br/>")
upgrade_template += subprocess.check_output("git --work-tree=/home/pi/odoo/ --git-dir=/home/pi/odoo/.git log -1", shell=True).decode('utf-8').replace("\n", "<br/>")
upgrade_template += """
</pre>
<div class='centering'>
+9 -6
View File
@@ -21,6 +21,8 @@ BANNED_DEVICES = {
"0424:9514", # Standard Microsystem Corp. Builtin Ethernet module
"1d6b:0002", # Linux Foundation 2.0 root hub
"0424:ec00", # Standard Microsystem Corp. Other Builtin Ethernet module
"0424:2514", # Standard Microsystems Corp. USB 2.0 Hub (rpi3b+)
"0424:7800", # Standard Microsystems Corp. (rpi3b+)
}
@@ -100,15 +102,16 @@ class Proxy(http.Controller):
"""
if debug is None:
resp += """(<a href="/hw_proxy/status?debug">debug version</a>)"""
devices = subprocess.check_output("lsusb").split('\n')
devices = subprocess.check_output("lsusb").decode('utf-8').split('\n')
count = 0
resp += "<div class='devices'>\n"
for device in devices:
device_name = device[device.find('ID')+2:]
device_id = device_name.split()[0]
if not (device_id in BANNED_DEVICES):
resp += "<div class='device' data-device='"+device+"'>"+device_name+"</div>\n"
count += 1
if device_name: # to avoid last empty line
device_id = device_name.split()[0]
if not (device_id in BANNED_DEVICES):
resp += "<div class='device' data-device='"+device+"'>"+device_name+"</div>\n"
count += 1
if count == 0:
resp += "<div class='device'>No USB Device Found</div>"
@@ -124,7 +127,7 @@ class Proxy(http.Controller):
%s
</pre>
""" % subprocess.check_output('lsusb -v', shell=True)
""" % subprocess.check_output('lsusb -v', shell=True).decode('utf-8')
return request.make_response(resp,{
'Cache-Control': 'no-cache',
+14 -14
View File
@@ -28,7 +28,7 @@ except ImportError:
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])
stat = status[status.index(b'?') + 1]
if stat == 0:
weight_info = 'ok'
else:
@@ -68,17 +68,17 @@ Toledo8217Protocol = ScaleProtocol(
parity=serial.PARITY_EVEN,
timeout=1,
writeTimeout=1,
weightRegexp="\x02\\s*([0-9.]+)N?\\r",
statusRegexp="\x02\\s*(\\?.)\\r",
weightRegexp=b"\x02\\s*([0-9.]+)N?\\r",
statusRegexp=b"\x02\\s*(\\?.)\\r",
statusParse=_toledo8217StatusParse,
commandDelay=0.2,
weightDelay=0.5,
newWeightDelay=0.2,
commandTerminator='',
weightCommand='W',
zeroCommand='Z',
tareCommand='T',
clearCommand='C',
commandTerminator=b'',
weightCommand=b'W',
zeroCommand=b'Z',
tareCommand=b'T',
clearCommand=b'C',
emptyAnswerValid=False,
autoResetWeight=False,
)
@@ -98,15 +98,15 @@ ADAMEquipmentProtocol = ScaleProtocol(
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",
commandTerminator=b"\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',
weightCommand=b'P',
zeroCommand=b'Z',
tareCommand=b'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
@@ -165,8 +165,8 @@ class Scale(Thread):
if not char:
break
else:
answer.append(char)
return ''.join(answer)
answer.append(bytes(char))
return b''.join(answer)
def _parse_weight_answer(self, protocol, answer):
""" Parse a scale's answer to a weighing request, returning
@@ -1251,6 +1251,7 @@
<field name="name">Prêts participatifs</field>
<field name="code">2741</field>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -1258,6 +1259,7 @@
<field name="name">Prêts aux associés</field>
<field name="code">2742</field>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -1265,6 +1267,7 @@
<field name="name">Prêts au personnel</field>
<field name="code">2743</field>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -1272,6 +1275,7 @@
<field name="name">Autres prêts</field>
<field name="code">2748</field>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -1279,6 +1283,7 @@
<field name="name">Dépôts</field>
<field name="code">2751</field>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -1286,6 +1291,7 @@
<field name="name">Cautionnements</field>
<field name="code">2755</field>
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
<field name="reconcile" eval="True"/>
<field name="chart_template_id" ref="l10n_fr_pcg_chart_template"/>
</record>
@@ -97,7 +97,7 @@
<td><span t-esc="allow"/></td>
</t>
</t>
<td t-esc="format(allowance[13], 'monetary', employee.company_id.currency_id)"/>
<td t-esc="allowance[13]" t-options="{'widget': 'monetary', 'display_currency': employee.company_id.currency_id}"/>
</tr>
<td><strong> Deductions:</strong></td>
<tr t-foreach="get_deduct()" t-as="deduct">
@@ -110,7 +110,7 @@
<t t-foreach="deduct[1:12]" t-as="d">
<td><span t-esc="d"/></td>
</t>
<td t-esc="format(deduct[13], 'monetary', employee.company_id.currency_id)"/>
<td t-esc="deduct[13]" t-options="{'widget': 'monetary', 'display_currency': employee.company_id.currency_id}"/>
</tr>
</tbody>
</table>
+4
View File
@@ -18,6 +18,10 @@
<field name="name">DIOT: 16%</field>
<field name="applicability">taxes</field>
</record>
<record id="tag_diot_16_non_cre" model="account.account.tag">
<field name="name">DIOT: 16% NO ACREDITABLE</field>
<field name="applicability">taxes</field>
</record>
<record id="tag_diot_16_imp" model="account.account.tag">
<field name="name">DIOT: 16% IMP</field>
<field name="applicability">taxes</field>
+3 -3
View File
@@ -167,8 +167,8 @@ class Partner(models.Model):
try:
base_template = self.env.ref(template_xmlid, raise_if_not_found=True)
except ValueError:
_logger.warning('QWeb template %s not found when sending notification emails. Skipping.' % (template_xmlid))
return False
_logger.warning('QWeb template %s not found when sending notification emails. Sending without layouting.' % (template_xmlid))
base_template = False
base_template_ctx = self._notify_prepare_template_context(message, model_description=model_description)
base_mail_values = self._notify_prepare_email_values(message, mail_auto_delete=mail_auto_delete)
@@ -185,7 +185,7 @@ class Partner(models.Model):
template_ctx = {**base_template_ctx, **recipient_template_values} # fixme: set button_unfollow to none
fol_values = {
'subject': message.subject or (message.record_name and 'Re: %s' % message.record_name),
'body': base_template.render(template_ctx, engine='ir.qweb', minimal_qcontext=True),
'body': base_template.render(template_ctx, engine='ir.qweb', minimal_qcontext=True) if base_template else message.body,
}
fol_values['body'] = self.env['mail.thread']._replace_local_links(fol_values['body'])
# send email
+1 -1
View File
@@ -288,7 +288,7 @@ class MaintenanceRequest(models.Model):
owner_user_id = fields.Many2one('res.users', string='Created by User', default=lambda s: s.env.uid)
category_id = fields.Many2one('maintenance.equipment.category', related='equipment_id.category_id', string='Category', store=True, readonly=True)
equipment_id = fields.Many2one('maintenance.equipment', string='Equipment', index=True)
technician_user_id = fields.Many2one('res.users', string='Owner', track_visibility='onchange', oldname='user_id')
technician_user_id = fields.Many2one('res.users', string='Technician', track_visibility='onchange', oldname='user_id')
stage_id = fields.Many2one('maintenance.stage', string='Stage', ondelete='restrict', track_visibility='onchange',
group_expand='_read_group_stage_ids', default=_default_stage)
priority = fields.Selection([('0', 'Very Low'), ('1', 'Low'), ('2', 'Normal'), ('3', 'High')], string='Priority')
+1 -1
View File
@@ -742,7 +742,7 @@ var Chrome = PosBaseWidget.extend(AbstractAction.prototype, {
if(err.message === 'XmlHttpRequestError '){
title = 'Network Failure (XmlHttpRequestError)';
body = 'The Point of Sale could not be loaded due to a network problem.\n Please check your internet connection.';
}else if(err.message === 'OpenERP Server Error'){
}else if(err.code === 200){
title = err.data.message;
body = err.data.debug;
}
+8 -2
View File
@@ -332,8 +332,8 @@ exports.PosModel = Backbone.Model.extend({
},
},{
model: 'res.currency',
fields: ['name','symbol','position','rounding'],
ids: function(self){ return [self.config.currency_id[0]]; },
fields: ['name','symbol','position','rounding','rate'],
ids: function(self){ return [self.config.currency_id[0], self.company.currency_id[0]]; },
loaded: function(self, currencies){
self.currency = currencies[0];
if (self.currency.rounding > 0 && self.currency.rounding < 1) {
@@ -342,6 +342,7 @@ exports.PosModel = Backbone.Model.extend({
self.currency.decimals = 0;
}
self.company_currency = currencies[1];
},
},{
model: 'pos.category',
@@ -360,7 +361,12 @@ exports.PosModel = Backbone.Model.extend({
domain: [['sale_ok','=',true],['available_in_pos','=',true]],
context: function(self){ return { display_default_code: false }; },
loaded: function(self, products){
var using_company_currency = self.config.currency_id[0] === self.company.currency_id[0];
var conversion_rate = self.currency.rate / self.company_currency.rate;
self.db.add_products(_.map(products, function (product) {
if (!using_company_currency) {
product.lst_price = round_pr(product.lst_price * conversion_rate, self.currency.rounding);
}
product.categ = _.findWhere(self.product_categories, {'id': product.categ_id[0]});
return new exports.Product({}, product);
}));
@@ -1281,7 +1281,8 @@ var ClientListScreenWidget = ScreenWidget.extend({
})
.then(function(partner_id){
self.saved_client_details(partner_id);
},function(type,err){
},function(err,ev){
ev.preventDefault();
var error_body = _t('Your Internet connection is probably down.');
if (err.data) {
var except = err.data;
@@ -48,14 +48,18 @@ function connect () {
if [ -z "${PASSWORD}" ] ; then
sudo iwconfig wlan0 essid "${ESSID}"
else
sudo wpa_passphrase "${ESSID}" "${PASSWORD}" > "${WPA_PASS_FILE}"
# Necessary in stretch: https://www.raspberrypi.org/forums/viewtopic.php?t=196927
sudo cp /etc/wpa_supplicant/wpa_supplicant.conf "${WPA_PASS_FILE}"
chmod 777 ${WPA_PASS_FILE}
sudo wpa_passphrase "${ESSID}" "${PASSWORD}" >> "${WPA_PASS_FILE}"
sudo wpa_supplicant -B -i wlan0 -c "${WPA_PASS_FILE}"
fi
sudo systemctl daemon-reload
sudo service dhcpcd restart
# give dhcp some time
timeout 30 sh -c 'until ifconfig wlan0 | grep "inet addr:" ; do sleep 0.1 ; done'
timeout 30 sh -c 'until ifconfig wlan0 | grep "inet " ; do sleep 0.1 ; done'
TIMEOUT_RETURN=$?
if [ ${TIMEOUT_RETURN} -eq 124 ] && [ -z "${NO_AP}" ] ; then
@@ -2,7 +2,7 @@
CURRENT_WIFI_NETWORK_FILE="/tmp/current_wifi_network.txt"
while true ; do
if [ -z "$(cat <(ifconfig eth0) <(ifconfig wlan0) | grep "inet addr" | awk -F: '{print $2}' | awk '{print $1}';)" ] ; then
if [ -z "$(cat <(ifconfig eth0) <(ifconfig wlan0) | grep "inet ";)" ] ; then
ESSID=$(head -n 1 "${CURRENT_WIFI_NETWORK_FILE}" | tr -d '\n')
PASSWORD=$(tail -n 1 "${CURRENT_WIFI_NETWORK_FILE}" | tr -d '\n')
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
FORCE_HOST_AP="${1}"
WIRED_IP=$(ifconfig eth0 | grep "inet addr" | awk -F: '{print $2}' | awk '{print $1}';)
WIRED_IP=$(python3 -c "import netifaces as ni; print(ni.ifaddresses('eth0').get(ni.AF_INET) and ni.ifaddresses('eth0')[ni.AF_INET][0]['addr'] or '')")
WIFI_NETWORK_FILE="/home/pi/wifi_network.txt"
# if there is no wired ip, attempt to start an AP through wireless interface
@@ -39,6 +39,8 @@ slaac private
# A hook script is provided to lookup the hostname if not set by the DHCP
# server, but it should not be run by default.
nohook lookup-hostname
# stretch: we do not want to start wpa_supplicant as our script starts wpa_supplicant itself
nohook wpa_supplicant
# dhcpcd will assign zeroconf 169.254.*.* addresses when
# it can't connect, which we don't want
@@ -14,7 +14,7 @@
. /lib/lsb/init-functions
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin
DAEMON=/home/pi/odoo/odoo.py
DAEMON=/home/pi/odoo/odoo-bin
NAME=odoo
DESC=odoo
CONFIG=/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/odoo.conf
@@ -21,11 +21,15 @@ apt-get update && apt-get -y upgrade
# Do not be too fast to upgrade to more recent firmware and kernel than 4.38
# Firmware 4.44 seems to prevent the LED mechanism from working
PKGS_TO_INSTALL="adduser postgresql-client python python-dateutil python-decorator python-docutils python-feedparser python-imaging python-jinja2 python-ldap python-libxslt1 python-lxml python-mako python-mock python-passlib python-psutil python-psycopg2 python-pybabel python-pychart python-pydot python-pyparsing python-pypdf python-reportlab python-requests python-tz python-vatnumber python-vobject python-werkzeug python-xlwt python-yaml postgresql python-gevent python-serial python-pip python-dev localepurge vim mc mg screen iw hostapd isc-dhcp-server git rsync console-data lightdm xserver-xorg-video-fbdev xserver-xorg-input-evdev iceweasel xdotool unclutter x11-utils openbox python-netifaces rpi-update"
PKGS_TO_INSTALL="adduser postgresql python3 python3-dateutil python3-decorator python3-docutils python3-feedparser python3-pil python3-jinja2 python3-ldap3 python3-lxml python3-mako python3-mock python3-openid python3-psutil python3-psycopg2 python3-babel python3-pydot python3-pyparsing python3-pypdf2 python3-reportlab python3-requests python3-simplejson python3-tz python3-vatnumber python3-werkzeug python3-yaml python3-serial python3-pip python3-dev localepurge vim mc mg screen iw hostapd isc-dhcp-server git rsync console-data lightdm xserver-xorg-video-fbdev xserver-xorg-input-evdev iceweasel xdotool unclutter x11-utils openbox python3-netifaces rpi-update python3-passlib python3-libsass python3-qrcode python3-html2text python3-unittest2 python3-simplejson"
# KEEP OWN CONFIG FILES DURING PACKAGE CONFIGURATION
# http://serverfault.com/questions/259226/automatically-keep-current-version-of-config-files-when-apt-get-install
apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes install ${PKGS_TO_INSTALL}
apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" install ${PKGS_TO_INSTALL}
pg_lsclusters
systemctl start postgresql@9.6-main
systemctl status postgresql@9.6-main
sudo -u postgres createuser -s pi
apt-get clean
localepurge
@@ -34,23 +38,15 @@ rm -rf /usr/share/doc
# python-usb in wheezy is too old
# the latest pyusb from pip does not work either, usb.core.find() never returns
# this may be fixed with libusb>2:1.0.11-1, but that's the most recent one in raspbian
# so we install the latest pyusb that works with this libusb
pip install pyusb==1.0.0b1
pip install qrcode
pip install evdev
pip install simplejson
pip install unittest2
# --upgrade because websocket_client in wheezy is bad:
# https://github.com/docker/compose/issues/1288
pip install --upgrade websocket_client
# so we install the latest pyusb that works with this libusb.
# Even in stretch, we had an error with langid (but worked otherwise)
pip3 install pyusb==1.0.0b1
pip3 install evdev
groupadd usbusers
usermod -a -G usbusers pi
usermod -a -G lp pi
usermod -a -G input lightdm
sudo -u postgres createuser -s pi
mkdir /var/log/odoo
chown pi:pi /var/log/odoo
chown pi:pi -R /home/pi/odoo/
@@ -28,6 +28,8 @@ __base="$(basename ${__file} .sh)"
MOUNT_POINT="${__dir}/root_mount"
OVERWRITE_FILES_BEFORE_INIT_DIR="${__dir}/overwrite_before_init"
OVERWRITE_FILES_AFTER_INIT_DIR="${__dir}/overwrite_after_init"
VERSION=11.0
REPO=https://github.com/odoo/odoo.git
if [ ! -f kernel-qemu ] || ! file_exists *raspbian*.img ; then
./posbox_download_images.sh
@@ -42,14 +44,14 @@ rm -rf "${CLONE_DIR}"
if [ ! -d $CLONE_DIR ]; then
echo "Clone Github repo"
mkdir -p "${CLONE_DIR}"
git clone -b 8.0 --no-local --no-checkout --depth 1 https://github.com/odoo/odoo.git "${CLONE_DIR}"
git clone -b ${VERSION} --no-local --no-checkout --depth 1 ${REPO} "${CLONE_DIR}"
cd "${CLONE_DIR}"
git config core.sparsecheckout true
echo "addons/web
addons/hw_*
addons/point_of_sale/tools/posbox/configuration
openerp/
odoo.py" | tee --append .git/info/sparse-checkout > /dev/null
odoo/
odoo-bin" | tee --append .git/info/sparse-checkout > /dev/null
git read-tree -mu HEAD
fi
@@ -89,7 +91,7 @@ sleep 5
e2fsck -f "${LOOP_MAPPER_PATH}" # resize2fs requires clean fs
resize2fs "${LOOP_MAPPER_PATH}"
mkdir "${MOUNT_POINT}"
mkdir -p "${MOUNT_POINT}" #-p: no error if existing
mount "${LOOP_MAPPER_PATH}" "${MOUNT_POINT}"
# 'overlay' the overwrite directory onto the mounted image filesystem
@@ -107,7 +109,8 @@ umount "${MOUNT_POINT}"
# from http://paulscott.co.za/blog/full-raspberry-pi-raspbian-emulation-with-qemu/
# ssh pi@localhost -p10022
QEMU_OPTS=(-kernel kernel-qemu -cpu arm1176 -m 256 -M versatilepb -no-reboot -serial stdio -append 'root=/dev/sda2 rootfstype=ext4 rw' -hda posbox.img -net user,hostfwd=tcp::10022-:22,hostfwd=tcp::18069-:8069 -net nic)
# as of stretch with newer kernels, the versatile-pb.dtb file is necessary
QEMU_OPTS=(-kernel kernel-qemu -cpu arm1176 -m 256 -M versatilepb -dtb versatile-pb.dtb -no-reboot -serial stdio -append 'root=/dev/sda2 rootfstype=ext4 rw' -hda posbox.img -net user,hostfwd=tcp::10022-:22,hostfwd=tcp::18069-:8069 -net nic)
if [ -z ${DISPLAY:-} ] ; then
QEMU_OPTS+=(-nographic)
fi
@@ -1,5 +1,8 @@
#!/bin/sh
wget 'http://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2017-07-05/2017-07-05-raspbian-jessie-lite.zip' -O raspbian.img.zip
wget 'http://downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2018-06-29/2018-06-27-raspbian-stretch-lite.zip' -O raspbian.img.zip
unzip raspbian.img.zip
wget 'https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/kernel-qemu-4.4.13-jessie' -O kernel-qemu
wget 'https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/kernel-qemu-4.9.59-stretch' -O kernel-qemu
wget 'https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/versatile-pb.dtb'
+6
View File
@@ -27,6 +27,12 @@
<div class="navbar navbar-default navbar-static-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-top-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="/" class="navbar-brand logo">
<span t-field="res_company.logo" t-options="{'widget': 'image'}" t-att-alt="'Logo of %s' % res_company.name" t-att-title="res_company.name"/>
</a>
+2 -1
View File
@@ -271,7 +271,8 @@ class ProductTemplate(models.Model):
@api.one
@api.depends('product_variant_ids.product_tmpl_id')
def _compute_product_variant_count(self):
self.product_variant_count = len(self.product_variant_ids)
# do not pollute variants to be prefetched when counting variants
self.product_variant_count = len(self.with_prefetch().product_variant_ids)
@api.depends('product_variant_ids', 'product_variant_ids.default_code')
def _compute_default_code(self):
+3 -1
View File
@@ -592,7 +592,9 @@ class UoM(models.Model):
if 'factor' in values or 'factor_inv' in values or 'category_id' in values:
changed = self.filtered(
lambda u: any(u[f] != values[f] if f in values else False
for f in {'factor', 'factor_inv', 'category_id'}))
for f in {'factor', 'factor_inv'})) + self.filtered(
lambda u: any(u[f].id != int(values[f]) if f in values else False
for f in {'category_id'}))
if changed:
stock_move_lines = self.env['stock.move.line'].search_count([
('product_uom_id.category_id', 'in', changed.mapped('category_id.id')),
+2 -1
View File
@@ -348,7 +348,8 @@ class ProductProduct(models.Model):
qty_delivered += qty_to_consider
# `move.price_unit` is negative if the move is out and positive if the move is
# dropshipped. Use its absolute value to compute the average price unit.
average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + abs(move.price_unit) * qty_to_consider) / qty_delivered
if qty_delivered:
average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + abs(move.price_unit) * qty_to_consider) / qty_delivered
if qty_delivered == quantity:
break
return average_price_unit
+1 -1
View File
@@ -151,7 +151,7 @@ class StockMove(models.Model):
def _get_price_unit(self):
""" Returns the unit price to store on the quant """
return self.price_unit or self.product_id.standard_price
return not self.company_id.currency_id.is_zero(self.price_unit) and self.price_unit or self.product_id.standard_price
@api.model
def _get_in_base_domain(self, company_id=False):
+7 -6
View File
@@ -260,14 +260,15 @@ var QueryGroup = Class.extend({
var count_key = (grouping_fields[0] && grouping_fields[0].split(':')[0]) + '_count';
var aggregates = {};
_(fixed_group).each(function (value, key) {
if (key.indexOf('__') === 0
for (var key in fixed_group) {
if (fixed_group.hasOwnProperty(key)) {
if (!(key.indexOf('__') === 0
|| _.contains(grouping_fields, key)
|| (key === count_key)) {
return;
|| (key === count_key))) {
aggregates[key] = fixed_group[key] || 0;
}
}
aggregates[key] = value || 0;
});
}
this.model = new Model(
model, fixed_group.__context, fixed_group.__domain);
+2 -10
View File
@@ -173,16 +173,8 @@
<t t-call-assets="website.assets_editor" t-css="false" groups="website.group_website_publisher"/>
</t>
</xpath>
<xpath expr="//header//div[hasclass('navbar-header')]" position="replace">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-top-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/" t-if="website" t-field="website.name">My Website</a>
</div>
<xpath expr="//header//a[hasclass('navbar-brand')]" position="replace">
<a class="navbar-brand" href="/" t-if="website" t-field="website.name">My Website</a>
</xpath>
<xpath expr="//header//ul[@id='top_menu']/li[hasclass('divider')]" position="attributes">
<attribute name="t-if">website.user_id != user_id</attribute>
+2 -2
View File
@@ -86,8 +86,8 @@ class Event(models.Model):
if not event.menu_id:
root_menu = self.env['website.menu'].create({'name': event.name})
event.menu_id = root_menu
for sequence, (name, url, xml_id) in enumerate(self._get_menu_entries()):
self._create_menu(sequence, name, url, xml_id)
for sequence, (name, url, xml_id) in enumerate(event._get_menu_entries()):
event._create_menu(sequence, name, url, xml_id)
return res
def _create_menu(self, sequence, name, url, xml_id):
+1 -1
View File
@@ -497,7 +497,7 @@ class Post(models.Model):
self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accept * mult)
if 'tag_ids' in vals:
tag_ids = set(tag.get('id') for tag in self.resolve_2many_commands('tag_ids', vals['tag_ids']))
if any(set(post.tag_ids) != tag_ids for post in self) and any(self.env.user.karma < post.forum_id.karma_edit_retag for post in self):
if any(set(post.tag_ids.ids) != tag_ids for post in self) and any(self.env.user.karma < post.forum_id.karma_edit_retag for post in self):
raise KarmaError(_('Not enough karma to retag.'))
if any(key not in trusted_keys for key in vals) and any(not post.can_edit for post in self):
raise KarmaError('Not enough karma to edit a post.')
+1 -1
View File
@@ -624,7 +624,7 @@
<div t-if="not is_answer">
<br/>
<input type="hidden" name="karma_edit_retag" t-attf-value="#{forum.karma_edit_retag}" id="karma_edit_retag"/>
<input type="text" name="post_tags" class="form-control col-md-9 js_select2" placeholder="Tags" t-attf-data-init-value="#{tags}" value="Tags"/>
<input type="text" name="post_tags" class="form-control col-md-9 js_select2" placeholder="Tags" t-attf-data-init-value="#{tags}" value="Tags" t-att-readonly="user.karma &lt; forum.karma_edit_retag" t-att-title="'You need to have sufficient karma to edit tags' if user.karma &lt; forum.karma_edit_retag else ''"/>
<br/>
</div>
<button type="submit" class="btn btn-primary btn-lg">Save</button>
+9
View File
@@ -0,0 +1,9 @@
United Kingdom, 2018-07-15
I hereby agree to the terms of the Odoo Individual Contributor License Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this declaration.
Signed,
Orya Blumeneld orya@rawrfoods.com https://github.com/rawrfnf
-7
View File
@@ -384,13 +384,6 @@ customize the amount of logging output
In case of conflict between :option:`--log-level` and
:option:`--log-handler`, the latter is used
translations
------------
.. option:: --i18n-import
.. option:: --i18n-export
emails
------
-1
View File
@@ -222,7 +222,6 @@ Use the following pattern :
*concerned_group* is the short name of the concerned group ('user'
for the 'model_name_group_user', 'public' for public user, 'company'
for multi-company rules, ...).
* For a group : :samp:`{<model_name>}_group_{<group_name>}` where *group_name* is the name of the group, generally 'user', 'manager', ...
.. code-block:: xml
+1
View File
@@ -9,5 +9,6 @@ Setting Up
setup/install
setup/deploy
setup/update
setup/cdn
setup/enterprise
+2 -2
View File
@@ -6,7 +6,7 @@
Installing Odoo
===============
There are mutliple ways to install Odoo, or not install it at all, depending
There are multiple ways to install Odoo, or not install it at all, depending
on the intended use case.
This documents attempts to describe most of the installation options.
@@ -544,7 +544,7 @@ Docker
======
The full documentation on how to use Odoo with Docker can be found on the
offcial Odoo `docker image <https://registry.hub.docker.com/_/odoo/>`_ page.
official Odoo `docker image <https://registry.hub.docker.com/_/odoo/>`_ page.
.. _demo: https://demo.odoo.com
.. _docker: https://www.docker.com
+151
View File
@@ -0,0 +1,151 @@
:banner: banners/installing_odoo.jpg
.. _setup/update:
=============================
Updating an Odoo installation
=============================
Introduction
============
In order to benefit from the latest improvements, security fixes, bug corrections and
performance boosts, you may need to update your Odoo installation from time to time.
This guide only applies when are using Odoo on your own hosting infrastructure.
If you are using one of the Odoo Cloud solutions, updates are automatically performed for you.
The terminology surrounding software updates is often confusing, so here are some preliminary
definitions:
Updating (an Odoo installation)
Refers to the process of obtaining the latest revision of the source code for
your current Odoo Edition. For example, updating your Odoo Enterprise 11.0 to the
latest revision.
This does not directly cause any change to the contents of your Odoo database, and
can be undone by reinstalling the previous revision of the source code.
Upgrading (an Odoo database)
Refers to a complex data processing operation where the structure and contents of your
database is permanently altered to make it compatible with a new release of Odoo.
This operation is irreversible and typically accomplished via Odoo's
`database upgrade service <https://upgrade.odoo.com>`_, when you decide to
switch to a newer release of Odoo.
Historically, this process has also been known as a "migration" because it involves moving data
around inside the database, even though the database may end up at the same physical location
after the upgrade.
This page describes the typical steps needed to *update* an Odoo installation to the latest
version. If you'd like more information about upgrading a database, please visit the
`Odoo Upgrade page <https://upgrade.odoo.com>`_ instead.
In a nutshell
=============
Updating Odoo is accomplished by simply reinstalling the latest version of your Odoo
Edition on top of your current installation. This will preserve your data without any alteration,
as long as you do not uninstall PostgreSQL (the database engine that comes with Odoo).
The main reference for updating is logically our :ref:`installation guide <setup/install>`,
which explains the common installation methods.
Updating is also most appropriately accomplished by the person who deployed Odoo initially,
because the procedure is very similar.
.. note:: We always recommend to download a complete new up-to-date Odoo version, rather than
manually applying patches, such as the security patches that come with Security
Advisories.
The patches are mainly provided for installations that are heavily customized, or for
technical personnel who prefer to apply minimal changes temporarily while testing a
complete update.
Step 1: Download an updated Odoo version
========================================
The central download page is https://www.odoo.com/page/download. If you see a "Buy" link for the
Odoo Enterprise download, make sure you are logged into Odoo.com with the same login that is
linked to your Odoo Enterprise subscription.
Alternatively, you can use the unique download link that was included with your Odoo Enterprise
purchase confirmation email.
.. note:: Downloading an updated version is not necessary if you installed via Github (see below)
Step 2: Make a backup of your database
======================================
The update procedure is quite safe and should not alter you data. However it's always best to take
a full database backup before performing any change on your installation, and to store it somewhere
safe, on a different computer.
If you have not disabled the database manager screen (see :ref:`here <security>` why you should), you
can use it (link at bottom of your database selection screen) to download a backup of your
database(s). If you disabled it, use the same procedure than for your usual backups.
Step 3: Install the updated version
===================================
Choose the method that matches your current installation:
Packaged Installers
-------------------
If you installed Odoo with an installation package downloaded on our website (the recommended method),
updating is very simple.
All you have to do is download the installation package corresponding to your system (see step #1)
and install it on your server. They are updated daily and include the latest security fixes.
Usually, you can simply double-click the package to install it on top of the current installation.
After installing the package, be sure to restart the Odoo service or reboot your server,
and you're all set.
Source Install (Tarball)
------------------------
If you have originally installed Odoo with the "tarball" version (source code archive), you have
to replace the installation directory with a newer version. First download the latest tarball
from Odoo.com. They are updated daily and include the latest security fixes (see step #1)
After downloading the package, extract it to a temporary location on your server.
You will get a folder labelled with the version of the source code, for example "odoo-11.0+e.20180719",
that contains a folder "odoo.egg-info" and the actual source code folder named "odoo" (for Odoo 10
and later) or "openerp" for older versions.
You can ignore the odoo.egg-info folder. Locate the folder where your current installation is deployed,
and replace it with the newer "odoo" or "openerp" folder that was in the archive you just extracted.
Be sure to match the folder layout, for example the new "addons" folder included in the source code
should end up exactly at the same path it was before. Next, watch out for any specific configuration
files that you may have manually copied or modified in the old folder, and copy them over to the
new folder.
Finally, restart the Odoo service or reboot the machine, and you are all set.
Source Install (Github)
-----------------------
If you have originally installed Odoo with a full Github clone of the official repositories, the
update procedure requires you to pull the latest source code via git.
Change into the directory for each repository (the main Odoo repository, and the Enterprise
repository), and run the following commands::
git fetch
git rebase --autostash
The last command may encounter source code conflicts if you had edited the Odoo source code locally.
The error message will give you the list of files with conflicts, and you will need to resolve
the conflicts manually, by editing them and deciding which part of the code to keep.
Alternatively, if you prefer to simply discard the conflicting changes and restore the official
version, you can use the following command::
git reset --hard
Finally, restart the Odoo service or reboot the machine, and you should be done.
Docker
------
Please refer to our `Docker image documentation <https://hub.docker.com/_/odoo/>`_ for
specific update instructions.
+10 -4
View File
@@ -111,8 +111,11 @@ class AssetsBundle(object):
self.javascripts.append(JavascriptAsset(self, url=f['url'], filename=f['filename'], inline=f['content']))
# depreciated and will remove after v11
def to_html(self, sep=None, css=True, js=True, debug=False, async=False, url_for=(lambda url: url)):
nodes = self.to_node(css=css, js=js, debug=debug, async=async)
def to_html(self, sep=None, css=True, js=True, debug=False, async_load=False, url_for=(lambda url: url), **kw):
if 'async' in kw:
_logger.warning("Using deprecated argument 'async' in to_html call, use 'async_load' instead.")
async_load = kw['async']
nodes = self.to_node(css=css, js=js, debug=debug, async_load=async_load)
if sep is None:
sep = u'\n '
@@ -130,10 +133,13 @@ class AssetsBundle(object):
return sep + sep.join(response)
def to_node(self, css=True, js=True, debug=False, async=False):
def to_node(self, css=True, js=True, debug=False, async_load=False, **kw):
"""
:returns [(tagName, attributes, content)] if the tag is auto close
"""
if 'async' in kw:
_logger.warning("Using deprecated argument 'async' in to_node call, use 'async_load' instead.")
async_load = kw['async']
response = []
if debug == 'assets':
if css and self.stylesheets:
@@ -166,7 +172,7 @@ class AssetsBundle(object):
response.append(JavascriptAsset(self, inline=self.dialog_message(msg)).to_node())
if js and self.javascripts:
attr = OrderedDict([
["async", "async" if async else None],
["async", "async" if async_load else None],
["type", "text/javascript"],
["src", self.js().url],
])
+10 -6
View File
@@ -278,24 +278,28 @@ class IrQWeb(models.AbstractModel, QWeb):
# compatibility to remove after v11 - DEPRECATED
@tools.conditional(
'xml' not in tools.config['dev_mode'],
tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async', keys=("website_id",)),
tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'kw.get("async")', 'async_load', keys=("website_id",)),
)
def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None):
def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async_load=False, values=None, **kw):
if 'async' in kw:
async_load = kw['async']
files, remains = self._get_asset_content(xmlid, options)
asset = self.get_asset_bundle(xmlid, files, remains, env=self.env)
return asset.to_html(css=css, js=js, debug=debug, async=async, url_for=(values or {}).get('url_for', lambda url: url))
return asset.to_html(css=css, js=js, debug=debug, async_load=async_load, url_for=(values or {}).get('url_for', lambda url: url))
@tools.conditional(
# in non-xml-debug mode we want assets to be cached forever, and the admin can force a cache clear
# by restarting the server after updating the source code (or using the "Clear server cache" in debug tools)
'xml' not in tools.config['dev_mode'],
tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'async', keys=("website_id",)),
tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', 'css', 'js', 'debug', 'kw.get("async")', 'async_load', keys=("website_id",)),
)
def _get_asset_nodes(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None):
def _get_asset_nodes(self, xmlid, options, css=True, js=True, debug=False, async_load=False, values=None, **kw):
if 'async' in kw:
async_load = kw['async']
files, remains = self._get_asset_content(xmlid, options)
asset = self.get_asset_bundle(xmlid, files, env=self.env)
remains = [node for node in remains if (css and node[0] == 'link') or (js and node[0] != 'link')]
return remains + asset.to_node(css=css, js=js, debug=debug, async=async)
return remains + asset.to_node(css=css, js=js, debug=debug, async_load=async_load)
@tools.ormcache_context('xmlid', 'options.get("lang", "en_US")', keys=("website_id",))
def _get_asset_content(self, xmlid, options):
@@ -470,6 +470,7 @@ class IrTranslation(models.Model):
discarded += trans
else:
trans.write({'src': matches[0], 'state': trans.state})
trans_src.append(matches[0]) # avoid reuse of term
else:
outdated += trans
+2
View File
@@ -101,6 +101,8 @@ _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
'CALL_FUNCTION_EX',
# Already in P2 but apparently the first one is used more aggressively in P3
'CALL_FUNCTION_KW', 'CALL_FUNCTION_VAR', 'CALL_FUNCTION_VAR_KW',
# Added in P3.7 https://bugs.python.org/issue26110
'CALL_METHOD', 'LOAD_METHOD',
'GET_ITER', 'FOR_ITER', 'YIELD_VALUE',
'JUMP_FORWARD', 'JUMP_IF_TRUE', 'JUMP_IF_FALSE', 'JUMP_ABSOLUTE',
# New in Python 2.7 - http://bugs.python.org/issue4715 :
+6 -3
View File
@@ -3,12 +3,15 @@ decorator==4.0.10
docutils==0.12
ebaysdk==2.1.5
feedparser==5.2.1
gevent==1.1.2 ; sys_platform != 'win32'
greenlet==0.4.10
gevent==1.1.2 ; sys_platform != 'win32' and python_version < '3.7'
gevent==1.3.4 ; sys_platform != 'win32' and python_version >= '3.7'
greenlet==0.4.10 ; python_version < '3.7'
greenlet==0.4.13 ; python_version >= '3.7'
html2text==2016.9.19
Jinja2==2.8
libsass==0.12.3
lxml==3.7.1 ; sys_platform != 'win32'
lxml==3.7.1 ; sys_platform != 'win32' and python_version < '3.7'
lxml==4.2.3 ; sys_platform != 'win32' and python_version >= '3.7'
lxml ; sys_platform == 'win32'
Mako==1.0.4
MarkupSafe==0.23