[MERGE] forward port branch saas-11.3 up to 0c42dcfc22
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
------
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -9,5 +9,6 @@ Setting Up
|
||||
|
||||
setup/install
|
||||
setup/deploy
|
||||
setup/update
|
||||
setup/cdn
|
||||
setup/enterprise
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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],
|
||||
])
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user