-
+ |
Subtotal
daysday
-
+
Your advantage
-
+
diff --git a/addons/stock_dropshipping/models/sale.py b/addons/stock_dropshipping/models/sale.py
index 5e23612a0981f..e80b7433e290c 100644
--- a/addons/stock_dropshipping/models/sale.py
+++ b/addons/stock_dropshipping/models/sale.py
@@ -13,7 +13,7 @@ class SaleOrderLine(models.Model):
def _get_qty_procurement(self, previous_product_uom_qty):
# People without purchase rights should be able to do this operation
purchase_lines_sudo = self.sudo().purchase_line_ids
- if not self.move_ids.filtered(lambda r: r.state != 'cancel') and purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
+ if purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
qty = 0.0
for po_line in purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
qty += po_line.product_uom._compute_quantity(po_line.product_qty, self.product_uom, rounding_method='HALF-UP')
diff --git a/addons/stock_picking_batch/report/report_picking_batch.xml b/addons/stock_picking_batch/report/report_picking_batch.xml
index 079ccdf1ed403..4eb4cf2ec6282 100644
--- a/addons/stock_picking_batch/report/report_picking_batch.xml
+++ b/addons/stock_picking_batch/report/report_picking_batch.xml
@@ -20,6 +20,7 @@
Picking Reference |
+ Barcode |
Status |
Commitment Date |
Scheduled Date |
@@ -30,6 +31,9 @@
|
+
+
+ |
|
diff --git a/addons/test_mail/tests/test_mail_gateway.py b/addons/test_mail/tests/test_mail_gateway.py
index 1a796b0750111..dea415d676521 100644
--- a/addons/test_mail/tests/test_mail_gateway.py
+++ b/addons/test_mail/tests/test_mail_gateway.py
@@ -502,7 +502,7 @@ def test_private_discussion(self):
self.assertEqual(msg.model, False,
'message_post: private discussion: parameter model not correctly ignored when having no res_id')
# Test: message-id
- self.assertIn('openerp-private', msg.message_id, 'message_post: private discussion: message-id should contain the private keyword')
+ self.assertIn('openerp-private', msg.message_id.split('@')[0], 'message_post: private discussion: message-id should contain the private keyword')
# Do: Bert replies through mailgateway (is a customer)
self.format_and_process(
@@ -530,9 +530,9 @@ def test_private_discussion(self):
@mute_logger('odoo.addons.mail.models.mail_thread', 'odoo.models', 'odoo.addons.mail.models.mail_mail')
def test_forward_parent_id(self):
msg = self.test_record.sudo(self.user_employee).message_post(no_auto_thread=True, subtype='mail.mt_comment')
- self.assertNotIn(msg.model, msg.message_id)
- self.assertNotIn('-%d-' % msg.res_id, msg.message_id)
- self.assertIn('reply_to', msg.message_id)
+ self.assertNotIn(msg.model, msg.message_id.split('@')[0])
+ self.assertNotIn('-%d-' % msg.res_id, msg.message_id.split('@')[0])
+ self.assertIn('reply_to', msg.message_id.split('@')[0])
# forward it to a new thread AND an existing thread
fw_msg_id = ''
diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py
index 3b7cf25da46b2..c5962339f68a8 100644
--- a/addons/test_mail/tests/test_mail_message.py
+++ b/addons/test_mail/tests/test_mail_message.py
@@ -31,7 +31,7 @@ def test_mail_message_values_basic(self):
'reply_to': 'test.reply@example.com',
'email_from': 'test.from@example.com',
})
- self.assertIn('-private', msg.message_id, 'mail_message: message_id for a void message should be a "private" one')
+ self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
self.assertEqual(msg.reply_to, 'test.reply@example.com')
self.assertEqual(msg.email_from, 'test.from@example.com')
@@ -39,7 +39,7 @@ def test_mail_message_values_default(self):
self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.domain')]).unlink()
msg = self.Message.create({})
- self.assertIn('-private', msg.message_id, 'mail_message: message_id for a void message should be a "private" one')
+ self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
self.assertEqual(msg.reply_to, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
self.assertEqual(msg.email_from, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
@@ -50,7 +50,7 @@ def test_mail_message_values_alias(self):
self.env['ir.config_parameter'].search([('key', '=', 'mail.catchall.alias')]).unlink()
msg = self.Message.create({})
- self.assertIn('-private', msg.message_id, 'mail_message: message_id for a void message should be a "private" one')
+ self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
self.assertEqual(msg.reply_to, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
self.assertEqual(msg.email_from, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
@@ -61,7 +61,7 @@ def test_mail_message_values_alias_catchall(self):
self.env['ir.config_parameter'].set_param('mail.catchall.alias', alias_catchall)
msg = self.Message.create({})
- self.assertIn('-private', msg.message_id, 'mail_message: message_id for a void message should be a "private" one')
+ self.assertIn('-private', msg.message_id.split('@')[0], 'mail_message: message_id for a void message should be a "private" one')
reply_to_name = self.env.user.company_id.name
reply_to_email = '%s@%s' % (alias_catchall, alias_domain)
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
@@ -74,7 +74,7 @@ def test_mail_message_values_document_no_alias(self):
'model': 'mail.test',
'res_id': self.alias_record.id
})
- self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id)
+ self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0])
self.assertEqual(msg.reply_to, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
self.assertEqual(msg.email_from, '%s <%s>' % (self.user_employee.name, self.user_employee.email))
@@ -88,7 +88,7 @@ def test_mail_message_values_document_alias(self):
'model': 'mail.test',
'res_id': self.alias_record.id
})
- self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id)
+ self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0])
reply_to_name = '%s %s' % (self.env.user.company_id.name, self.alias_record.name)
reply_to_email = '%s@%s' % (self.alias_record.alias_name, alias_domain)
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
@@ -104,7 +104,7 @@ def test_mail_message_values_document_alias_catchall(self):
'model': 'mail.test',
'res_id': self.alias_record.id
})
- self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id)
+ self.assertIn('-openerp-%d-mail.test' % self.alias_record.id, msg.message_id.split('@')[0])
reply_to_name = '%s %s' % (self.env.user.company_id.name, self.alias_record.name)
reply_to_email = '%s@%s' % (self.alias_record.alias_name, alias_domain)
self.assertEqual(msg.reply_to, formataddr((reply_to_name, reply_to_email)))
@@ -116,9 +116,9 @@ def test_mail_message_values_no_auto_thread(self):
'res_id': self.alias_record.id,
'no_auto_thread': True,
})
- self.assertIn('reply_to', msg.message_id)
- self.assertNotIn('mail.test', msg.message_id)
- self.assertNotIn('-%d-' % self.alias_record.id, msg.message_id)
+ self.assertIn('reply_to', msg.message_id.split('@')[0])
+ self.assertNotIn('mail.test', msg.message_id.split('@')[0])
+ self.assertNotIn('-%d-' % self.alias_record.id, msg.message_id.split('@')[0])
def test_mail_message_base64_image(self):
msg = self.env['mail.message'].sudo(self.user_employee).create({
diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js
index 0934ae462b335..af31c252898fe 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -19,6 +19,10 @@ var QuickCreate = require('web.CalendarQuickCreate');
var _t = core._t;
var QWeb = core.qweb;
+function dateToServer (date) {
+ return date.clone().utc().locale('en').format('YYYY-MM-DD HH:mm:ss');
+}
+
var CalendarController = AbstractController.extend({
custom_events: _.extend({}, AbstractController.prototype.custom_events, {
changeDate: '_onChangeDate',
@@ -236,7 +240,7 @@ var CalendarController = AbstractController.extend({
for (var k in context) {
if (context[k] && context[k]._isAMomentObject) {
- context[k] = context[k].clone().utc().format('YYYY-MM-DD HH:mm:ss');
+ context[k] = dateToServer(context[k]);
}
}
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 7a6bca6aa7a5e..f28190d27003a 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -1505,7 +1505,7 @@
-
+
diff --git a/addons/web_editor/static/src/js/backend/convert_inline.js b/addons/web_editor/static/src/js/backend/convert_inline.js
index b6b36a388efe4..0c985ba761f97 100644
--- a/addons/web_editor/static/src/js/backend/convert_inline.js
+++ b/addons/web_editor/static/src/js/backend/convert_inline.js
@@ -175,7 +175,7 @@ function getMatchedCSSRules(a) {
break;
}
$el = $el.parent();
- } while (!$el.is('html'));
+ } while ($el.length && !$el.is('html'));
}
return style;
diff --git a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js
index b4e783a5f6d4c..b8c6057bb0e9f 100644
--- a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js
+++ b/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js
@@ -392,6 +392,7 @@ var SnippetEditor = Widget.extend({
*/
_onDragAndDropStart: function () {
var self = this;
+ this.dropped = false;
self.size = {
width: self.$target.width(),
height: self.$target.height()
@@ -439,10 +440,21 @@ var SnippetEditor = Widget.extend({
* 'move' button.
*
* @private
+ * @param {Event} ev
+ * @param {Object} ui
*/
- _onDragAndDropStop: function () {
+ _onDragAndDropStop: function (ev, ui) {
this.$editable.find('.oe_drop_zone').droppable('destroy').remove();
+ // TODO lot of this is duplicated code of the d&d feature of snippets
+ if (!this.dropped) {
+ var $el = $.nearest({x: ui.position.left, y: ui.position.top}, '.oe_drop_zone').first();
+ if ($el.length) {
+ $el.after(this.$target);
+ this.dropped = true;
+ }
+ }
+
var prev = this.$target.first()[0].previousSibling;
var next = this.$target.last()[0].nextSibling;
var $parent = this.$target.parent();
diff --git a/addons/web_editor/static/src/scss/web_editor.common.scss b/addons/web_editor/static/src/scss/web_editor.common.scss
index 6f83dc28524b5..76f167e0a7dd4 100644
--- a/addons/web_editor/static/src/scss/web_editor.common.scss
+++ b/addons/web_editor/static/src/scss/web_editor.common.scss
@@ -203,7 +203,7 @@ ul.oe_menu_editor {
@mixin o-spacing-all($factor: 1) {
// Generate vertical margin/padding classes used by the editor
@for $i from 0 through (256 / 8) {
- @include o-vspacing($i * 8);
+ @include o-vspacing($i * 8, $factor);
}
@include o-vspacing(4, $factor);
diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py
index 0280dbd71c798..1174f91d40e16 100644
--- a/addons/website_event/controllers/main.py
+++ b/addons/website_event/controllers/main.py
@@ -181,10 +181,7 @@ def event(self, event, **post):
def event_register(self, event, **post):
if not event.can_access_from_current_website():
raise werkzeug.exceptions.NotFound()
- if not request.context.get('pricelist'):
- pricelist = request.website.get_current_pricelist()
- if pricelist:
- event = event.with_context(pricelist=pricelist.id)
+
values = {
'event': event,
'main_object': event,
diff --git a/addons/website_event_sale/controllers/main.py b/addons/website_event_sale/controllers/main.py
index 689fe5cdc81ac..acf5f58be2d98 100644
--- a/addons/website_event_sale/controllers/main.py
+++ b/addons/website_event_sale/controllers/main.py
@@ -11,6 +11,10 @@ class WebsiteEventSaleController(WebsiteEventController):
@http.route()
def event_register(self, event, **post):
event = event.with_context(pricelist=request.website.id)
+ if not request.context.get('pricelist'):
+ pricelist = request.website.get_current_pricelist()
+ if pricelist:
+ event = event.with_context(pricelist=pricelist.id)
return super(WebsiteEventSaleController, self).event_register(event, **post)
def _process_tickets_details(self, data):
diff --git a/addons/website_forum/static/src/scss/website_forum.scss b/addons/website_forum/static/src/scss/website_forum.scss
index 391adf9d79224..aadd4802fd4ac 100644
--- a/addons/website_forum/static/src/scss/website_forum.scss
+++ b/addons/website_forum/static/src/scss/website_forum.scss
@@ -223,3 +223,7 @@ img.o_forum_avatar_big {
height: $owprofile-tabs-height;
background-color: #66445e;
}
+
+.website_forum {
+ margin-bottom: $spacer;
+}
diff --git a/doc/cla/corporate/inspur.md b/doc/cla/corporate/inspur.md
index 88999bdf10b2c..0a6ad6352c17e 100644
--- a/doc/cla/corporate/inspur.md
+++ b/doc/cla/corporate/inspur.md
@@ -15,3 +15,4 @@ List of contributors:
Brain Wang wangbuke@inspur.com
David Yu yudw@inspur.com
Alex Aisin-Gioro wubai@inspur.com
+Colin Li lizheng02@inspur.com https://github.com/Colinliz
\ No newline at end of file
diff --git a/doc/cla/individual/iledarn.md b/doc/cla/individual/iledarn.md
new file mode 100644
index 0000000000000..0f174ec032279
--- /dev/null
+++ b/doc/cla/individual/iledarn.md
@@ -0,0 +1,11 @@
+Russia, 2019-03-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,
+
+Ildar Nasyrov iledarn@gmail.com https://github.com/iledarn
diff --git a/doc/webservices/upgrade.rst b/doc/webservices/upgrade.rst
index fa168935b2385..92984b4b29d9a 100644
--- a/doc/webservices/upgrade.rst
+++ b/doc/webservices/upgrade.rst
@@ -55,7 +55,7 @@ The ``create`` method
:param str contract: (required) your enterprise contract reference
:param str email: (required) your email address
- :param str target: (required) the Odoo version you want to upgrade to. Valid choices: 6.0, 6.1, 7.0, 8.0
+ :param str target: (required) the Odoo version you want to upgrade to. Valid choices: 10.0, 11.0, 12.0
:param str aim: (required) the purpose of your upgrade database request. Valid choices: test, production.
:param str filename: (required) a purely informative name for you database dump file
:param str timezone: (optional) the timezone used by your server. Only for Odoo source version < 6.1
@@ -96,10 +96,9 @@ See a sample output aside.
"failures": [
{
"expected": [
- "6.0",
- "6.1",
- "7.0",
- "8.0",
+ "10.0",
+ "11.0",
+ "12.0",
],
"message": "Invalid value \"5.0\"",
"reason": "TARGET:INVALID",
@@ -135,7 +134,7 @@ Sample script
Here are 2 examples of database upgrade request creation using:
-* one in the python programming language using the pycurl library
+* one in the python programming language using the requests library
* one in the bash programming language using `curl `_ (tool
for transfering data using http) and `jq `_ (JSON processor):
@@ -145,15 +144,12 @@ Here are 2 examples of database upgrade request creation using:
.. code-block:: python
- from urllib import urlencode
- from io import BytesIO
- import pycurl
- import json
+ import requests
CREATE_URL = "https://upgrade.odoo.com/database/v1/create"
CONTRACT = "M123456-abcdef"
AIM = "test"
- TARGET = "8.0"
+ TARGET = "12.0"
EMAIL = "john.doe@example.com"
FILENAME = "db_name.dump"
@@ -164,27 +160,15 @@ Here are 2 examples of database upgrade request creation using:
('contract', CONTRACT),
('target', TARGET),
])
- postfields = urlencode(fields)
- c = pycurl.Curl()
- c.setopt(pycurl.URL, CREATE_URL)
- c.setopt(c.POSTFIELDS, postfields)
- data = BytesIO()
- c.setopt(c.WRITEFUNCTION, data.write)
- c.perform()
-
- # transform output into a dict:
- response = json.loads(data.getvalue())
-
- # get http status:
- http_code = c.getinfo(pycurl.HTTP_CODE)
- c.close()
+ r = requests.get(CREATE_URL, data=fields)
+ print(r.text)
.. code-block:: bash
CONTRACT=M123456-abcdef
AIM=test
- TARGET=8.0
+ TARGET=12.0
EMAIL=john.doe@example.com
FILENAME=db_name.dump
CREATE_URL="https://upgrade.odoo.com/database/v1/create"
@@ -235,33 +219,19 @@ should be empty if everything went fine.
.. code-block:: python
- import os
- import pycurl
- from urllib import urlencode
+ import requests
UPLOAD_URL = "https://upgrade.odoo.com/database/v1/upload"
- DUMPFILE = "openchs.70.cdump"
+ DUMPFILE = "/tmp/dump.sql"
fields = dict([
('request', '10534'),
('key', 'Aw7pItGVKFuZ_FOR3U8VFQ=='),
])
headers = {"Content-Type": "application/octet-stream"}
- postfields = urlencode(fields)
-
- c = pycurl.Curl()
- c.setopt(pycurl.URL, UPLOAD_URL+"?"+postfields)
- c.setopt(pycurl.POST, 1)
- filesize = os.path.getsize(DUMPFILE)
- c.setopt(pycurl.POSTFIELDSIZE, filesize)
- fp = open(DUMPFILE, 'rb')
- c.setopt(pycurl.READFUNCTION, fp.read)
- c.setopt(
- pycurl.HTTPHEADER,
- ['%s: %s' % (k, headers[k]) for k in headers])
-
- c.perform()
- c.close()
+
+ with open(DUMPFILE, 'rb') as f:
+ requests.post(UPLOAD_URL, data=f, params=fields, headers=headers)
.. code-block:: bash
@@ -311,29 +281,20 @@ The ``request_sftp_access`` method returns a JSON dictionary containing the foll
.. code-block:: python
- import os
- import pycurl
- from urllib import urlencode
+ import requests
UPLOAD_URL = "https://upgrade.odoo.com/database/v1/request_sftp_access"
- SSH_KEYS="/path/to/your/authorized_keys"
+ SSH_KEY = "$HOME/.ssh/id_rsa.pub"
+ SSH_KEY_CONTENT = open(SSH_KEY,'r').read()
fields = dict([
('request', '10534'),
('key', 'Aw7pItGVKFuZ_FOR3U8VFQ=='),
+ ('ssh_keys', SSH_KEY_CONTENT)
])
- postfields = urlencode(fields)
- c = pycurl.Curl()
- c.setopt(pycurl.URL, UPLOAD_URL+"?"+postfields)
- c.setopt(pycurl.POST, 1)
- c.setopt(c.HTTPPOST,[("ssh_keys",
- (c.FORM_FILE, SSH_KEYS,
- c.FORM_CONTENTTYPE, "text/plain"))
- ])
-
- c.perform()
- c.close()
+ r = requests.post(UPLOAD_URL, params=fields)
+ print(r.text)
.. code-block:: bash
@@ -442,10 +403,7 @@ should be empty if everything went fine.
.. code-block:: python
- from urllib import urlencode
- from io import BytesIO
- import pycurl
- import json
+ import requests
PROCESS_URL = "https://upgrade.odoo.com/database/v1/process"
@@ -453,25 +411,66 @@ should be empty if everything went fine.
('request', '10534'),
('key', 'Aw7pItGVKFuZ_FOR3U8VFQ=='),
])
- postfields = urlencode(fields)
- c = pycurl.Curl()
- c.setopt(pycurl.URL, PROCESS_URL)
- c.setopt(c.POSTFIELDS, postfields)
- data = BytesIO()
- c.setopt(c.WRITEFUNCTION, data.write)
- c.perform()
+ r = requests.get(PROCESS_URL, data=fields)
+ print(r.text)
+
+ .. code-block:: bash
+
+ PROCESS_URL="https://upgrade.odoo.com/database/v1/process"
+ KEY="Aw7pItGVKFuZ_FOR3U8VFQ=="
+ REQUEST_ID="10534"
+ URL_PARAMS="key=${KEY}&request=${REQUEST_ID}"
+ curl -sS "${PROCESS_URL}?${URL_PARAMS}"
+
+.. _upgrade-api-status-method:
+
+
+Asking to skip the tests
+=========================
+
+This action asks the Upgrade Platform to skip the tests for your request.
+If you don't want Odoo to test and validate the migration, you can bypass the testing stage and directly get the migrated dump.
+
+The ``skip_test`` method
+----------------------
+
+.. py:function:: https://upgrade.odoo.com/database/v1/skip_test
- # transform output into a dict:
- response = json.loads(data.getvalue())
+ Skip the tests, deliver the upgraded dump, and set the state to 'delivered'
- # get http status:
- http_code = c.getinfo(pycurl.HTTP_CODE)
- c.close()
+ :param str key: (required) your private key
+ :param str request: (required) your request id
+ :return: request result
+ :rtype: JSON dictionary
+
+The request id and the private key are obtained using the :ref:`create method
+`
+
+The result is a JSON dictionary containing the list of ``failures``, which
+should be empty if everything went fine.
+
+.. rst-class:: setup doc-aside
+
+.. switcher::
+
+ .. code-block:: python
+
+ import requests
+
+ PROCESS_URL = "https://upgrade.odoo.com/database/v1/skip_test"
+
+ fields = dict([
+ ('request', '10534'),
+ ('key', 'Aw7pItGVKFuZ_FOR3U8VFQ=='),
+ ])
+
+ r = requests.get(PROCESS_URL, data=fields)
+ print(r.text)
.. code-block:: bash
- PROCESS_URL="https://upgrade.odoo.com/database/v1/process"
+ PROCESS_URL="https://upgrade.odoo.com/database/v1/skip_test"
KEY="Aw7pItGVKFuZ_FOR3U8VFQ=="
REQUEST_ID="10534"
URL_PARAMS="key=${KEY}&request=${REQUEST_ID}"
@@ -508,30 +507,17 @@ database upgrade request.
.. code-block:: python
- from urllib import urlencode
- from io import BytesIO
- import pycurl
- import json
+ import requests
- STATUS_URL = "https://upgrade.odoo.com/database/v1/status"
+ PROCESS_URL = "https://upgrade.odoo.com/database/v1/status"
fields = dict([
('request', '10534'),
('key', 'Aw7pItGVKFuZ_FOR3U8VFQ=='),
])
- postfields = urlencode(fields)
-
- c = pycurl.Curl()
- c.setopt(pycurl.URL, PROCESS_URL)
- c.setopt(c.POSTFIELDS, postfields)
- data = BytesIO()
- c.setopt(c.WRITEFUNCTION, data.write)
- c.perform()
-
- # transform output into a dict:
- response = json.loads(data.getvalue())
- c.close()
+ r = requests.get(PROCESS_URL, data=fields)
+ print(r.text)
.. code-block:: bash
@@ -615,7 +601,7 @@ The ``request`` key contains various useful information about your request:
"id": 10534,
"key": "Aw7pItGVKFuZ_FOR3U8VFQ==",
"email": "john.doe@example.com",
- "target": "8.0",
+ "target": "12.0",
"aim": "test",
"filename": "db_name.dump",
"timezone": null,
@@ -631,7 +617,7 @@ The ``request`` key contains various useful information about your request:
"modules_url": "https://upgrade.odoo.com/database/eu1/10534/Aw7pItGVKFuZ_FOR3U8VFQ==/modules/archive",
"filesize": "912.99 Kb",
"database_uuid": null,
- "created_at": "2015-09-09 07:13:49",
+ "created_at": "2018-09-09 07:13:49",
"estimated_time": null,
"processed_at": null,
"elapsed": "00:00",
diff --git a/odoo/addons/base/models/ir_logging.py b/odoo/addons/base/models/ir_logging.py
index 8d8a21f6d6cea..04acbe3387305 100644
--- a/odoo/addons/base/models/ir_logging.py
+++ b/odoo/addons/base/models/ir_logging.py
@@ -1,5 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import fields, models
+from odoo import api, fields, models
class IrLogging(models.Model):
@@ -7,8 +7,22 @@ class IrLogging(models.Model):
_description = 'Logging'
_order = 'id DESC'
- create_date = fields.Datetime(readonly=True)
- create_uid = fields.Integer(string='Uid', readonly=True) # Integer not m2o is intentionnal
+ # The _log_access fields are defined manually for the following reasons:
+ #
+ # - The entries in ir_logging are filled in with sql queries bypassing the orm. As the --log-db
+ # cli option allows to insert ir_logging entries into a remote database, the one2many *_uid
+ # fields make no sense in the first place but we will keep it for backward compatibility.
+ #
+ # - Also, when an ir_logging entry is triggered by the orm (when using --log-db) at the moment
+ # it is making changes to the res.users model, the ALTER TABLE will aquire an exclusive lock
+ # on res_users, preventing the ir_logging INSERT to be processed, hence the ongoing module
+ # install/update will hang forever as the orm is blocked by the ir_logging query that will
+ # never occur.
+ create_uid = fields.Integer(string='Created by', readonly=True)
+ create_date = fields.Datetime(string='Created on', readonly=True)
+ write_uid = fields.Integer(string='Last Updated by', readonly=True)
+ write_date = fields.Datetime(string='Last Updated on', readonly=True)
+
name = fields.Char(required=True)
type = fields.Selection([('client', 'Client'), ('server', 'Server')], required=True, index=True)
dbname = fields.Char(string='Database Name', index=True)
@@ -17,3 +31,8 @@ class IrLogging(models.Model):
path = fields.Char(required=True)
func = fields.Char(string='Function', required=True)
line = fields.Char(required=True)
+
+ @api.model_cr
+ def init(self):
+ super(IrLogging, self).init()
+ self._cr.execute("ALTER TABLE ir_logging DROP CONSTRAINT IF EXISTS ir_logging_write_uid_fkey")
diff --git a/odoo/addons/base/models/res_partner.py b/odoo/addons/base/models/res_partner.py
index 25c183670a402..b7f6a5e3f89e4 100644
--- a/odoo/addons/base/models/res_partner.py
+++ b/odoo/addons/base/models/res_partner.py
@@ -543,6 +543,8 @@ def write(self, vals):
@api.model_create_multi
def create(self, vals_list):
+ if self.env.context.get('import_file'):
+ self._check_import_consistency(vals_list)
for vals in vals_list:
if vals.get('website'):
vals['website'] = self._clean_website(vals['website'])
@@ -866,6 +868,26 @@ def get_import_templates(self):
'template': '/base/static/xls/res_partner.xls'
}]
+ @api.model
+ def _check_import_consistency(self, vals_list):
+ """
+ The values created by an import are generated by a name search, field by field.
+ As a result there is no check that the field values are consistent with each others.
+ We check that if the state is given a value, it does belong to the given country, or we remove it.
+ """
+ States = self.env['res.country.state']
+ states_ids = {vals['state_id'] for vals in vals_list if vals.get('state_id')}
+ state_to_country = States.search([('id', 'in', list(states_ids))]).read(['country_id'])
+ for vals in vals_list:
+ if vals.get('state_id'):
+ country_id = next(c['country_id'][0] for c in state_to_country if c['id'] == vals.get('state_id'))
+ state = States.browse(vals['state_id'])
+ if state.country_id.id != country_id:
+ state_domain = [('code', '=', state.code),
+ ('country_id', '=', country_id)]
+ state = States.search(state_domain, limit=1)
+ vals['state_id'] = state.id # replace state or remove it if not found
+
@api.multi
def _get_country_name(self):
return self.country_id.name or ''
diff --git a/odoo/addons/base/tests/test_translate.py b/odoo/addons/base/tests/test_translate.py
index 0ae6ab1007feb..f6a556be937bf 100644
--- a/odoo/addons/base/tests/test_translate.py
+++ b/odoo/addons/base/tests/test_translate.py
@@ -130,6 +130,19 @@ def test_translate_xml_inline4(self):
self.assertItemsEqual(terms,
['Form stuff', ''])
+ def test_translate_xml_inline5(self):
+ """ Test xml_translate() with inline elements with empty translated attrs only. """
+ terms = []
+ source = """"""
+ result = xml_translate(terms.append, source)
+ self.assertEquals(result, source)
+ self.assertItemsEqual(terms, ['Form stuff'])
+
def test_translate_xml_t(self):
""" Test xml_translate() with t-* attributes. """
terms = []
diff --git a/odoo/service/server.py b/odoo/service/server.py
index 68ea2c002ace6..977cc0a363016 100644
--- a/odoo/service/server.py
+++ b/odoo/service/server.py
@@ -25,9 +25,25 @@
# Unix only for workers
import fcntl
import resource
+ try:
+ import inotify
+ from inotify.adapters import InotifyTrees
+ from inotify.constants import IN_MODIFY, IN_CREATE, IN_MOVED_TO
+ INOTIFY_LISTEN_EVENTS = IN_MODIFY | IN_CREATE | IN_MOVED_TO
+ except ImportError:
+ inotify = None
else:
# Windows shim
signal.SIGHUP = -1
+ inotify = None
+
+if not inotify:
+ try:
+ import watchdog
+ from watchdog.observers import Observer
+ from watchdog.events import FileCreatedEvent, FileModifiedEvent, FileMovedEvent
+ except ImportError:
+ watchdog = None
# Optional process names for workers
try:
@@ -45,13 +61,6 @@
_logger = logging.getLogger(__name__)
-try:
- import watchdog
- from watchdog.observers import Observer
- from watchdog.events import FileCreatedEvent, FileModifiedEvent, FileMovedEvent
-except ImportError:
- watchdog = None
-
SLEEP_INTERVAL = 60 # 1 min
def memory_info(process):
@@ -181,7 +190,24 @@ def _handle_request_noblock(self):
#----------------------------------------------------------
# FileSystem Watcher for autoreload and cache invalidation
#----------------------------------------------------------
-class FSWatcher(object):
+class FSWatcherBase(object):
+ def handle_file(self, path):
+ if path.endswith('.py') and not os.path.basename(path).startswith('.~'):
+ try:
+ source = open(path, 'rb').read() + b'\n'
+ compile(source, path, 'exec')
+ except IOError:
+ _logger.error('autoreload: python code change detected, IOError for %s', path)
+ except SyntaxError:
+ _logger.error('autoreload: python code change detected, SyntaxError in %s', path)
+ else:
+ if not getattr(odoo, 'phoenix', False):
+ _logger.info('autoreload: python code updated, autoreload activated')
+ restart()
+ return True
+
+
+class FSWatcherWatchdog(FSWatcherBase):
def __init__(self):
self.observer = Observer()
for path in odoo.modules.module.ad_paths:
@@ -192,27 +218,60 @@ def dispatch(self, event):
if isinstance(event, (FileCreatedEvent, FileModifiedEvent, FileMovedEvent)):
if not event.is_directory:
path = getattr(event, 'dest_path', event.src_path)
- if path.endswith('.py') and not os.path.basename(path).startswith('.~'):
- try:
- source = open(path, 'rb').read() + b'\n'
- compile(source, path, 'exec')
- except FileNotFoundError:
- _logger.error('autoreload: python code change detected, FileNotFound for %s', path)
- except SyntaxError:
- _logger.error('autoreload: python code change detected, SyntaxError in %s', path)
- else:
- if not getattr(odoo, 'phoenix', False):
- _logger.info('autoreload: python code updated, autoreload activated')
- restart()
+ self.handle_file(path)
def start(self):
self.observer.start()
- _logger.info('AutoReload watcher running')
+ _logger.info('AutoReload watcher running with watchdog')
def stop(self):
self.observer.stop()
self.observer.join()
+
+class FSWatcherInotify(FSWatcherBase):
+ def __init__(self):
+ self.started = False
+ # ignore warnings from inotify in case we have duplicate addons paths.
+ inotify.adapters._LOGGER.setLevel(logging.ERROR)
+ # recreate a list as InotifyTrees' __init__ deletes the list's items
+ paths_to_watch = []
+ for path in odoo.modules.module.ad_paths:
+ paths_to_watch.append(path)
+ _logger.info('Watching addons folder %s', path)
+ self.watcher = InotifyTrees(paths_to_watch, mask=INOTIFY_LISTEN_EVENTS, block_duration_s=.5)
+
+ def run(self):
+ _logger.info('AutoReload watcher running with inotify')
+ dir_creation_events = set(('IN_MOVED_TO', 'IN_CREATE'))
+ while self.started:
+ for event in self.watcher.event_gen(timeout_s=0, yield_nones=False):
+ (_, type_names, path, filename) = event
+ if 'IN_ISDIR' not in type_names:
+ # despite not having IN_DELETE in the watcher's mask, the
+ # watcher sends these events when a directory is deleted.
+ if 'IN_DELETE' not in type_names:
+ full_path = os.path.join(path, filename)
+ if self.handle_file(full_path):
+ return
+ elif dir_creation_events.intersection(type_names):
+ full_path = os.path.join(path, filename)
+ for root, _, files in os.walk(full_path):
+ for file in files:
+ if self.handle_file(os.path.join(root, file)):
+ return
+
+ def start(self):
+ self.started = True
+ self.thread = threading.Thread(target=self.run, name="odoo.service.autoreload.watcher")
+ self.thread.setDaemon(True)
+ self.thread.start()
+
+ def stop(self):
+ self.started = False
+ self.thread.join()
+
+
#----------------------------------------------------------
# Servers: Threaded, Gevented and Prefork
#----------------------------------------------------------
@@ -883,6 +942,8 @@ def _runloop(self):
while self.alive:
self.multi.pipe_ping(self.watchdog_pipe)
self.sleep()
+ if not self.alive:
+ break
self.process_work()
self.check_limits()
except:
@@ -1113,21 +1174,28 @@ def start(preload=None, stop=False):
server = ThreadedServer(odoo.service.wsgi_server.application)
watcher = None
- if 'reload' in config['dev_mode']:
- if watchdog:
- watcher = FSWatcher()
+ if 'reload' in config['dev_mode'] and not odoo.evented:
+ if inotify:
+ watcher = FSWatcherInotify()
+ watcher.start()
+ elif watchdog:
+ watcher = FSWatcherWatchdog()
watcher.start()
else:
- _logger.warning("'watchdog' module not installed. Code autoreload feature is disabled")
+ if os.name == 'posix' and platform.system() != 'Darwin':
+ module = 'inotify'
+ else:
+ module = 'watchdog'
+ _logger.warning("'%s' module not installed. Code autoreload feature is disabled", module)
if 'werkzeug' in config['dev_mode']:
server.app = DebuggedApplication(server.app, evalex=True)
rc = server.run(preload, stop)
+ if watcher:
+ watcher.stop()
# like the legend of the phoenix, all ends with beginnings
if getattr(odoo, 'phoenix', False):
- if watcher:
- watcher.stop()
_reexec()
return rc if rc else 0
diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py
index e20ebf9fed790..5760a39f61c53 100644
--- a/odoo/tools/translate.py
+++ b/odoo/tools/translate.py
@@ -257,7 +257,7 @@ def process(node):
result.tail = node.tail
has_text = (
todo_has_text or nonspace(result.text) or nonspace(result.tail)
- or any(name in TRANSLATED_ATTRS for name in result.attrib)
+ or any((key in TRANSLATED_ATTRS and val) for key, val in result.attrib.items())
)
return (has_text, result)
|