diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-07-19 15:36:54 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-07-19 15:36:54 +0200 |
| commit | 1f7a39964b327e9084fb905c7b325e94774080da (patch) | |
| tree | 0cf9f1d68ece39d0bdb455d76d4e39815c630c85 | |
| parent | c65ca4ec1626ee1c35e727c3b70b64c103917312 (diff) | |
| parent | 0c42dcfc229922e21d83a3f10425fdf86ce3f8f2 (diff) | |
[MERGE] forward port branch saas-11.3 up to 0c42dcfc229
49 files changed, 640 insertions, 149 deletions
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 2c2b80d3385..87bd379879d 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -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 diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index cdbc5a0ebf3..7578f9c4d1d 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -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); }); diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index 7ebffc19e94..4fb3f997368 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -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(); + }); }); }); diff --git a/addons/account/tests/test_reconciliation.py b/addons/account/tests/test_reconciliation.py index b95875eb954..4a43da513b6 100644 --- a/addons/account/tests/test_reconciliation.py +++ b/addons/account/tests/test_reconciliation.py @@ -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 diff --git a/addons/board/static/src/js/board_view.js b/addons/board/static/src/js/board_view.js index f65e6bb1e85..aa0613e9785 100644 --- a/addons/board/static/src/js/board_view.js +++ b/addons/board/static/src/js/board_view.js @@ -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); }); @@ -402,6 +409,16 @@ var BoardRenderer = FormRenderer.extend({ 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 * interfering with the ActionManager. diff --git a/addons/board/static/tests/dashboard_tests.js b/addons/board/static/tests/dashboard_tests.js index 9afb88e5648..e777b7b36b7 100644 --- a/addons/board/static/tests/dashboard_tests.js +++ b/addons/board/static/tests/dashboard_tests.js @@ -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); diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py index fa57b18501e..805d8388a8a 100644 --- a/addons/hw_escpos/controllers/main.py +++ b/addons/hw_escpos/controllers/main.py @@ -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() diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py index 77cec6d6554..7fc0e196b39 100644 --- a/addons/hw_escpos/escpos/escpos.py +++ b/addons/hw_escpos/escpos/escpos.py @@ -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 diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py index 73ce2a70a13..6ba6c1ff62a 100644 --- a/addons/hw_posbox_homepage/controllers/main.py +++ b/addons/hw_posbox_homepage/controllers/main.py @@ -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']) diff --git a/addons/hw_posbox_upgrade/controllers/main.py b/addons/hw_posbox_upgrade/controllers/main.py index b863afdf318..f71dcca2f60 100644 --- a/addons/hw_posbox_upgrade/controllers/main.py +++ b/addons/hw_posbox_upgrade/controllers/main.py @@ -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'> diff --git a/addons/hw_proxy/controllers/main.py b/addons/hw_proxy/controllers/main.py index 3d42918c2ad..31d718616e4 100644 --- a/addons/hw_proxy/controllers/main.py +++ b/addons/hw_proxy/controllers/main.py @@ -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', diff --git a/addons/hw_scale/controllers/main.py b/addons/hw_scale/controllers/main.py index 090e6193fe6..162ab7a866d 100644 --- a/addons/hw_scale/controllers/main.py +++ b/addons/hw_scale/controllers/main.py @@ -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 diff --git a/addons/l10n_fr/data/account_chart_template_data.xml b/addons/l10n_fr/data/account_chart_template_data.xml index aac0137d150..d184a621db5 100644 --- a/addons/l10n_fr/data/account_chart_template_data.xml +++ b/addons/l10n_fr/data/account_chart_template_data.xml @@ -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> diff --git a/addons/l10n_in_hr_payroll/views/report_hr_yearly_salary_detail_template.xml b/addons/l10n_in_hr_payroll/views/report_hr_yearly_salary_detail_template.xml index 8a322cea9bc..3dc77974e37 100644 --- a/addons/l10n_in_hr_payroll/views/report_hr_yearly_salary_detail_template.xml +++ b/addons/l10n_in_hr_payroll/views/report_hr_yearly_salary_detail_template.xml @@ -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> diff --git a/addons/l10n_mx/data/account_tax_data.xml b/addons/l10n_mx/data/account_tax_data.xml index 02d65cbeb29..b961639b36a 100644 --- a/addons/l10n_mx/data/account_tax_data.xml +++ b/addons/l10n_mx/data/account_tax_data.xml @@ -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> diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py index 74f59d7d80e..a8df8efce74 100644 --- a/addons/mail/models/res_partner.py +++ b/addons/mail/models/res_partner.py @@ -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 diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py index 3e17f2c5b32..b1a95bb495e 100644 --- a/addons/maintenance/models/maintenance.py +++ b/addons/maintenance/models/maintenance.py @@ -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') diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js index dc407c78aa9..a049417d98d 100644 --- a/addons/point_of_sale/static/src/js/chrome.js +++ b/addons/point_of_sale/static/src/js/chrome.js @@ -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; } diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index cba3a288228..91e234dafc5 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -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); })); diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js index 1b59aa34cba..172bd53a152 100644 --- a/addons/point_of_sale/static/src/js/screens.js +++ b/addons/point_of_sale/static/src/js/screens.js @@ -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; diff --git a/addons/point_of_sale/tools/posbox/configuration/connect_to_wifi.sh b/addons/point_of_sale/tools/posbox/configuration/connect_to_wifi.sh index 681732ae714..8f2d6b567cc 100755 --- a/addons/point_of_sale/tools/posbox/configuration/connect_to_wifi.sh +++ b/addons/point_of_sale/tools/posbox/configuration/connect_to_wifi.sh @@ -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 diff --git a/addons/point_of_sale/tools/posbox/configuration/keep_wifi_alive.sh b/addons/point_of_sale/tools/posbox/configuration/keep_wifi_alive.sh index a04aba620d4..5c8a27492bb 100755 --- a/addons/point_of_sale/tools/posbox/configuration/keep_wifi_alive.sh +++ b/addons/point_of_sale/tools/posbox/configuration/keep_wifi_alive.sh @@ -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') diff --git a/addons/point_of_sale/tools/posbox/configuration/wireless_ap.sh b/addons/point_of_sale/tools/posbox/configuration/wireless_ap.sh index 97b011b296d..b925dc05cae 100755 --- a/addons/point_of_sale/tools/posbox/configuration/wireless_ap.sh +++ b/addons/point_of_sale/tools/posbox/configuration/wireless_ap.sh @@ -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 diff --git a/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/dhcpcd.conf b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/dhcpcd.conf index 00a1d04f28f..22698cb46ba 100644 --- a/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/dhcpcd.conf +++ b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/dhcpcd.conf @@ -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 diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo index 4ab50a456a1..8cd645c1557 100755 --- a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo @@ -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 diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh index 38de23ca312..07e5a6d4c1d 100755 --- a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh @@ -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/ diff --git a/addons/point_of_sale/tools/posbox/posbox_create_image.sh b/addons/point_of_sale/tools/posbox/posbox_create_image.sh index ae3f52ceb55..1f2764628d8 100755 --- a/addons/point_of_sale/tools/posbox/posbox_create_image.sh +++ b/addons/point_of_sale/tools/posbox/posbox_create_image.sh @@ -28,6 +28,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 diff --git a/addons/point_of_sale/tools/posbox/posbox_download_images.sh b/addons/point_of_sale/tools/posbox/posbox_download_images.sh index 33beddac041..b13505ce16b 100755 --- a/addons/point_of_sale/tools/posbox/posbox_download_images.sh +++ b/addons/point_of_sale/tools/posbox/posbox_download_images.sh @@ -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' + + diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml index 9522a1fcda4..18e425bd651 100644 --- a/addons/portal/views/portal_templates.xml +++ b/addons/portal/views/portal_templates.xml @@ -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> diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py index 9629a1f111e..bcb24773cd6 100644 --- a/addons/product/models/product_template.py +++ b/addons/product/models/product_template.py @@ -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): diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py index 741923862bc..b64574b6b3e 100644 --- a/addons/stock/models/product.py +++ b/addons/stock/models/product.py @@ -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')), diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index 564160f85cb..5a55b89b93f 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -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 diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py index 07cb19163d7..6acdb0d927f 100644 --- a/addons/stock_account/models/stock.py +++ b/addons/stock_account/models/stock.py @@ -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): diff --git a/addons/web/static/src/js/_deprecated/data.js b/addons/web/static/src/js/_deprecated/data.js index eb70f330076..9f8115561b2 100644 --- a/addons/web/static/src/js/_deprecated/data.js +++ b/addons/web/static/src/js/_deprecated/data.js @@ -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); diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 6d84577b4d5..45bcfcb00e4 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -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> diff --git a/addons/website_event/models/event.py b/addons/website_event/models/event.py index 15652bcdbd9..ac3eb999efe 100644 --- a/addons/website_event/models/event.py +++ b/addons/website_event/models/event.py @@ -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): diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py index 7186e19f112..dc9403448f1 100644 --- a/addons/website_forum/models/forum.py +++ b/addons/website_forum/models/forum.py @@ -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.') diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml index 6294cb9b469..255ca26f2df 100644 --- a/addons/website_forum/views/website_forum.xml +++ b/addons/website_forum/views/website_forum.xml @@ -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 < forum.karma_edit_retag" t-att-title="'You need to have sufficient karma to edit tags' if user.karma < forum.karma_edit_retag else ''"/> <br/> </div> <button type="submit" class="btn btn-primary btn-lg">Save</button> diff --git a/doc/cla/individual/rwfnf.md b/doc/cla/individual/rwfnf.md new file mode 100644 index 00000000000..0e92424bf9f --- /dev/null +++ b/doc/cla/individual/rwfnf.md @@ -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 diff --git a/doc/reference/cmdline.rst b/doc/reference/cmdline.rst index 12cd5f7a9d8..058eac51ddd 100644 --- a/doc/reference/cmdline.rst +++ b/doc/reference/cmdline.rst @@ -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 ------ diff --git a/doc/reference/guidelines.rst b/doc/reference/guidelines.rst index c718d82b989..6a62891fffe 100644 --- a/doc/reference/guidelines.rst +++ b/doc/reference/guidelines.rst @@ -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 diff --git a/doc/setup.rst b/doc/setup.rst index f200108964e..99b4bd3ad51 100644 --- a/doc/setup.rst +++ b/doc/setup.rst @@ -9,5 +9,6 @@ Setting Up setup/install setup/deploy + setup/update setup/cdn setup/enterprise diff --git a/doc/setup/install.rst b/doc/setup/install.rst index e4da081260b..db9f832782f 100644 --- a/doc/setup/install.rst +++ b/doc/setup/install.rst @@ -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 diff --git a/doc/setup/update.rst b/doc/setup/update.rst new file mode 100644 index 00000000000..7c7b5c06227 --- /dev/null +++ b/doc/setup/update.rst @@ -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.
\ No newline at end of file diff --git a/odoo/addons/base/models/assetsbundle.py b/odoo/addons/base/models/assetsbundle.py index 393faab10a5..4cd424c994a 100644 --- a/odoo/addons/base/models/assetsbundle.py +++ b/odoo/addons/base/models/assetsbundle.py @@ -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], ]) diff --git a/odoo/addons/base/models/ir_qweb.py b/odoo/addons/base/models/ir_qweb.py index a8cfaf951a5..d082db9e197 100644 --- a/odoo/addons/base/models/ir_qweb.py +++ b/odoo/addons/base/models/ir_qweb.py @@ -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): diff --git a/odoo/addons/base/models/ir_translation.py b/odoo/addons/base/models/ir_translation.py index 1cc17189323..0ad132d4c33 100644 --- a/odoo/addons/base/models/ir_translation.py +++ b/odoo/addons/base/models/ir_translation.py @@ -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 diff --git a/odoo/tools/safe_eval.py b/odoo/tools/safe_eval.py index 7f5d6e35e71..7a5ec03707a 100644 --- a/odoo/tools/safe_eval.py +++ b/odoo/tools/safe_eval.py @@ -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 : diff --git a/requirements.txt b/requirements.txt index 0d0083a9533..74db0c9fc38 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 |
