From e1769cbea9dc9b13453f507321b4de2e86de2d36 Mon Sep 17 00:00:00 2001
From: amoyaux
Date: Tue, 10 Oct 2017 14:01:41 +0200
Subject: [PATCH 001/153] [FIX] base_import: parse date and float for
relational fields
Use case to reproduce:
-> Import a file with a float using a coma related to the model
(for example the product.supplierinfo/price)
It happens because the method that parse the date and float field
only compare them to the main imported model.
This commit complete the method by using a recursion on relational
model in order to also parse the relational subfields.
---
addons/base_import/models/base_import.py | 17 +++++++++++--
addons/base_import/tests/test_base_import.py | 26 ++++++++++++++++++++
2 files changed, 41 insertions(+), 2 deletions(-)
diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py
index 9f1804395ce..2727663b68c 100644
--- a/addons/base_import/models/base_import.py
+++ b/addons/base_import/models/base_import.py
@@ -605,9 +605,18 @@ class Import(models.TransientModel):
@api.multi
def _parse_import_data(self, data, import_fields, options):
+ """ Lauch first call to _parse_import_data_recursive with an
+ empty prefix. _parse_import_data_recursive will be run
+ recursively for each relational field.
+ """
+ return self._parse_import_data_recursive(self.res_model, '', data, import_fields, options)
+
+ @api.multi
+ def _parse_import_data_recursive(self, model, prefix, data, import_fields, options):
# Get fields of type date/datetime
- all_fields = self.env[self.res_model].fields_get()
+ all_fields = self.env[model].fields_get()
for name, field in all_fields.iteritems():
+ name = prefix + name
if field['type'] in ('date', 'datetime') and name in import_fields:
# Parse date
index = import_fields.index(name)
@@ -624,7 +633,11 @@ class Import(models.TransientModel):
raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, ustr(e.message)))
except Exception, e:
raise ValueError(_("Error Parsing Date [%s:L%d]: %s") % (name, num + 1, ustr(e.message)))
-
+ # Check if the field is in import_field and is a relational (followed by /)
+ # Also verify that the field name exactly match the import_field at the correct level.
+ elif any(name + '/' in import_field and name == import_field.split('/')[prefix.count('/')] for import_field in import_fields):
+ # Recursive call with the relational as new model and add the field name to the prefix
+ self._parse_import_data_recursive(field['relation'], name + '/', data, import_fields, options)
elif field['type'] in ('float', 'monetary') and name in import_fields:
# Parse float, sometimes float values from file have currency symbol or () to denote a negative value
# We should be able to manage both case
diff --git a/addons/base_import/tests/test_base_import.py b/addons/base_import/tests/test_base_import.py
index 891fc3e7671..f858101fef3 100644
--- a/addons/base_import/tests/test_base_import.py
+++ b/addons/base_import/tests/test_base_import.py
@@ -402,7 +402,33 @@ class test_convert_import_data(TransactionCase):
# if results empty, no errors
self.assertItemsEqual(results, [])
+ def test_parse_relational_fields(self):
+ """ Ensure that relational fields float and date are correctly
+ parsed during the import call.
+ """
+ import_wizard = self.env['base_import.import'].create({
+ 'res_model': 'res.partner',
+ 'file': 'name,parent_id/id,parent_id/date,parent_id/credit_limit\n'
+ '"foo","__export__.res_partner_1","2017年10月12日","5,69"\n',
+ 'file_type': 'text/csv'
+ })
+ options = {
+ 'date_format': '%Y年%m月%d日',
+ 'quoting': '"',
+ 'separator': ',',
+ 'float_decimal_separator': ',',
+ 'float_thousand_separator': '.',
+ 'headers': True
+ }
+ data, import_fields = import_wizard._convert_import_data(
+ ['name', 'parent_id/.id', 'parent_id/date', 'parent_id/credit_limit'],
+ options
+ )
+ result = import_wizard._parse_import_data(data, import_fields, options)
+ # Check if the data 5,69 as been correctly parsed.
+ self.assertEqual(float(result[0][-1]), 5.69)
+ self.assertEqual(str(result[0][-2]), '2017-10-12')
def test_filtered(self):
""" If ``False`` is provided as field mapping for a column,
From 8f1d889e50e18d81db7d4e8bb0555513a2b1019c Mon Sep 17 00:00:00 2001
From: Martin Trigaux
Date: Fri, 13 Oct 2017 10:28:04 +0200
Subject: [PATCH 002/153] [FIX] tools: correctly export the non-module
translations
8ac39287 introdcude a way to export the source terms not located inside an
addons module (e.g. error messages in openerp/service/models.py)
0529a7f9 fixed a bug in the get_module_from_path with addons path with similar
names.
The above commit introduced a regression making files located outside of an
addons path to be wrongly considered as a module
e.g. openerp/service/models.py used not to match any module and was considered
correctly as belonging to base module
After, 0529a7f9, '~/openerp/service' being different than '~/openerp/service/'
the module was considered as 'models.py' (which is incorrect).
Compare correctly both parent path to have a correct match
Fixes # 19907
---
openerp/tools/translate.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py
index 6929b31f954..886a87860b3 100644
--- a/openerp/tools/translate.py
+++ b/openerp/tools/translate.py
@@ -958,7 +958,8 @@ def trans_generate(lang, modules, cr):
def get_module_from_path(path):
for (mp, rec) in path_list:
mp = os.path.join(mp, '')
- if rec and path.startswith(mp) and os.path.dirname(path) != mp:
+ dirname = os.path.join(os.path.dirname(path), '')
+ if rec and path.startswith(mp) and dirname != mp:
path = path[len(mp):]
return path.split(os.path.sep)[0]
return 'base' # files that are not in a module are considered as being in 'base' module
From c68ad2e0ccac6654e5ac0ddfc294b81007b2d915 Mon Sep 17 00:00:00 2001
From: fda-odoo
Date: Fri, 13 Oct 2017 09:27:07 +0200
Subject: [PATCH 003/153] [FIX] crm: remove duplicate filter in seach bar of
pipeline report
The search bar of the pipeline report page had duplicate filters. For each, there was one with "selection" widget and one without the widget.
opw-771298
---
addons/crm/report/crm_opportunity_report_views.xml | 7 -------
1 file changed, 7 deletions(-)
diff --git a/addons/crm/report/crm_opportunity_report_views.xml b/addons/crm/report/crm_opportunity_report_views.xml
index 8f2c96bfb2a..6eb888b2662 100644
--- a/addons/crm/report/crm_opportunity_report_views.xml
+++ b/addons/crm/report/crm_opportunity_report_views.xml
@@ -113,17 +113,10 @@
-
-
-
-
-
-
-
From c0a47980fc2458b2ef5f1e0c689842c8eafdde06 Mon Sep 17 00:00:00 2001
From: Jeremy Kersten
Date: Thu, 12 Oct 2017 14:28:36 +0200
Subject: [PATCH 004/153] [FIX] base: change width by default for apps
description
---
addons/crm/static/description/index.html | 2 +-
.../base/static/src/css/description.css | 39 +++++++++----------
.../base/static/src/css/description.sass | 36 ++++++++---------
3 files changed, 38 insertions(+), 39 deletions(-)
diff --git a/addons/crm/static/description/index.html b/addons/crm/static/description/index.html
index df8b9a447f8..6f4ab397d69 100644
--- a/addons/crm/static/description/index.html
+++ b/addons/crm/static/description/index.html
@@ -43,7 +43,7 @@
Automate activities based on your sales scripts: calls, meetings, mails and quotations. Get all information you need, right on the opportunity: page viewed, mails received, etc.
-
+
diff --git a/odoo/addons/base/static/src/css/description.css b/odoo/addons/base/static/src/css/description.css
index cbfa3d5e968..b1f436b872f 100644
--- a/odoo/addons/base/static/src/css/description.css
+++ b/odoo/addons/base/static/src/css/description.css
@@ -1,5 +1,4 @@
-@charset "utf-8";
-/*
+/**
* This CSS is for the html description of modules
* TODO clean
*/
@@ -78,11 +77,11 @@
/* ------ BASE GRID CONSTRUCTS ----- */
.oe_page {
margin: 0px auto 64px auto;
- max-width: 992px;
+ max-width: 100%;
}
.oe_row {
- width: 928px;
+ width: 100%;
margin-top: 16px;
margin-bottom: 16px;
margin-left: auto;
@@ -111,35 +110,35 @@
}
.oe_span12 {
- width: 928px;
+ width: 100%;
}
.oe_span10 {
- width: 773px;
+ width: 83.33333%;
}
.oe_span9 {
- width: 696px;
+ width: 75%;
}
.oe_span8 {
- width: 618px;
+ width: 66.66667%;
}
.oe_span6 {
- width: 464px;
+ width: 50%;
}
.oe_span4 {
- width: 309px;
+ width: 33.33333%;
}
.oe_span3 {
- width: 232px;
+ width: 25%;
}
.oe_span2 {
- width: 154px;
+ width: 16.66667%;
}
[class*='oe_span'].oe_fit {
@@ -162,28 +161,28 @@
width: auto;
}
.oe_row.oe_flex .oe_span12 {
- max-width: 928px;
+ max-width: 100%;
}
.oe_row.oe_flex .oe_span10 {
- max-width: 769px;
+ max-width: 83.33333%;
}
.oe_row.oe_flex .oe_span9 {
- max-width: 692px;
+ max-width: 75%;
}
.oe_row.oe_flex .oe_span8 {
- max-width: 614px;
+ max-width: 66.66667%;
}
.oe_row.oe_flex .oe_span6 {
- max-width: 460px;
+ max-width: 50%;
}
.oe_row.oe_flex .oe_span4 {
- max-width: 305px;
+ max-width: 33.33333%;
}
.oe_row.oe_flex .oe_span3 {
- max-width: 228px;
+ max-width: 25%;
}
.oe_row.oe_flex .oe_span2 {
- max-width: 150px;
+ max-width: 16.66667%;
}
.oe_mb0 {
diff --git a/odoo/addons/base/static/src/css/description.sass b/odoo/addons/base/static/src/css/description.sass
index 4fa2ed100d8..49be6054652 100644
--- a/odoo/addons/base/static/src/css/description.sass
+++ b/odoo/addons/base/static/src/css/description.sass
@@ -86,11 +86,11 @@ $v8_anchor_visited_color: rgb(91, 40, 79)
.oe_page
margin: 0px auto 64px auto
- max-width: 992px
+ max-width: 100%
.oe_row
- width: 928px
+ width: 100%
margin-top: 16px
margin-bottom: 16px
margin-left: auto
@@ -107,14 +107,14 @@ $v8_anchor_visited_color: rgb(91, 40, 79)
line-height: 0
height: 0
-$oe_span12_width: 928px
-$oe_span10_width: 773px
-$oe_span9_width: 696px
-$oe_span8_width: 618px
-$oe_span6_width: 464px
-$oe_span4_width: 309px
-$oe_span3_width: 232px
-$oe_span2_width: 154px
+$oe_span12_width: 100%
+$oe_span10_width: 83.33333333%
+$oe_span9_width: 75%
+$oe_span8_width: 66.66666667%
+$oe_span6_width: 50%
+$oe_span4_width: 33.33333333%
+$oe_span3_width: 25%
+$oe_span2_width: 16.66666667%
[class*='oe_span']
float: left
@@ -155,19 +155,19 @@ $oe_span2_width: 154px
.oe_span12
max-width: $oe_span12_width
.oe_span10
- max-width: ($oe_span10_width +-4px)
+ max-width: $oe_span10_width
.oe_span9
- max-width: ($oe_span9_width +-4px)
- .oe_span8
- max-width: ($oe_span8_width +-4px)
+ max-width: $oe_span9_width
+ .oe_span8
+ max-width: $oe_span8_width
.oe_span6
- max-width: ($oe_span6_width +-4px)
+ max-width: $oe_span6_width
.oe_span4
- max-width: ($oe_span4_width +-4px)
+ max-width: $oe_span4_width
.oe_span3
- max-width: ($oe_span3_width +-4px)
+ max-width: $oe_span3_width
.oe_span2
- max-width: ($oe_span2_width +-4px)
+ max-width: $oe_span2_width
.oe_mb0
margin-bottom: 0px !important
From dc5bb0d6b2806e81d1dd32839e6a600c8782bad3 Mon Sep 17 00:00:00 2001
From: Jeremy Kersten
Date: Fri, 13 Oct 2017 10:50:24 +0200
Subject: [PATCH 005/153] [FIX] website_forum: add missing karma_answer field
in form view
This commit closes #19672
---
addons/website_forum/views/forum.xml | 1 +
1 file changed, 1 insertion(+)
diff --git a/addons/website_forum/views/forum.xml b/addons/website_forum/views/forum.xml
index a8b45e64c04..03dd07cda85 100644
--- a/addons/website_forum/views/forum.xml
+++ b/addons/website_forum/views/forum.xml
@@ -63,6 +63,7 @@
+
From 7fc6205419427ecefb250de6abc361e0422821b7 Mon Sep 17 00:00:00 2001
From: Raphael Collet
Date: Fri, 13 Oct 2017 12:13:50 +0200
Subject: [PATCH 006/153] [FIX] base_import_module: retrieve message from
exception (#20101)
---
odoo/loglevels.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/odoo/loglevels.py b/odoo/loglevels.py
index 400f08e1a8f..0972dc5d2b4 100644
--- a/odoo/loglevels.py
+++ b/odoo/loglevels.py
@@ -94,7 +94,7 @@ def ustr(value, hint_encoding='utf-8', errors='strict'):
def exception_to_unicode(e):
if (sys.version_info[:2] < (2,6)) and hasattr(e, 'message'):
return ustr(e.message)
- if hasattr(e, 'args'):
+ if getattr(e, 'args', ()):
return "\n".join((ustr(a) for a in e.args))
try:
return unicode(e)
From 51861e889ab7c8795cccc3eaca27b90b62ceb89c Mon Sep 17 00:00:00 2001
From: Josse Colpaert
Date: Thu, 12 Oct 2017 13:55:12 +0200
Subject: [PATCH 007/153] [FIX] mrp: no propagation of stock moves when
destination is a component of an mo
When you cancel or split a move, it will check the propagation setting
to see if it needs to cancel or split the next move too. If this next
move is a raw material in a manufacturing order however, it does not
make sense to do this as a lot of logic is also on these moves related
to bill of material lines, workorders, ...
opw-770062
---
addons/mrp/models/stock_move.py | 12 ++++++++++++
addons/stock/models/stock_move.py | 26 ++++++++++++++++----------
2 files changed, 28 insertions(+), 10 deletions(-)
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index da140337817..a87d6623dcc 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -138,6 +138,14 @@ class StockMove(models.Model):
self.check_move_lots()
return res
+ def _propagate_cancel(self):
+ self.ensure_one()
+ if not self.move_dest_id.raw_material_production_id:
+ super(StockMove, self)._propagate_cancel()
+ elif self.move_dest_id.state == 'waiting':
+ # If waiting, the chain will be broken and we are not sure if we can still wait for it (=> could take from stock instead)
+ self.move_dest_id.write({'state': 'confirmed'})
+
@api.multi
def action_cancel(self):
if any(move.quantity_done for move in self):
@@ -373,6 +381,10 @@ class StockMove(models.Model):
self.sudo().unlink()
return processed_moves
+ def _propagate_split(self, new_move, qty):
+ if not self.move_dest_id.raw_material_production_id:
+ super(StockMove, self)._propagate_split(new_move, qty)
+
def _generate_move_phantom(self, bom_line, quantity):
if bom_line.product_id.type in ['product', 'consu']:
return self.copy(default={
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 0455c5b95e3..30c988954d7 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -658,6 +658,15 @@ class StockMove(models.Model):
if not no_prepare:
self.check_recompute_pack_op()
+ def _propagate_cancel(self):
+ self.ensure_one()
+ if self.move_dest_id:
+ if self.propagate:
+ self.move_dest_id.action_cancel()
+ elif self.move_dest_id.state == 'waiting':
+ # If waiting, the chain will be broken and we are not sure if we can still wait for it (=> could take from stock instead)
+ self.move_dest_id.write({'state': 'confirmed'})
+
@api.multi
def action_cancel(self):
""" Cancels the moves and if all moves are cancelled it cancels the picking. """
@@ -674,12 +683,7 @@ class StockMove(models.Model):
pass
# procurements.search([('move_dest_id', '=', move.id)]).cancel()
else:
- if move.move_dest_id:
- if move.propagate:
- move.move_dest_id.action_cancel()
- elif move.move_dest_id.state == 'waiting':
- # If waiting, the chain will be broken and we are not sure if we can still wait for it (=> could take from stock instead)
- move.move_dest_id.write({'state': 'confirmed'})
+ move._propagate_cancel()
if move.procurement_id:
procurements |= move.procurement_id
@@ -919,6 +923,11 @@ class StockMove(models.Model):
raise UserError(_('You can only delete draft moves.'))
return super(StockMove, self).unlink()
+ def _propagate_split(self, new_move, qty):
+ if self.move_dest_id and self.propagate and self.move_dest_id.state not in ('done', 'cancel'):
+ new_move_prop = self.move_dest_id.split(qty)
+ new_move.write({'move_dest_id': new_move_prop})
+
@api.multi
def split(self, qty, restrict_lot_id=False, restrict_partner_id=False):
""" Splits qty from move move into a new move
@@ -959,10 +968,7 @@ class StockMove(models.Model):
# TDE CLEANME: used only in write in this file, to clean
# ctx['do_not_propagate'] = True
self.with_context(do_not_propagate=True, rounding_method='HALF-UP').write({'product_uom_qty': self.product_uom_qty - uom_qty})
-
- if self.move_dest_id and self.propagate and self.move_dest_id.state not in ('done', 'cancel'):
- new_move_prop = self.move_dest_id.split(qty)
- new_move.write({'move_dest_id': new_move_prop})
+ self._propagate_split(new_move, qty)
# returning the first element of list returned by action_confirm is ok because we checked it wouldn't be exploded (and
# thus the result of action_confirm should always be a list of 1 element length)
new_move.action_confirm()
From 26517172510bfff003e7f555aa63ab463334d05a Mon Sep 17 00:00:00 2001
From: Yannick Tivisse
Date: Mon, 14 Aug 2017 10:15:53 +0200
Subject: [PATCH 008/153] [IMP] base_import: Defer parent computation after
record creation/update
Currently when importing files, the parent left and right are recomputed on each record creation/update, which could lead to a lot a request.
This commit will defer the parent field recomputation to the end on the `load` function by recomputing the values from the MPTT and inserting directly the new values on the records.
Tested on a customer instance, this will reduce the execution time when importing a thousand stock.location from 15 minutes to several seconds.
---
addons/base_import/models/base_import.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/addons/base_import/models/base_import.py b/addons/base_import/models/base_import.py
index 2727663b68c..0be10bf6da8 100644
--- a/addons/base_import/models/base_import.py
+++ b/addons/base_import/models/base_import.py
@@ -681,7 +681,13 @@ class Import(models.TransientModel):
}]
_logger.info('importing %d rows...', len(data))
- import_result = self.env[self.res_model].with_context(import_file=True).load(import_fields, data)
+
+ model = self.env[self.res_model].with_context(import_file=True)
+ defer_parent_store = self.env.context.get('defer_parent_store_computation', True)
+ if defer_parent_store and model._parent_store:
+ model = model.with_context(defer_parent_store_computation=True)
+
+ import_result = model.load(import_fields, data)
_logger.info('done')
# If transaction aborted, RELEASE SAVEPOINT is going to raise
From 8c2c954785de3c841748969052a03c14b093a7f1 Mon Sep 17 00:00:00 2001
From: Christophe Simonis
Date: Fri, 13 Oct 2017 15:53:34 +0200
Subject: [PATCH 009/153] Revert "[FIX] hr_expense: avoid crash when
submitting"
Commit 46c1091f36803a5991c90982b80d43972a47ff24 was only a workaround.
Inability to submit expenses was actually a js bug that hass beed fixed
by 7d448d57e34511eb668a695e9238a2096c05411e.
Close #19877
---
addons/hr_expense/models/hr_expense.py | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index cfbd6e77967..17cbc2b14bf 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -109,17 +109,16 @@ class HrExpense(models.Model):
raise UserError(_("You cannot report twice the same line!"))
if len(self.mapped('employee_id')) != 1:
raise UserError(_("You cannot report expenses for different employees in the same report!"))
- expense_sheet = self.env['hr.expense.sheet'].create({
- 'expense_line_ids': [(4, line.id) for line in self],
- 'name': self[0].name if len(self.ids) == 1 else '',
- 'employee_id': self[0].employee_id.id,
- })
return {
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'hr.expense.sheet',
'target': 'current',
- 'res_id': expense_sheet.id,
+ 'context': {
+ 'default_expense_line_ids': [line.id for line in self],
+ 'default_employee_id': self[0].employee_id.id,
+ 'default_name': self[0].name if len(self.ids) == 1 else ''
+ }
}
def _prepare_move_line(self, line):
From 9866322a3d236ca9846457491b31cb0bcb503986 Mon Sep 17 00:00:00 2001
From: Richard Mathot
Date: Fri, 13 Oct 2017 16:22:08 +0200
Subject: [PATCH 010/153] [FIX] base: bad translation
---
odoo/addons/base/i18n/fr.po | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/odoo/addons/base/i18n/fr.po b/odoo/addons/base/i18n/fr.po
index cbf12b572ad..8b06e3f2f62 100644
--- a/odoo/addons/base/i18n/fr.po
+++ b/odoo/addons/base/i18n/fr.po
@@ -15600,7 +15600,7 @@ msgstr "Mois"
#. module: base
#: model:ir.ui.view,arch_db:base.sequence_view
msgid "Month: %(month)s"
-msgstr "Mois : %(mois)"
+msgstr "Mois : %(month)s"
#. module: base
#: selection:ir.cron,interval_type:0
From f4332118d3156e2ba2408c1d31ab92ee2066ef73 Mon Sep 17 00:00:00 2001
From: Nicolas Lempereur
Date: Fri, 13 Oct 2017 15:23:58 +0200
Subject: [PATCH 011/153] [FIX] mass_mailing: unsubscribe error if contacts
If the mass mailing was to mail contacts, this could cause an error if
the recipient unsubscribed. If the mass mailing was to mailing list,
unsubscribe would not work.
This change update unsubscribe to take into account changes in 765e752
and e15372c07 that allowed mailing list / mailing list contacts to be
used separately.
opw-770473
closes #20145
---
addons/mass_mailing/controllers/main.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py
index 276056311b8..2b93d4706d5 100644
--- a/addons/mass_mailing/controllers/main.py
+++ b/addons/mass_mailing/controllers/main.py
@@ -16,10 +16,10 @@ class MassMailController(http.Controller):
if mailing.exists():
res_id = res_id and int(res_id)
res_ids = []
- if mailing.mailing_model == 'mail.mass_mailing.contact':
+ if mailing.mailing_model == 'mail.mass_mailing.list':
contacts = request.env['mail.mass_mailing.contact'].sudo().search([
('email', '=', email),
- ('list_id', 'in', [mailing_list.id for mailing_list in mailing.contact_list_ids])
+ ('list_ids', 'in', [mailing_list.id for mailing_list in mailing.contact_list_ids])
])
res_ids = contacts.ids
else:
From 7392e476b8f8721260d0eb10413c7a52a25ca469 Mon Sep 17 00:00:00 2001
From: Simon Lejeune
Date: Thu, 12 Oct 2017 11:19:36 +0200
Subject: [PATCH 012/153] [FIX] stock_account: always call `_get_price_unit` in
`_action_done` of a move
It is possible that the price unit the move was created with is no
longer valid (for instance, one could change the price unit on the
purchase before validating the receipt, or the currency rate has changed
in the meantime).
Fixes #20035
---
addons/purchase/tests/__init__.py | 1 +
addons/purchase/tests/test_stockvaluation.py | 198 +++++++++++++++++++
addons/stock_account/models/stock.py | 2 +-
3 files changed, 200 insertions(+), 1 deletion(-)
create mode 100644 addons/purchase/tests/test_stockvaluation.py
diff --git a/addons/purchase/tests/__init__.py b/addons/purchase/tests/__init__.py
index 924cfc8c823..fd93f8e2b61 100644
--- a/addons/purchase/tests/__init__.py
+++ b/addons/purchase/tests/__init__.py
@@ -5,3 +5,4 @@ from . import test_onchange_product_id
from . import test_purchase_order
from . import test_create_picking
from . import test_purchase_lead_time
+from . import test_stockvaluation
diff --git a/addons/purchase/tests/test_stockvaluation.py b/addons/purchase/tests/test_stockvaluation.py
new file mode 100644
index 00000000000..53d30855cfe
--- /dev/null
+++ b/addons/purchase/tests/test_stockvaluation.py
@@ -0,0 +1,198 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+import time
+from datetime import datetime
+
+from odoo.tests.common import TransactionCase
+from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
+
+
+class TestStockValuation(TransactionCase):
+ def setUp(self):
+ super(TestStockValuation, self).setUp()
+ self.supplier_location = self.env.ref('stock.stock_location_suppliers')
+ self.stock_location = self.env.ref('stock.stock_location_stock')
+ self.partner_id = self.env.ref('base.res_partner_1')
+ self.product1 = self.env.ref('product.product_product_8')
+ Account = self.env['account.account']
+ self.stock_input_account = Account.create({
+ 'name': 'Stock Input',
+ 'code': 'StockIn',
+ 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
+ })
+ self.stock_output_account = Account.create({
+ 'name': 'Stock Output',
+ 'code': 'StockOut',
+ 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
+ })
+ self.stock_valuation_account = Account.create({
+ 'name': 'Stock Valuation',
+ 'code': 'Stock Valuation',
+ 'user_type_id': self.env.ref('account.data_account_type_current_assets').id,
+ })
+ self.stock_journal = self.env['account.journal'].create({
+ 'name': 'Stock Journal',
+ 'code': 'STJTEST',
+ 'type': 'general',
+ })
+ self.product1.categ_id.write({
+ 'property_stock_account_input_categ_id': self.stock_input_account.id,
+ 'property_stock_account_output_categ_id': self.stock_output_account.id,
+ 'property_stock_valuation_account_id': self.stock_valuation_account.id,
+ 'property_stock_journal': self.stock_journal.id,
+ })
+
+ def test_change_unit_cost_average_1(self):
+ """ Confirm a purchase order and create the associated receipt, change the unit cost of the
+ purchase order before validating the receipt, the value of the received goods should be set
+ according to the last unit cost.
+ """
+ self.product1.product_tmpl_id.cost_method = 'average'
+ po1 = self.env['purchase.order'].create({
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product1.uom_po_id.id,
+ 'price_unit': 100.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ }),
+ ],
+ })
+ po1.button_confirm()
+
+ picking1 = po1.picking_ids[0]
+ move1 = picking1.move_lines[0]
+
+ # the unit price of the purchase order line is copied to the in move
+ self.assertEquals(move1.price_unit, 100)
+
+ # update the unit price on the purchase order line
+ po1.order_line.price_unit = 200
+
+ # the unit price on the stock move is not directly updated
+ self.assertEquals(move1.price_unit, 100)
+
+ # validate the receipt
+ res_dict = picking1.button_validate()
+ wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
+ wizard.process()
+
+ # the unit price of the stock move has been updated to the latest value
+ self.assertEquals(move1.price_unit, 200)
+
+ self.assertEquals(self.product1.stock_value, 2000)
+
+ def test_standard_price_change_1(self):
+ """ Confirm a purchase order and create the associated receipt, change the unit cost of the
+ purchase order and the standard price of the product before validating the receipt, the
+ value of the received goods should be set according to the last standard price.
+ """
+ self.product1.product_tmpl_id.cost_method = 'standard'
+
+ # set a standard price
+ self.product1.product_tmpl_id.standard_price = 10
+
+ po1 = self.env['purchase.order'].create({
+ 'partner_id': self.partner_id.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product1.uom_po_id.id,
+ 'price_unit': 11.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ }),
+ ],
+ })
+ po1.button_confirm()
+
+ picking1 = po1.picking_ids[0]
+ move1 = picking1.move_lines[0]
+
+ # the move's unit price reflects the purchase order line's cost even if it's useless when
+ # the product's cost method is standard
+ self.assertEquals(move1.price_unit, 11)
+
+ # set a new standard price
+ self.product1.product_tmpl_id.standard_price = 12
+
+ # the unit price on the stock move is not directly updated
+ self.assertEquals(move1.price_unit, 11)
+
+ # validate the receipt
+ res_dict = picking1.button_validate()
+ wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
+ wizard.process()
+
+ # the unit price of the stock move has been updated to the latest value
+ self.assertEquals(move1.price_unit, 12)
+
+ self.assertEquals(self.product1.stock_value, 120)
+
+ def test_change_currency_rate_average_1(self):
+ """ Confirm a purchase order in another currency and create the associated receipt, change
+ the currency rate, validate the receipt and then check that the value of the received goods
+ is set according to the last currency rate.
+ """
+ usd_currency = self.env.ref('base.USD')
+ eur_currency = self.env.ref('base.EUR')
+
+ self.product1.product_tmpl_id.cost_method = 'average'
+
+ # default currency is USD, create a purchase order in EUR
+ po1 = self.env['purchase.order'].create({
+ 'partner_id': self.partner_id.id,
+ 'currency_id': eur_currency.id,
+ 'order_line': [
+ (0, 0, {
+ 'name': self.product1.name,
+ 'product_id': self.product1.id,
+ 'product_qty': 10.0,
+ 'product_uom': self.product1.uom_po_id.id,
+ 'price_unit': 100.0,
+ 'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
+ }),
+ ],
+ })
+ po1.button_confirm()
+
+ picking1 = po1.picking_ids[0]
+ move1 = picking1.move_lines[0]
+
+ # convert the price unit in the company currency
+ price_unit_usd = po1.currency_id.compute(po1.order_line.price_unit, po1.company_id.currency_id, round=False)
+
+ # the unit price of the move is the unit price of the purchase order line converted in
+ # the company's currency
+ self.assertEqual(move1.price_unit, price_unit_usd)
+
+ # change the rate of the currency
+ self.env['res.currency.rate'].create({
+ 'name': time.strftime('%Y-%m-%d'),
+ 'rate': 2.0,
+ 'currency_id': eur_currency.id,
+ 'company_id': po1.company_id.id,
+ })
+ eur_currency._compute_current_rate()
+ price_unit_usd_new_rate = po1.currency_id.compute(po1.order_line.price_unit, po1.company_id.currency_id, round=False)
+
+ # the new price_unit is lower than th initial because of the rate's change
+ self.assertLess(price_unit_usd_new_rate, price_unit_usd)
+
+ # the unit price on the stock move is not directly updated
+ self.assertEquals(move1.price_unit, price_unit_usd)
+
+ # validate the receipt
+ res_dict = picking1.button_validate()
+ wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
+ wizard.process()
+
+ # the unit price of the stock move has been updated to the latest value
+ self.assertEquals(move1.price_unit, price_unit_usd_new_rate)
+
+ self.assertAlmostEqual(self.product1.stock_value, price_unit_usd_new_rate * 10, delta=0.1)
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index 5d5631c4069..5ab89e8fca1 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -244,7 +244,7 @@ class StockMove(models.Model):
self.ensure_one()
if self._is_in():
if self.product_id.cost_method in ['fifo', 'average']:
- price_unit = self.price_unit or self._get_price_unit()
+ price_unit = self._get_price_unit()
value = price_unit * (quantity or self.product_qty)
vals = {
'price_unit': price_unit,
From 57f03f40b973505b22b1c17bc78486a77a2538f0 Mon Sep 17 00:00:00 2001
From: Simon Lejeune
Date: Fri, 13 Oct 2017 18:28:34 +0200
Subject: [PATCH 013/153] [FIX] purchase: fix rev
7392e476b8f8721260d0eb10413c7a52a25ca469
Force the company currency before playing with the rates.
---
addons/purchase/tests/test_stockvaluation.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/addons/purchase/tests/test_stockvaluation.py b/addons/purchase/tests/test_stockvaluation.py
index 53d30855cfe..09359417738 100644
--- a/addons/purchase/tests/test_stockvaluation.py
+++ b/addons/purchase/tests/test_stockvaluation.py
@@ -140,6 +140,8 @@ class TestStockValuation(TransactionCase):
is set according to the last currency rate.
"""
usd_currency = self.env.ref('base.USD')
+ self.env.user.company_id.currency_id = usd_currency.id
+
eur_currency = self.env.ref('base.EUR')
self.product1.product_tmpl_id.cost_method = 'average'
From 2e6f25580e1d50ec7176b29bb6ca5ae249cd248a Mon Sep 17 00:00:00 2001
From: "Yenthe V.G"
Date: Fri, 13 Oct 2017 14:20:03 +0200
Subject: [PATCH 014/153] [FIX] sms: add missing import for translations
This commit closes #20141
---
addons/sms/wizard/send_sms.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/sms/wizard/send_sms.py b/addons/sms/wizard/send_sms.py
index 25fa0828934..7aa76205a9f 100644
--- a/addons/sms/wizard/send_sms.py
+++ b/addons/sms/wizard/send_sms.py
@@ -3,7 +3,7 @@
import logging
-from odoo import api, fields, models
+from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.addons.iap.models import iap
From 4f0d6f5ae162c0263acb7fac64350577595b22a9 Mon Sep 17 00:00:00 2001
From: "Yenthe V.G"
Date: Mon, 16 Oct 2017 09:06:38 +0200
Subject: [PATCH 015/153] [Requirements] FIX: Upgrade ebaysdk to 2.1.5 due to
bug
Upgrade the dependency for ebaysdk from 2.1.4 to 2.1.5.
There was a bug in in 2.1.4 that prevented installing (see https://github.com/timotheus/ebaysdk-python/issues/223).
Closes #20148
---
requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements.txt b/requirements.txt
index ee6c39b586d..f003d714858 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,7 @@
Babel==2.3.4
decorator==4.0.10
docutils==0.12
-ebaysdk==2.1.4
+ebaysdk==2.1.5
feedparser==5.2.1
gevent==1.1.2
greenlet==0.4.10
From cd21942ab8473718a5f986f12a5d76d7f71b2909 Mon Sep 17 00:00:00 2001
From: Goffin Simon
Date: Thu, 12 Oct 2017 16:13:42 +0200
Subject: [PATCH 016/153] =?UTF-8?q?[FIX]=C2=A0website=5Fforum:=20404=20err?=
=?UTF-8?q?or=20message=20for=20empty=20question?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
When a empty question was posted in the forum, a 404 error was raised.
opw:76940
---
addons/website_forum/controllers/main.py | 2 +-
addons/website_forum/i18n/website_forum.pot | 6 ++++++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py
index fb21827b0fe..8ec59225e6b 100644
--- a/addons/website_forum/controllers/main.py
+++ b/addons/website_forum/controllers/main.py
@@ -322,7 +322,7 @@ class WebsiteForum(http.Controller):
if post_type == 'question' and not post.get('post_name', ''):
return request.render('website.http_error', {'status_code': _('Bad Request'), 'status_message': _('Title should not be empty.')})
if post.get('content', '') == '
':
- return werkzeug.utils.redirect("/forum/%s/question/%s?nocontent=1" % (slug(forum), post_parent and slug(post_parent)))
+ return request.render('website.http_error', {'status_code': _('Bad Request'), 'status_message': _('Question should not be empty.')})
post_tag_ids = forum._tag_to_write_vals(post.get('post_tags', ''))
diff --git a/addons/website_forum/i18n/website_forum.pot b/addons/website_forum/i18n/website_forum.pot
index 75bdf73ced2..d160e5971bb 100644
--- a/addons/website_forum/i18n/website_forum.pot
+++ b/addons/website_forum/i18n/website_forum.pot
@@ -2471,6 +2471,12 @@ msgstr ""
msgid "Title must not be empty"
msgstr ""
+#. module: website_forum
+#: code:addons/website_forum/controllers/main.py:325
+#, python-format
+msgid "Question should not be empty."
+msgstr ""
+
#. module: website_forum
#: code:addons/website_forum/controllers/main.py:323
#: code:addons/website_forum/controllers/main.py:394
From c9e36d3d15ad5265ffc64e1de700d5a9175d7b05 Mon Sep 17 00:00:00 2001
From: Goffin Simon
Date: Thu, 12 Oct 2017 14:00:18 +0200
Subject: [PATCH 017/153] [FIX] point_of_sale: Variant description doesn't show
on POS invoice
The display_name of the product must be shown as the name of the invoice line
to have the same behavior as in Sale app.
opw:771302
---
addons/point_of_sale/models/pos_order.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index 8200bb3269f..82e66fe7073 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -193,7 +193,7 @@ class PosOrder(models.Model):
# We convert a new id object back to a dictionary to write to
# bridge between old and new api
inv_line = invoice_line._convert_to_write({name: invoice_line[name] for name in invoice_line._cache})
- inv_line.update(price_unit=line.price_unit, discount=line.discount)
+ inv_line.update(price_unit=line.price_unit, discount=line.discount, name=inv_name)
return InvoiceLine.sudo().create(inv_line)
def _create_account_move_line(self, session=None, move=None):
From cd77219cbd14929c8035de0a795ffd75c625cb4c Mon Sep 17 00:00:00 2001
From: Goffin Simon
Date: Thu, 12 Oct 2017 11:22:03 +0200
Subject: [PATCH 018/153] [FIX] account: Duplicating journal entries
When duplictating a Journal Entry that has been created from
a new payment (accounting/purchases/payments/create), the new copy
of the Journal Entry also duplicated the already existent relationship (link)
to the payment, causing possible future conflicts in functionalities
like reports, payments, reconciliations, among others.
opw:775151
---
addons/account/models/account_move.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 4458152f940..2c04fcd283f 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -385,7 +385,7 @@ class AccountMoveLine(models.Model):
help="The move of this entry line.", index=True, required=True, auto_join=True)
narration = fields.Text(related='move_id.narration', string='Narration')
ref = fields.Char(related='move_id.ref', string='Reference', store=True, copy=False, index=True)
- payment_id = fields.Many2one('account.payment', string="Originator Payment", help="Payment that created this entry")
+ payment_id = fields.Many2one('account.payment', string="Originator Payment", help="Payment that created this entry", copy=False)
statement_id = fields.Many2one('account.bank.statement', string='Statement',
help="The bank statement used for bank reconciliation", index=True, copy=False)
reconciled = fields.Boolean(compute='_amount_residual', store=True)
From ef671e79520f49335a54ac9cea24da23923500ee Mon Sep 17 00:00:00 2001
From: Goffin Simon
Date: Mon, 16 Oct 2017 11:25:39 +0200
Subject: [PATCH 019/153] [FIX] base: Wrong default value for
number_next_actual in ir.sequence.date_range
The default value for number_next_actual in ir.sequence.date_range must be at least one
otherwise it will raise error because the next value cannot be equal to the date_from.
opw:776205
---
odoo/addons/base/ir/ir_sequence.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/odoo/addons/base/ir/ir_sequence.py b/odoo/addons/base/ir/ir_sequence.py
index 95aa941c86e..fb3765b9cab 100644
--- a/odoo/addons/base/ir/ir_sequence.py
+++ b/odoo/addons/base/ir/ir_sequence.py
@@ -306,6 +306,12 @@ class IrSequenceDateRange(models.Model):
for seq in self:
seq.write({'number_next': seq.number_next_actual or 0})
+ @api.model
+ def default_get(self, fields):
+ result = super(IrSequenceDateRange, self).default_get(fields)
+ result['number_next_actual'] = 1
+ return result
+
date_from = fields.Date(string='From', required=True)
date_to = fields.Date(string='To', required=True)
sequence_id = fields.Many2one("ir.sequence", string='Main Sequence', required=True, ondelete='cascade')
From bb692c76d41bb308ef24e729f907d707c44b99ca Mon Sep 17 00:00:00 2001
From: Khoi Nguyen
Date: Thu, 12 Oct 2017 17:42:18 +0200
Subject: [PATCH 020/153] [FIX] web: tab navigation with phone widgets in form
view
This commit ensures that tab navigation works properly when editing a
form view that contains input fields with phone widgets. Previously,
pressing TAB would skip those fields.
---
.../web/static/src/js/fields/basic_fields.js | 2 +-
.../static/tests/fields/basic_fields_tests.js | 27 +++++++++++++++++++
2 files changed, 28 insertions(+), 1 deletion(-)
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 6392870dc90..7d7c6735531 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -966,7 +966,7 @@ var FieldPhone = FieldEmail.extend({
* @override
*/
getFocusableElement: function () {
- if (this._canCall()) {
+ if (this.mode !== 'readonly' || this._canCall()) {
return this._super.apply(this, arguments);
}
return $();
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index f01f2d8b589..c18c8514774 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -2828,6 +2828,33 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('use TAB to navigate to a phone field', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'',
+ });
+
+ form.$('input[name=display_name]').click();
+ assert.strictEqual(form.$('input[name="display_name"]')[0], document.activeElement,
+ "display_name should be focused");
+ form.$('input[name="display_name"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
+ assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement,
+ "foo should be focused");
+
+ form.destroy();
+ });
+
QUnit.module('PriorityWidget');
QUnit.test('priority widget when not set', function (assert) {
From 9f7af9f9b742d08ca69a4d4d0b963f625c4b4bba Mon Sep 17 00:00:00 2001
From: Khoi Nguyen
Date: Wed, 11 Oct 2017 10:00:29 +0200
Subject: [PATCH 021/153] [FIX] web: ticking a checkbox in an x2many after
onchange
This commits fixes a traceback that appears when ticking a checkbox in
an x2many which triggers an onchange. The root cause of the issue is
twofold:
- confirmUpdate requires currentFieldIndex to be defined, but might be
needed when setting currentFieldIndex;
- confirmUpdate attempts to restore the selection range even if we are
not dealing with a char field (e.g. checkbox).
We solve the issue by ensuring that _selectCell gives a temporary value
to currentFieldIndex. This value is chosen so that confirmUpdate calls
_selectCell with the right parameters. Moreover, we only restore the
selection range when the range is defined.
---
.../static/src/js/views/form/form_renderer.js | 1 +
.../js/views/list/list_editable_renderer.js | 14 +++++++--
.../tests/fields/relational_fields_tests.js | 29 +++++++++++++++++++
3 files changed, 42 insertions(+), 2 deletions(-)
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index ab5e4beb6b5..0652ee6bc4c 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -855,6 +855,7 @@ var FormRenderer = BasicRenderer.extend({
if (ev.data.direction === "next") {
index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target);
this._activateNextFieldWidget(this.state, index);
+ debugger;
} else if (ev.data.direction === "previous") {
index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target);
this._activatePreviousFieldWidget(this.state, index);
diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js
index 9027fc3c984..b0ddbdcd385 100644
--- a/addons/web/static/src/js/views/list/list_editable_renderer.js
+++ b/addons/web/static/src/js/views/list/list_editable_renderer.js
@@ -136,7 +136,9 @@ ListRenderer.include({
currentRowID = this.state.data[this.currentRow].id;
currentWidget = this.allFieldWidgets[currentRowID][this.currentFieldIndex];
focusedElement = currentWidget.getFocusableElement().get(0);
- selectionRange = dom.getSelectionRange(focusedElement);
+ if (currentWidget.formatType !== 'boolean') {
+ selectionRange = dom.getSelectionRange(focusedElement);
+ }
}
var oldData = this.state.data;
@@ -179,7 +181,9 @@ ListRenderer.include({
currentRowID = self.state.data[newRowIndex].id;
currentWidget = self.allFieldWidgets[currentRowID][self.currentFieldIndex];
focusedElement = currentWidget.getFocusableElement().get(0);
- dom.setSelectionRange(focusedElement, selectionRange);
+ if (selectionRange) {
+ dom.setSelectionRange(focusedElement, selectionRange);
+ }
});
}
});
@@ -585,12 +589,18 @@ ListRenderer.include({
if (fieldIndex >= (self.allFieldWidgets[record.id] || []).length) {
return $.Deferred().reject();
}
+ // _activateFieldWidget might trigger an onchange,
+ // which requires currentFieldIndex to be set
+ // so that the cursor can be restored
+ var oldFieldIndex = self.currentFieldIndex;
+ self.currentFieldIndex = fieldIndex;
fieldIndex = self._activateFieldWidget(record, fieldIndex, {
inc: 1,
wrap: wrap,
event: options && options.event,
});
if (fieldIndex < 0) {
+ self.currentFieldIndex = oldFieldIndex;
return $.Deferred().reject();
}
self.currentFieldIndex = fieldIndex;
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 641546b3fc1..824b68c520d 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -6248,6 +6248,35 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('checkbox in an x2many that triggers an onchange', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.onchanges = {
+ bar: function () {}
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ });
+
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ form.$('.o_field_widget[name=bar] input').click();
+ assert.notOk(form.$('.o_field_widget[name=bar] input').prop('checked'),
+ "the checkbox should be unticked");
+
+ form.destroy();
+ });
+
QUnit.test('one2many with default value: edit line to make it invalid', function (assert) {
assert.expect(3);
From 5afb5ecd04cf49be6bedb655b187b19a65e1e02d Mon Sep 17 00:00:00 2001
From: Laurent Smet
Date: Mon, 16 Oct 2017 11:35:08 +0200
Subject: [PATCH 022/153] [FIX] account: fix complex tax computations (multiple
price included taxes, etc.)
Suppose the correct computation:
seq | amount | incl | incl_base | base | amount
----------------------------------------------------
1 | 10% | t | t | 100.0 | 10.0
2 | 10% | | | 110.0 | 11.0
3 | 10% | t | | 110.0 | 11.0
4 | 10% | | | 110.0 | 11.0
5 | 10% | t | | 110.0 | 11.0
----------------------------------------------------
with an initial amount of 132. Without this commit, the base amount was computed by:
132 / 1.1 / 1.1 = 109.09
instead of:
132 / 1.2 = 110.0
see task: https://www.odoo.com/web#id=34471&view_type=form&model=project.task&action=333&active_id=967&menu_id=4720
Was PR #18275
---
addons/account/models/account.py | 165 ++++++++++----
addons/account/tests/test_tax.py | 213 ++++++++++++++++---
addons/point_of_sale/static/src/js/models.js | 156 ++++++++++----
3 files changed, 421 insertions(+), 113 deletions(-)
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index c64473778ec..4630ece0eeb 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -619,6 +619,7 @@ class AccountTax(models.Model):
price_unit * quantity eventually affected by previous taxes (if tax is include_base_amount XOR price_include)
"""
self.ensure_one()
+ price_include = self._context.get('force_price_include', self.price_include)
if self.amount_type == 'fixed':
# Use copysign to take into account the sign of the base amount which includes the sign
# of the quantity and the sign of the price_unit
@@ -632,11 +633,11 @@ class AccountTax(models.Model):
return math.copysign(quantity, base_amount) * self.amount
else:
return quantity * self.amount
- if (self.amount_type == 'percent' and not self.price_include) or (self.amount_type == 'division' and self.price_include):
+ if (self.amount_type == 'percent' and not price_include) or (self.amount_type == 'division' and self.price_include):
return base_amount * self.amount / 100
- if self.amount_type == 'percent' and self.price_include:
+ if self.amount_type == 'percent' and price_include:
return base_amount - (base_amount / (1 + self.amount / 100))
- if self.amount_type == 'division' and not self.price_include:
+ if self.amount_type == 'division' and not price_include:
return base_amount / (1 - self.amount / 100) - base_amount
@api.v8
@@ -659,13 +660,58 @@ class AccountTax(models.Model):
'analytic': boolean,
}]
} """
+
+ # 1) Flatten the taxes.
+
+ def collect_taxes(self, all_taxes=None):
+ # Collect all the taxes recursively ordered by the sequence.
+ # Example:
+ # group | seq | sub-group |
+ # ------------|-----------|
+ # | 1 | |
+ # ------------|-----------|
+ # t | 2 | | seq | |
+ # | | | 4 | |
+ # | | | 5 | |
+ # | | | 6 | |
+ # | | |
+ # ------------|-----------|
+ # | 3 | |
+ # ------------|-----------|
+ # Result: 1-4-5-6-3
+ if not all_taxes:
+ all_taxes = self.env['account.tax']
+ for tax in self.sorted(key=lambda r: r.sequence):
+ if tax.amount_type == 'group':
+ all_taxes = collect_taxes(tax.children_tax_ids, all_taxes)
+ else:
+ all_taxes += tax
+ return all_taxes
+
+ taxes = collect_taxes(self)
+
+ # 2) Avoid dealing with taxes mixing price_include=False && include_base_amount=True
+ # with price_include=True
+
+ base_excluded_flag = False # price_include=False && include_base_amount=True
+ included_flag = False # price_include=True
+ for tax in taxes:
+ if tax.price_include:
+ included_flag = True
+ elif tax.include_base_amount:
+ base_excluded_flag = True
+ if base_excluded_flag and included_flag:
+ raise UserError(_('Unable to mix any taxes being price included with taxes affecting the base amount but not included in price.'))
+
+ # 3) Deal with the rounding methods
+
if len(self) == 0:
company_id = self.env.user.company_id
else:
company_id = self[0].company_id
if not currency:
currency = company_id.currency_id
- taxes = []
+
# By default, for each tax, tax amount will first be computed
# and rounded at the 'Account' decimal precision for each
# PO/SO/invoice line and then these rounded amounts will be
@@ -690,50 +736,93 @@ class AccountTax(models.Model):
if not round_tax:
prec += 5
- base_values = self.env.context.get('base_values')
- if not base_values:
- total_excluded = total_included = base = round(price_unit * quantity, prec)
+ # 4) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation.
+ # tax | base | amount |
+ # /\ ----------------------------
+ # || tax_1 | XXXX | | <- we are looking for that, it's the total_excluded
+ # || tax_2 | | |
+ # || tax_3 | | |
+ # || ... | .. | .. |
+ # ----------------------------
+
+ def recompute_base(base_amount, fixed_amount, percent_amount):
+ # Recompute the new base amount based on included fixed/percent amount and the current base amount.
+ # Example:
+ # tax | amount |
+ # ------------------
+ # tax_1 | 10% |
+ # tax_2 | 15 |
+ # tax_3 | 20% |
+ # ------------------
+ # if base_amount = 145, the new base is computed as:
+ # (145 - 15) / (1.0 + ((10 + 20) / 100.0)) = 130 / 1.3 = 100
+ if fixed_amount == 0.0 and percent_amount == 0.0:
+ return base_amount
+ return round((base_amount - fixed_amount) / (1.0 + percent_amount / 100.0), prec)
+
+ base = round(price_unit * quantity, prec)
+
+ # For the computation of move lines, we could have a negative base value.
+ # In this case, compute all with positive values and negative them at the end.
+ if base < 0:
+ base = -base
+ sign = -1
else:
- total_excluded, total_included, base = base_values
+ sign = 1
- # Sorting key is mandatory in this case. When no key is provided, sorted() will perform a
- # search. However, the search method is overridden in account.tax in order to add a domain
- # depending on the context. This domain might filter out some taxes from self, e.g. in the
- # case of group taxes.
- for tax in self.sorted(key=lambda r: r.sequence):
- if tax.amount_type == 'group':
- children = tax.children_tax_ids.with_context(base_values=(total_excluded, total_included, base))
- ret = children.compute_all(price_unit, currency, quantity, product, partner)
- total_excluded = ret['total_excluded']
- base = ret['base'] if tax.include_base_amount else base
- total_included = ret['total_included']
- tax_amount = total_included - total_excluded
- taxes += ret['taxes']
- continue
+ # Keep track of the accumulated included fixed/percent amount.
+ incl_fixed_amount = incl_percent_amount = 0
+ for tax in reversed(taxes):
+ if tax.include_base_amount:
+ base = recompute_base(base, incl_fixed_amount, incl_percent_amount)
+ incl_fixed_amount = incl_percent_amount = 0
+ if tax.price_include:
+ if tax.amount_type == 'fixed':
+ incl_fixed_amount += tax.amount
+ elif tax.amount_type == 'percent':
+ incl_percent_amount += tax.amount
+ # Start the computation of accumulated amounts at the total_excluded value.
+ total_excluded = total_included = base = recompute_base(base, incl_fixed_amount, incl_percent_amount)
- tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner)
+ # 5) Iterate the taxes in the sequence order to fill missing base/amount values.
+ # tax | base | amount |
+ # || ----------------------------
+ # || tax_1 | OK | XXXX |
+ # || tax_2 | XXXX | XXXX |
+ # || tax_3 | XXXX | XXXX |
+ # \/ ... | .. | .. |
+ # ----------------------------
+ taxes_vals = []
+ for tax in taxes:
+ # Compute the amount of the tax but don't deal with the price_include because it's already
+ # took into account on the base amount except for 'division' tax:
+ # (tax.amount_type == 'percent' && not tax.price_include)
+ # == (tax.amount_type == 'division' && tax.price_include)
+ tax_amount = tax.with_context(force_price_include=False)._compute_amount(base, price_unit, quantity, product, partner)
if not round_tax:
tax_amount = round(tax_amount, prec)
else:
tax_amount = currency.round(tax_amount)
- if tax.price_include:
- total_excluded -= tax_amount
- base -= tax_amount
- else:
- total_included += tax_amount
-
- # Keep base amount used for the current tax
+ # Suppose:
+ # seq | amount | incl | incl_base | base | amount
+ # -----------------------------------------------
+ # 1 | 10 % | t | t | 100.0 | 10.0
+ # -----------------------------------------------
+ # ... the next computation must be done using 100.0 + 10.0 = 110.0 as base but
+ # the tax base of this tax will be 100.0.
tax_base = base
-
if tax.include_base_amount:
base += tax_amount
- taxes.append({
+ # The total_included amount is computed as the sum of total_excluded with all tax_amount
+ total_included += tax_amount
+
+ taxes_vals.append({
'id': tax.id,
'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name,
- 'amount': tax_amount,
- 'base': tax_base,
+ 'amount': sign * tax_amount,
+ 'base': sign * tax_base,
'sequence': tax.sequence,
'account_id': tax.account_id.id,
'refund_account_id': tax.refund_account_id.id,
@@ -741,10 +830,10 @@ class AccountTax(models.Model):
})
return {
- 'taxes': sorted(taxes, key=lambda k: k['sequence']),
- 'total_excluded': currency.round(total_excluded) if round_total else total_excluded,
- 'total_included': currency.round(total_included) if round_total else total_included,
- 'base': base,
+ 'taxes': taxes_vals,
+ 'total_excluded': sign * (currency.round(total_excluded) if round_total else total_excluded),
+ 'total_included': sign * (currency.round(total_included) if round_total else total_included),
+ 'base': sign * base,
}
@api.v7
diff --git a/addons/account/tests/test_tax.py b/addons/account/tests/test_tax.py
index 02308fa6498..5b6361bf69c 100644
--- a/addons/account/tests/test_tax.py
+++ b/addons/account/tests/test_tax.py
@@ -1,6 +1,7 @@
+# -*- coding: utf-8 -*-
+
from openerp.addons.account.tests.account_test_users import AccountTestUsers
-from openerp.tests.common import TransactionCase
-from openerp.tools import float_compare
+
import time
@@ -67,47 +68,110 @@ class TestTax(AccountTestUsers):
self.bank_account = self.bank_journal.default_debit_account_id
self.expense_account = self.env['account.account'].search([('user_type_id.type', '=', 'payable')], limit=1) #Should be done by onchange later
+ def _check_compute_all_results(self, base, total_included, total_excluded, taxes, res):
+ self.assertAlmostEqual(res['base'], base)
+ self.assertAlmostEqual(res['total_included'], total_included)
+ self.assertAlmostEqual(res['total_excluded'], total_excluded)
+ for i in range(0, len(taxes)):
+ self.assertAlmostEqual(res['taxes'][i]['base'], taxes[i][0])
+ self.assertAlmostEqual(res['taxes'][i]['amount'], taxes[i][1])
+
def test_tax_group_of_group_tax(self):
self.fixed_tax.include_base_amount = True
- self.group_tax.include_base_amount = True
- self.group_of_group_tax.include_base_amount = True
res = self.group_of_group_tax.compute_all(200.0)
- self.assertEquals(res['total_excluded'], 200.0)
- # After calculation of first group
- # base = 210
- # total_included = 231
- # Base of the first grouped is passed
- # Base after the second group (220) is dropped.
- # Base of the group of groups is passed out,
- # so we obtain base as after first group
- self.assertEquals(res['base'], 210.0)
- self.assertEquals(res['total_included'], 263.0)
+ self._check_compute_all_results(
+ 220, # 'base'
+ 263, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 10.0), # | 1 | 10 | | t
+ (210.0, 21.0), # | 3 | 10% | |
+ (210.0, 10.0), # | 1 | 10 | | t
+ (220.0, 22.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
def test_tax_group(self):
res = self.group_tax.compute_all(200.0)
- self.assertEquals(res['total_excluded'], 200.0)
- self.assertEquals(res['total_included'], 230.0)
- self.assertEquals(len(res['taxes']), 2)
- self.assertEquals(res['taxes'][0]['amount'], 10.0)
- self.assertEquals(res['taxes'][1]['amount'], 20.0)
+ self._check_compute_all_results(
+ 200, # 'base'
+ 230, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 10.0), # | 1 | 10 | |
+ (200.0, 20.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
def test_tax_percent_division(self):
self.division_tax.price_include = True
self.division_tax.include_base_amount = True
+ res_division = self.division_tax.compute_all(200.0)
+ self._check_compute_all_results(
+ 220, # 'base'
+ 220, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 20.0), # | 4 | 10/ | t | t
+ # ---------------------------------------------------
+ ],
+ res_division
+ )
self.percent_tax.price_include = False
self.percent_tax.include_base_amount = False
- res_division = self.division_tax.compute_all(200.0)
res_percent = self.percent_tax.compute_all(200.0)
- self.assertEquals(res_division['taxes'][0]['amount'], 20.0)
- self.assertEquals(res_percent['taxes'][0]['amount'], 20.0)
+ self._check_compute_all_results(
+ 200, # 'base'
+ 220, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 20.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res_percent
+ )
self.division_tax.price_include = False
self.division_tax.include_base_amount = False
+ res_division = self.division_tax.compute_all(200.0)
+ self._check_compute_all_results(
+ 200, # 'base'
+ 222.22, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 22.22), # | 4 | 10/ | |
+ # ---------------------------------------------------
+ ],
+ res_division
+ )
self.percent_tax.price_include = True
self.percent_tax.include_base_amount = True
- res_division = self.division_tax.compute_all(200.0)
res_percent = self.percent_tax.compute_all(200.0)
- self.assertEquals(res_division['taxes'][0]['amount'], 22.22)
- self.assertEquals(res_percent['taxes'][0]['amount'], 18.18)
+ self._check_compute_all_results(
+ 200, # 'base'
+ 200, # 'total_included'
+ 181.82, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (181.82, 18.18), # | 3 | 10% | t | t
+ # ---------------------------------------------------
+ ],
+ res_percent
+ )
def test_tax_sequence_normalized_set(self):
self.division_tax.sequence = 1
@@ -115,19 +179,53 @@ class TestTax(AccountTestUsers):
self.percent_tax.sequence = 3
taxes_set = (self.group_tax | self.division_tax)
res = taxes_set.compute_all(200.0)
- self.assertEquals(res['taxes'][0]['amount'], 22.22)
- self.assertEquals(res['taxes'][1]['amount'], 10.0)
- self.assertEquals(res['taxes'][2]['amount'], 20.0)
+ self._check_compute_all_results(
+ 200, # 'base'
+ 252.22, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 22.22), # | 1 | 10/ | |
+ (200.0, 10.0), # | 2 | 10 | |
+ (200.0, 20.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
def test_tax_include_base_amount(self):
self.fixed_tax.include_base_amount = True
res = self.group_tax.compute_all(200.0)
- self.assertEquals(res['total_included'], 231.0)
+ self._check_compute_all_results(
+ 210, # 'base'
+ 231, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 10.0), # | 1 | 10 | | t
+ (210.0, 21.0), # | 3 | 10% | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
def test_tax_currency(self):
self.division_tax.amount = 15.0
res = self.division_tax.compute_all(200.0, currency=self.env.ref('base.VEF'))
- self.assertAlmostEqual(res['total_included'], 235.2941)
+ self._check_compute_all_results(
+ 200, # 'base'
+ 235.2941, # 'total_included'
+ 200, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (200.0, 35.2941), # | 4 | 15/ | |
+ # ---------------------------------------------------
+ ],
+ res
+ )
def test_tax_move_lines_creation(self):
""" Test that creating a move.line with tax_ids generates the tax move lines and adjust line amount when a tax is price_include """
@@ -172,3 +270,58 @@ class TestTax(AccountTestUsers):
aml_with_taxes = move.line_ids.filtered(lambda l: set(l.tax_ids.ids) == set([self.group_tax.id, self.fixed_tax_bis.id]))
self.assertEquals(len(aml_with_taxes), 1)
self.assertEquals(aml_with_taxes.credit, 190)
+
+ def test_advanced_taxes_computation_0(self):
+ '''Test more advanced taxes computation (see issue 34471).'''
+ tax_1 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_1',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'price_include': True,
+ 'include_base_amount': True,
+ 'sequence': 1,
+ })
+ tax_2 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_2',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'sequence': 2,
+ })
+ tax_3 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_3',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'price_include': True,
+ 'sequence': 3,
+ })
+ tax_4 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_4',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'sequence': 4,
+ })
+ tax_5 = self.env['account.tax'].create({
+ 'name': 'test_advanced_taxes_computation_0_5',
+ 'amount_type': 'percent',
+ 'amount': 10,
+ 'price_include': True,
+ 'sequence': 5,
+ })
+ taxes = tax_1 + tax_2 + tax_3 + tax_4 + tax_5
+ res = taxes.compute_all(132.0)
+ self._check_compute_all_results(
+ 110, # 'base'
+ 154, # 'total_included'
+ 100, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (100.0, 10.0), # | 1 | 10% | t | t
+ (110.0, 11.0), # | 3 | 10% | |
+ (110.0, 11.0), # | 3 | 10% | t |
+ (110.0, 11.0), # | 3 | 10% | |
+ (110.0, 11.0), # | 3 | 10% | t |
+ # ---------------------------------------------------
+ ],
+ res
+ )
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 814856f4e5a..a18bd707da6 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -202,7 +202,7 @@ exports.PosModel = Backbone.Model.extend({
},
},{
model: 'account.tax',
- fields: ['name','amount', 'price_include', 'include_base_amount', 'amount_type', 'children_tax_ids'],
+ fields: ['name','amount', 'price_include', 'include_base_amount', 'amount_type', 'children_tax_ids', 'sequence'],
domain: null,
loaded: function(self, taxes){
self.taxes = taxes;
@@ -1332,72 +1332,138 @@ exports.Orderline = Backbone.Model.extend({
return tax;
},
- _compute_all: function(tax, base_amount, quantity) {
+ /**
+ * Mirror JS method of:
+ * _compute_amount in addons/account/models/account.py
+ */
+ _compute_all: function(tax, base_amount, quantity, price_include) {
+ if(price_include === undefined)
+ price_include = tax.price_include;
if (tax.amount_type === 'fixed') {
var sign_base_amount = base_amount >= 0 ? 1 : -1;
return (Math.abs(tax.amount) * sign_base_amount) * quantity;
}
- if ((tax.amount_type === 'percent' && !tax.price_include) || (tax.amount_type === 'division' && tax.price_include)){
+ if ((tax.amount_type === 'percent' && !price_include) || (tax.amount_type === 'division' && tax.price_include)){
return base_amount * tax.amount / 100;
}
- if (tax.amount_type === 'percent' && tax.price_include){
+ if (tax.amount_type === 'percent' && price_include){
return base_amount - (base_amount / (1 + tax.amount / 100));
}
- if (tax.amount_type === 'division' && !tax.price_include) {
+ if (tax.amount_type === 'division' && !price_include) {
return base_amount / (1 - tax.amount / 100) - base_amount;
}
return false;
},
+ /**
+ * Mirror JS method of:
+ * compute_all in addons/account/models/account.py
+ *
+ * Read comments in the python side method for more details about each sub-methods.
+ */
compute_all: function(taxes, price_unit, quantity, currency_rounding, no_map_tax) {
var self = this;
- var list_taxes = [];
+
+ // 1) Flatten the taxes.
+
+ var _collect_taxes = function(taxes, all_taxes){
+ taxes.sort(function (tax1, tax2) {
+ return tax1.sequence - tax2.sequence;
+ });
+ _(taxes).each(function(tax){
+ if(tax.amount_type === 'group')
+ all_taxes = _collect_taxes(tax.children_tax_ids, all_taxes);
+ else
+ all_taxes.push(tax);
+ });
+ return all_taxes;
+ }
+ var collect_taxes = function(taxes){
+ return _collect_taxes(taxes, []);
+ }
+
+ taxes = collect_taxes(taxes);
+
+ // 2) Avoid dealing with taxes mixing price_include=False && include_base_amount=True
+ // with price_include=True
+
+ var base_excluded_flag = false; // price_include=False && include_base_amount=True
+ var included_flag = false; // price_include=True
+ _(taxes).each(function(tax){
+ if(tax.price_include)
+ included_flag = true;
+ else if(tax.include_base_amount)
+ base_excluded_flag = true
+ if(base_excluded_flag && included_flag)
+ throw new Error('Unable to mix any taxes being price included with taxes affecting the base amount but not included in price.');
+ });
+
+ // 3) Deal with the rounding methods
+
var currency_rounding_bak = currency_rounding;
if (this.pos.company.tax_calculation_rounding_method == "round_globally"){
currency_rounding = currency_rounding * 0.00001;
}
- var total_excluded = round_pr(price_unit * quantity, currency_rounding);
- var total_included = total_excluded;
- var base = total_excluded;
- _(taxes).each(function(tax) {
- if (!no_map_tax){
- tax = self._map_tax_fiscal_position(tax);
- }
- if (!tax){
- return;
- }
- if (tax.amount_type === 'group'){
- var ret = self.compute_all(tax.children_tax_ids, price_unit, quantity, currency_rounding);
- total_excluded = ret.total_excluded;
- base = ret.total_excluded;
- total_included = ret.total_included;
- list_taxes = list_taxes.concat(ret.taxes);
- }
- else {
- var tax_amount = self._compute_all(tax, base, quantity);
- tax_amount = round_pr(tax_amount, currency_rounding);
- if (tax_amount){
- if (tax.price_include) {
- total_excluded -= tax_amount;
- base -= tax_amount;
- }
- else {
- total_included += tax_amount;
- }
- if (tax.include_base_amount) {
- base += tax_amount;
- }
- var data = {
- id: tax.id,
- amount: tax_amount,
- name: tax.name,
- };
- list_taxes.push(data);
- }
+ var recompute_base = function(base_amount, fixed_amount, percent_amount){
+ if(fixed_amount === 0.0 && percent_amount === 0.0)
+ return base_amount;
+ return round_pr((base_amount - fixed_amount) / (1.0 + percent_amount / 100.0), currency_rounding);
+ }
+
+ // 4) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation.
+
+ var base = round_pr(price_unit * quantity, currency_rounding);
+ var sign = 1;
+ if(base < 0){
+ base = -base;
+ var sign = -1;
+ }
+
+ var incl_fixed_amount = 0.0;
+ var incl_percent_amount = 0.0;
+ _(taxes.reverse()).each(function(tax){
+ if(tax.include_base_amount){
+ base = recompute_base(base, incl_fixed_amount, incl_percent_amount);
+ incl_fixed_amount = 0.0;
+ incl_percent_amount = 0.0;
+ }
+ if(tax.price_include){
+ if(tax.amount_type === 'fixed')
+ incl_fixed_amount += tax.amount;
+ else if(tax.amount_type === 'percent')
+ incl_percent_amount += tax.amount;
}
});
+
+ base = recompute_base(base, incl_fixed_amount, incl_percent_amount);
+ var total_excluded = base;
+ var total_included = base;
+
+ // 5) Iterate the taxes in the sequence order to fill missing base/amount values.
+
+ var taxes_vals = [];
+ _(taxes.reverse()).each(function(tax){
+ var tax_amount = self._compute_all(tax, base, quantity, false);
+ tax_amount = round_pr(tax_amount, currency_rounding);
+
+ var tax_base = base;
+
+ if(tax.include_base_amount)
+ base += tax_amount;
+
+ total_included += tax_amount;
+
+ var tax_vals = {
+ id: tax.id,
+ amount: sign * tax_amount,
+ name: tax.name,
+ };
+
+ taxes_vals.push(tax_vals);
+ });
+
return {
- taxes: list_taxes,
+ taxes: taxes_vals,
total_excluded: round_pr(total_excluded, currency_rounding_bak),
total_included: round_pr(total_included, currency_rounding_bak)
};
From b6c3ee4e24ffefe6de326bb94861226c176a5bd9 Mon Sep 17 00:00:00 2001
From: Khoi Nguyen
Date: Fri, 29 Sep 2017 14:54:48 +0200
Subject: [PATCH 023/153] [FIX] web: discarding a new record in a modal closes
the modal
Discarding a new record triggers the 'history_back' action. This makes
sense if the record was discarded from a normal form view, but not so
much when in a modal, where the expected behaviour is to close the
dialog.
This commit overrides the function in the action manager which handles
the switch to the previous view. It checks whether a dialog window is
open, in which case it simply closes it.
---
addons/web/static/src/js/chrome/action_manager.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/addons/web/static/src/js/chrome/action_manager.js b/addons/web/static/src/js/chrome/action_manager.js
index 8b88ee0c54f..aa5572f5e7d 100644
--- a/addons/web/static/src/js/chrome/action_manager.js
+++ b/addons/web/static/src/js/chrome/action_manager.js
@@ -420,6 +420,10 @@ var ActionManager = Widget.extend({
return this.inner_widget;
},
history_back: function() {
+ if (this.dialog) {
+ this.dialog.close();
+ return;
+ }
var nbViews = this.inner_action.get_nb_views();
if (nbViews > 1) {
// Stay on this action, but select the previous view
From 8191d6d72e82282c3ea4547d02bfd6ee9fa5022b Mon Sep 17 00:00:00 2001
From: Khoi Nguyen
Date: Thu, 12 Oct 2017 17:42:18 +0200
Subject: [PATCH 024/153] [FIX] web: tab navigation with phone widgets in form
view
This commit ensures that tab navigation works properly when editing a
form view that contains input fields with phone widgets. Previously,
pressing TAB would skip those fields.
---
.../web/static/src/js/fields/basic_fields.js | 2 +-
.../static/tests/fields/basic_fields_tests.js | 27 +++++++++++++++++++
2 files changed, 28 insertions(+), 1 deletion(-)
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index f2c7fcf37cd..1097039ba2c 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -924,7 +924,7 @@ var FieldPhone = FieldEmail.extend({
* @override
*/
getFocusableElement: function () {
- if (this._canCall()) {
+ if (this.mode !== 'readonly' || this._canCall()) {
return this._super.apply(this, arguments);
}
return $();
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 0dd11e549bd..b7d78afda73 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -2771,6 +2771,33 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('use TAB to navigate to a phone field', function (assert) {
+ assert.expect(2);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'',
+ });
+
+ form.$('input[name=display_name]').click();
+ assert.strictEqual(form.$('input[name="display_name"]')[0], document.activeElement,
+ "display_name should be focused");
+ form.$('input[name="display_name"]').trigger($.Event('keydown', {which: $.ui.keyCode.TAB}));
+ assert.strictEqual(form.$('input[name="foo"]')[0], document.activeElement,
+ "foo should be focused");
+
+ form.destroy();
+ });
+
QUnit.module('PriorityWidget');
QUnit.test('priority widget when not set', function (assert) {
From 0958eee295a6fd36f1a421b1ac9841d4563a901f Mon Sep 17 00:00:00 2001
From: Khoi Nguyen
Date: Mon, 16 Oct 2017 13:10:25 +0200
Subject: [PATCH 025/153] [FIX] web: Remove debugger
Introduced by
https://github.com/odoo/odoo/commit/9f7af9f9b742d08ca69a4d4d0b963f625c4b4bba
Sorry!
---
addons/web/static/src/js/views/form/form_renderer.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index 0652ee6bc4c..ab5e4beb6b5 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -855,7 +855,6 @@ var FormRenderer = BasicRenderer.extend({
if (ev.data.direction === "next") {
index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target);
this._activateNextFieldWidget(this.state, index);
- debugger;
} else if (ev.data.direction === "previous") {
index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target);
this._activatePreviousFieldWidget(this.state, index);
From fea667a2433dfc2387daa7051989e840d30e18e0 Mon Sep 17 00:00:00 2001
From: amoyaux
Date: Wed, 11 Oct 2017 14:59:47 +0200
Subject: [PATCH 026/153] [FIX] account_invoice: Wrong default uom
The UOM by default should be the purchase UOM
opw-775105
---
addons/account/models/account_invoice.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 8d73a1f5c85..06fba95630c 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -1310,6 +1310,9 @@ class AccountInvoiceLine(models.Model):
self.price_unit = 0.0
domain['uom_id'] = []
else:
+ # Use the purchase uom by default
+ self.uom_id = self.product_id.uom_po_id
+
if part.lang:
product = self.product_id.with_context(lang=part.lang)
else:
From 0aefcdbad77ffe560f24eda0dd5239f0ec9bc645 Mon Sep 17 00:00:00 2001
From: amoyaux
Date: Mon, 16 Oct 2017 11:05:24 +0200
Subject: [PATCH 027/153] [FIX] stock: procurement generated by scheduler
truncate datetime
When purchase order are created from a scheduler they always
have the same hours even if they were created at different
time. Also the procurement order that generate the purchase
order line have the same problem.
It happens because the transformation from datetime to string
use a date format and thus truncate the DEFAULT_SERVER_TIME_FORMAT
This commit use a datetime format instead the dateformat.
opw-775833
---
addons/stock/models/stock_warehouse.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/addons/stock/models/stock_warehouse.py b/addons/stock/models/stock_warehouse.py
index 4dd80595c53..b112d11567b 100644
--- a/addons/stock/models/stock_warehouse.py
+++ b/addons/stock/models/stock_warehouse.py
@@ -8,7 +8,7 @@ from dateutil import relativedelta
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError, ValidationError
-from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
+from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
import logging
@@ -843,7 +843,7 @@ class Orderpoint(models.Model):
# These days will be substracted when creating the PO
days += self.product_id._select_seller().delay or 0.0
date_planned = start_date + relativedelta.relativedelta(days=days)
- return date_planned.strftime(DEFAULT_SERVER_DATE_FORMAT)
+ return date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
@api.multi
def _prepare_procurement_values(self, product_qty, date=False, group=False):
From 671c20555e97fa882dfc42cbd9e429141a87e7c5 Mon Sep 17 00:00:00 2001
From: amoyaux
Date: Fri, 13 Oct 2017 11:59:16 +0200
Subject: [PATCH 028/153] [FIX] mrp: Unbuild multi serial number components
Use case to reproduce:
- Create a product with a BOM that use at least
twice the same component tracked by SN
- Build this product with SN 'A' and SN 'B'
- Unbuild it
-> Constraint error on duplicate SN
It happens because the unbuild order will create only
one stock.move.lots for SN A with a quantity of 2.
This commit create a stock.move.lots by quant used,
for example if the MO used 2 quants with a qty of 1
and one with lot 'A' and the other with lot 'B'. It will
create 2 stock.move.lots with lots 'A' and 'B'.
Still a know issue: If the user try to unbuild twice
the same MO it will reuse the same unbuilded componenets
than the first unbuild order.
---
addons/mrp/models/mrp_unbuild.py | 20 +++++++++++++-------
1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/addons/mrp/models/mrp_unbuild.py b/addons/mrp/models/mrp_unbuild.py
index 4fc4af387c4..90ab1cdeed4 100644
--- a/addons/mrp/models/mrp_unbuild.py
+++ b/addons/mrp/models/mrp_unbuild.py
@@ -121,14 +121,20 @@ class MrpUnbuild(models.Model):
for produce_move in produce_moves:
if produce_move.has_tracking != 'none':
original = original_quants.filtered(lambda quant: quant.product_id == produce_move.product_id)
- if original and not original[0].lot_id:
+ if not original:
raise UserError(_("You don't have in the stock the required lot/serial number for %s .") % (produce_move.product_id.name,))
- self.env['stock.move.lots'].create({
- 'move_id': produce_move.id,
- 'lot_id': original[0].lot_id.id,
- 'quantity_done': produce_move.product_uom_qty,
- 'quantity': produce_move.product_uom_qty
- })
+ quantity_todo = produce_move.product_qty
+ for quant in original:
+ if quantity_todo <= 0:
+ break
+ move_quantity = min(quantity_todo, quant.qty)
+ self.env['stock.move.lots'].create({
+ 'move_id': produce_move.id,
+ 'lot_id': quant.lot_id.id,
+ 'quantity_done': produce_move.product_id.uom_id._compute_quantity(move_quantity, produce_move.product_uom),
+ 'quantity': produce_move.product_id.uom_id._compute_quantity(move_quantity, produce_move.product_uom),
+ })
+ quantity_todo -= move_quantity
else:
produce_move.quantity_done = produce_move.product_uom_qty
produce_moves.move_validate()
From 912a7318d929720b1a2c0bbf05909120518e19fe Mon Sep 17 00:00:00 2001
From: Martin Trigaux
Date: Mon, 16 Oct 2017 13:12:53 +0200
Subject: [PATCH 029/153] [FIX] mrp: clarify uncomplete message
When selecting the BoM type 'kit' the explanations were unclear
Courtesy of Erwin van der Ploeg, Odoo Experts
opw-776612
---
addons/mrp/i18n/mrp.pot | 42 +++++++++++++-----------------
addons/mrp/models/mrp_bom.py | 5 ++--
addons/mrp/views/mrp_bom_views.xml | 17 +++++++++---
3 files changed, 34 insertions(+), 30 deletions(-)
diff --git a/addons/mrp/i18n/mrp.pot b/addons/mrp/i18n/mrp.pot
index 617117deea1..0b2fac329be 100644
--- a/addons/mrp/i18n/mrp.pot
+++ b/addons/mrp/i18n/mrp.pot
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-10 11:34+0000\n"
-"PO-Revision-Date: 2017-10-10 11:34+0000\n"
+"POT-Creation-Date: 2017-10-16 11:13+0000\n"
+"PO-Revision-Date: 2017-10-16 11:13+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -423,18 +423,12 @@ msgstr ""
#: model:ir.actions.act_window,name:mrp.template_open_bom
#: model:ir.model.fields,field_description:mrp.field_product_product_bom_ids
#: model:ir.model.fields,field_description:mrp.field_product_template_bom_ids
-#: model:ir.ui.view,arch_db:mrp.mrp_bom_line_tree_view
#: model:ir.ui.view,arch_db:mrp.mrp_bom_tree_view
#: model:ir.ui.view,arch_db:mrp.product_product_form_view_bom_button
#: model:ir.ui.view,arch_db:mrp.product_template_form_view_bom_button
msgid "Bill of Materials"
msgstr ""
-#. module: mrp
-#: model:ir.actions.act_window,name:mrp.mrp_bom_line_action_from_bom
-msgid "Bill of Materials Structure"
-msgstr ""
-
#. module: mrp
#: model:ir.model.fields,help:mrp.field_mrp_production_bom_id
msgid "Bill of Materials allow you to define the list of required raw materials to make a finished product."
@@ -1202,6 +1196,21 @@ msgstr ""
msgid "If the manufacturing order is unlocked you can add to the initial demand."
msgstr ""
+#. module: mrp
+#: model:ir.ui.view,arch_db:mrp.mrp_bom_form_view
+msgid "If the product is a finished product: When processing a sales\n"
+" order for this product, the delivery order will contain the raw\n"
+" materials, instead of the finished product."
+msgstr ""
+
+#. module: mrp
+#: model:ir.ui.view,arch_db:mrp.mrp_bom_form_view
+msgid "If the product is a semi-finished product: When processing a\n"
+" manufacturing order that contains that product as component,\n"
+" the raw materials of that product will be added to the\n"
+" manufacturing order of the final product."
+msgstr ""
+
#. module: mrp
#: model:ir.ui.view,arch_db:mrp.mrp_workcenter_kanban
#: model:ir.ui.view,arch_db:mrp.stock_production_type_kanban
@@ -1301,14 +1310,10 @@ msgstr ""
#. module: mrp
#: model:ir.ui.view,arch_db:mrp.view_mrp_bom_filter
+#: selection:mrp.bom,type:0
msgid "Kit"
msgstr ""
-#. module: mrp
-#: model:ir.model.fields,help:mrp.field_mrp_bom_type
-msgid "Kit (Phantom): When processing a sales order for this product, the delivery order will contain the raw materials, instead of the finished product."
-msgstr ""
-
#. module: mrp
#: model:product.product,name:mrp.product_product_laptop_charger
#: model:product.template,name:mrp.product_product_laptop_charger_product_template
@@ -2601,11 +2606,6 @@ msgstr ""
msgid "Settings"
msgstr ""
-#. module: mrp
-#: selection:mrp.bom,type:0
-msgid "Ship this product as a set of components (kit)"
-msgstr ""
-
#. module: mrp
#: code:addons/mrp/models/mrp_unbuild.py:89
#, python-format
@@ -3206,12 +3206,6 @@ msgstr ""
msgid "When a procurement has a ‘produce’ route with a operation type set, it will try to create a Manufacturing Order for that product using a BoM of the same operation type. That allows to define procurement rules which trigger different manufacturing orders with different BoMs."
msgstr ""
-#. module: mrp
-#: model:ir.ui.view,arch_db:mrp.mrp_bom_form_view
-msgid "When processing a sales order for this product, the delivery order\n"
-" will contain the raw materials, instead of the finished product."
-msgstr ""
-
#. module: mrp
#: model:ir.model.fields,help:mrp.field_stock_warehouse_manufacture_to_resupply
msgid "When products are manufactured, they can be manufactured in this warehouse."
diff --git a/addons/mrp/models/mrp_bom.py b/addons/mrp/models/mrp_bom.py
index f41e4a5a111..4d9a0e5bcbe 100644
--- a/addons/mrp/models/mrp_bom.py
+++ b/addons/mrp/models/mrp_bom.py
@@ -24,9 +24,8 @@ class MrpBom(models.Model):
help="If the active field is set to False, it will allow you to hide the bills of material without removing it.")
type = fields.Selection([
('normal', 'Manufacture this product'),
- ('phantom', 'Ship this product as a set of components (kit)')], 'BoM Type',
- default='normal', required=True,
- help="Kit (Phantom): When processing a sales order for this product, the delivery order will contain the raw materials, instead of the finished product.")
+ ('phantom', 'Kit')], 'BoM Type',
+ default='normal', required=True)
product_tmpl_id = fields.Many2one(
'product.template', 'Product',
domain="[('type', 'in', ['product', 'consu'])]", required=True)
diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml
index 3d6f1cb793a..787ef1ea3ba 100644
--- a/addons/mrp/views/mrp_bom_views.xml
+++ b/addons/mrp/views/mrp_bom_views.xml
@@ -29,9 +29,20 @@
-
- When processing a sales order for this product, the delivery order
- will contain the raw materials, instead of the finished product.
+
+
+
+ If the product is a finished product: When processing a sales
+ order for this product, the delivery order will contain the raw
+ materials, instead of the finished product.
+
+
+ If the product is a semi-finished product: When processing a
+ manufacturing order that contains that product as component,
+ the raw materials of that product will be added to the
+ manufacturing order of the final product.
+
+
From 3315c7c727cf3db258d1e5a4d5605a49d74eab4b Mon Sep 17 00:00:00 2001
From: Khoi Nguyen
Date: Fri, 13 Oct 2017 15:30:40 +0200
Subject: [PATCH 030/153] [REV] mrp: reallow quantity/lot/SN changes from work
orders 'manager view'"
This reverts commit
https://github.com/odoo/odoo/commit/44f06a66467cb39780fa65d0cefb44704b072e94.
The reverted changes will be applied in enterprise/quality_mrp instead.
---
addons/mrp/views/mrp_workorder_views.xml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/addons/mrp/views/mrp_workorder_views.xml b/addons/mrp/views/mrp_workorder_views.xml
index 994b4e1dd68..a3522f85999 100644
--- a/addons/mrp/views/mrp_workorder_views.xml
+++ b/addons/mrp/views/mrp_workorder_views.xml
@@ -140,13 +140,13 @@
-
-
+
+
-
+
From 296498fb588614b8ce64da47954faac71eef1751 Mon Sep 17 00:00:00 2001
From: qsm-odoo
Date: Mon, 16 Oct 2017 11:11:18 +0200
Subject: [PATCH 031/153] [FIX] web_editor: do not break when pasting a
element
This fixes a bug when pasting text in a paragraph, it was always
creating a new paragraph for no reason.
---
.../static/lib/summernote/src/js/module/Clipboard.js | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/addons/web_editor/static/lib/summernote/src/js/module/Clipboard.js b/addons/web_editor/static/lib/summernote/src/js/module/Clipboard.js
index 8b42726ff7a..955b49437a8 100644
--- a/addons/web_editor/static/lib/summernote/src/js/module/Clipboard.js
+++ b/addons/web_editor/static/lib/summernote/src/js/module/Clipboard.js
@@ -206,6 +206,18 @@ define([
r.select();
}
+ // If only pasting a element in an unique element, only paste
+ // the element text
+ var $p = $arch.children('p');
+ var onlyAP = ($p.length === 1 && $arch.children().length === 1);
+ if (onlyAP) {
+ var $p1 = $(r.sc).closest('p');
+ var $p2 = $(r.ec).closest('p');
+ if ($p1.length && $p2.length && $p1[0] === $p2[0]) {
+ $arch.html($p.text());
+ }
+ }
+
/*
insert content
*/
From a18a3fd31a9217325c8709613544591ec71272cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A9ry=20Debongnie?=
Date: Mon, 16 Oct 2017 14:32:59 +0200
Subject: [PATCH 032/153] [FIX] web,report: display warning when report
download is blocked
iOS devices need to open a window to allow downloading files. This is
fine, unless the browser is setup to block the popups, which is quite
probably the default settings. In that case, it just fails silently,
and the user has no idea of what just happened.
So, with this commit, we simply display a warning to let the user know
of what is actually going on.
---
addons/report/i18n/report.pot | 14 ++++++++++++++
addons/report/static/src/js/qwebactionmanager.js | 7 +++++--
addons/web/static/src/js/core/ajax.js | 11 ++++++++++-
3 files changed, 29 insertions(+), 3 deletions(-)
diff --git a/addons/report/i18n/report.pot b/addons/report/i18n/report.pot
index d31525d6058..513a35c423c 100644
--- a/addons/report/i18n/report.pot
+++ b/addons/report/i18n/report.pot
@@ -38,6 +38,13 @@ msgid "No logo found!\n"
" your address, etc."
msgstr ""
+#. module: report
+#. openerp-web
+#: code:addons/report/static/src/js/qwebactionmanager.js:113
+#, python-format
+msgid "A popup window with your report was blocked. You may need to change your browser settings to allow popup windows for this page."
+msgstr ""
+
#. module: report
#: selection:report.paperformat,format:0
msgid "A0 5 841 x 1189 mm"
@@ -634,6 +641,13 @@ msgstr ""
msgid "VAT:"
msgstr ""
+#. module: report
+#. openerp-web
+#: code:addons/report/static/src/js/qwebactionmanager.js:113
+#, python-format
+msgid "Warning"
+msgstr ""
+
#. module: report
#: model:ir.ui.view,arch_db:report.external_layout_boxed
#: model:ir.ui.view,arch_db:report.external_layout_standard
diff --git a/addons/report/static/src/js/qwebactionmanager.js b/addons/report/static/src/js/qwebactionmanager.js
index 3f477bf7d16..b5756ceac5d 100644
--- a/addons/report/static/src/js/qwebactionmanager.js
+++ b/addons/report/static/src/js/qwebactionmanager.js
@@ -21,7 +21,7 @@ var WKHTMLTOPDF_MESSAGES = {
};
var trigger_download = function (session, response, c, action, options) {
- session.get_file({
+ return session.get_file({
url: '/report/download',
data: {data: JSON.stringify(response)},
complete: framework.unblockUI,
@@ -108,7 +108,10 @@ ActionManager.include({
report_urls['qweb-pdf'],
action.report_type, //The 'root' report is considered the maine one, so we use its type for all the others.
];
- trigger_download(self.getSession(), response, c, current_action, options);
+ var success = trigger_download(self.getSession(), response, c, current_action, options);
+ if (!success) {
+ self.do_warn(_t('Warning'), _t('A popup window with your report was blocked. You may need to change your browser settings to allow popup windows for this page.'), true);
+ }
treated_actions.push(current_action);
current_action = current_action.next_report_to_generate;
diff --git a/addons/web/static/src/js/core/ajax.js b/addons/web/static/src/js/core/ajax.js
index dee6f4599b4..fe9b00203ce 100644
--- a/addons/web/static/src/js/core/ajax.js
+++ b/addons/web/static/src/js/core/ajax.js
@@ -212,6 +212,9 @@ var loadJS = (function () {
* @param {Function} [options.success] callback in case of download success
* @param {Function} [options.error] callback in case of request error, provided with the error body
* @param {Function} [options.complete] called after both ``success`` and ``error` callbacks have executed
+ * @returns {boolean} a false value means that a popup window was blocked. This
+ * mean that we probably need to inform the user that something needs to be
+ * changed to make it work.
*/
function get_file(options) {
// need to detect when the file is done downloading (not used
@@ -232,7 +235,12 @@ function get_file(options) {
var url = options.session.url(options.url, params);
if (options.complete) { options.complete(); }
- return window.open(url);
+ var w = window.open(url);
+ if (!w || w.closed || typeof w.closed === 'undefined') {
+ // popup was blocked
+ return false;
+ }
+ return true;
}
var $form, $form_data = $('
');
@@ -309,6 +317,7 @@ function get_file(options) {
}
};
timer = setTimeout(waitLoop, CHECK_INTERVAL);
+ return true;
};
function post (controller_url, data) {
From 2d0c3abd5cd39d525456bc9c20ecd9a5526b230a Mon Sep 17 00:00:00 2001
From: Christophe Matthieu
Date: Mon, 16 Oct 2017 15:35:11 +0200
Subject: [PATCH 033/153] [FIX] account: reconciliation: loose the current view
when reload
Todo in master: change action and add information in params
---
.../reconciliation/reconciliation_action.js | 23 +++++++++++++++++--
1 file changed, 21 insertions(+), 2 deletions(-)
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js
index fabfee1ad00..9535fcb8f5b 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_action.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js
@@ -38,7 +38,9 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
// used to instanciate the action interface
ActionRenderer: ReconciliationRenderer.StatementRenderer,
// used to instanciate each widget line
- LineRenderer: ReconciliationRenderer.LineRenderer
+ LineRenderer: ReconciliationRenderer.LineRenderer,
+ // used context params
+ params: ['statement_ids'],
},
/**
@@ -56,6 +58,17 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
if (!this.action_manager) {
this.set_cp_bus(new Widget());
}
+ // Adding values from the context is necessary to put this information in the url via the action manager so that
+ // you can retrieve it if the person shares his url or presses f5
+ _.each(params.params, function (value, name) {
+ params.context[name] = name.indexOf('_ids') !== -1 ? _.map((value+'').split(), parseFloat) : value;
+ });
+ params.params = {};
+ _.each(this.config.params, function (name) {
+ if (params.context[name]) {
+ params.params[name] = name.indexOf('_ids') !== -1 && _.isArray(params.context[name]) ? params.context[name].join() : params.context[name];
+ }
+ });
},
/**
@@ -305,8 +318,14 @@ var ManualAction = StatementAction.extend({
config: {
Model: ReconciliationModel.ManualModel,
ActionRenderer: ReconciliationRenderer.ManualRenderer,
- LineRenderer: ReconciliationRenderer.ManualLineRenderer
+ LineRenderer: ReconciliationRenderer.ManualLineRenderer,
+ params: ['company_ids', 'mode', 'partner_ids', 'account_ids'],
},
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
/**
* call 'validate' or 'autoReconciliation' model method then destroy the
* reconcilied lines, update the not reconcilied and update the action
From c06aa12777af9288555881c77b5edc5a9044a70c Mon Sep 17 00:00:00 2001
From: Christophe Matthieu
Date: Mon, 16 Oct 2017 15:35:17 +0200
Subject: [PATCH 034/153] [FIX] account: reconciliation: small fixes
* reset pager when change filter
* display 10 lines in manual reconciliation (add limitMoveLines in action's
'params' options to choose the number)
---
.../reconciliation/reconciliation_action.js | 8 ++-
.../js/reconciliation/reconciliation_model.js | 53 +++++++++++--------
.../reconciliation/reconciliation_renderer.js | 8 +--
.../static/src/xml/account_reconciliation.xml | 2 +-
.../static/tests/reconciliation_tests.js | 7 ++-
5 files changed, 48 insertions(+), 30 deletions(-)
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_action.js b/addons/account/static/src/js/reconciliation/reconciliation_action.js
index 9535fcb8f5b..e2c63b9bc05 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_action.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_action.js
@@ -41,6 +41,8 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
LineRenderer: ReconciliationRenderer.LineRenderer,
// used context params
params: ['statement_ids'],
+ // number of moves lines displayed in 'match' mode
+ limitMoveLines: 5,
},
/**
@@ -53,7 +55,10 @@ var StatementAction = Widget.extend(ControlPanelMixin, {
this._super.apply(this, arguments);
this.action_manager = parent;
this.params = params;
- this.model = new this.config.Model(this, {modelName: "account.bank.statement.line"});
+ this.model = new this.config.Model(this, {
+ modelName: "account.bank.statement.line",
+ limitMoveLines: params.params && params.params.limitMoveLines || this.config.limitMoveLines,
+ });
this.widgets = [];
if (!this.action_manager) {
this.set_cp_bus(new Widget());
@@ -320,6 +325,7 @@ var ManualAction = StatementAction.extend({
ActionRenderer: ReconciliationRenderer.ManualRenderer,
LineRenderer: ReconciliationRenderer.ManualLineRenderer,
params: ['company_ids', 'mode', 'partner_ids', 'account_ids'],
+ limitMoveLines: 10,
},
//--------------------------------------------------------------------------
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js
index 1f83968f4d0..fd2850cfcb8 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_model.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js
@@ -66,6 +66,7 @@ var _t = core._t;
* }
* mv_lines: object - idem than reconciliation_proposition
* offset: integer
+ * limitMoveLines: integer
* filter: string
* [createForm]: {
* account_id: {
@@ -95,8 +96,11 @@ var StatementModel = BasicModel.extend({
/**
* @override
+ *
+ * @param {Widget} parent
+ * @param {object} options
*/
- init: function () {
+ init: function (parent, options) {
this._super.apply(this, arguments);
this.reconcileModels = [];
this.lines = {};
@@ -104,6 +108,7 @@ var StatementModel = BasicModel.extend({
this.valuemax = 0;
this.alreadyDisplayed = [];
this.defaultDisplayQty = 10;
+ this.limitMoveLines = options && options.limitMoveLines || 5;
},
//--------------------------------------------------------------------------
@@ -162,7 +167,9 @@ var StatementModel = BasicModel.extend({
* @returns {Deferred}
*/
changeFilter: function (handle, filter) {
- this.getLine(handle).filter = filter;
+ var line = this.getLine(handle);
+ line.filter = filter;
+ line.offset = 0;
return this._performMoveLine(handle);
},
/**
@@ -217,7 +224,7 @@ var StatementModel = BasicModel.extend({
* @returns {Deferred}
*/
changeOffset: function (handle, offset) {
- this.getLine(handle).offset += offset;
+ this.getLine(handle).offset += (offset > 0 ? 1 : -1) * this.limitMoveLines;
return this._performMoveLine(handle);
},
/**
@@ -357,14 +364,14 @@ var StatementModel = BasicModel.extend({
self.context.journal_id = statement.journal_id;
_.each(statement.st_lines_ids, function (id) {
self.lines[_.uniqueId('rline')] = {
- 'id': id,
- 'reconciled': false,
- 'mode': 'inactive',
- 'mv_lines': [],
- 'offset': 0,
- 'filter': "",
- 'reconciliation_proposition': [],
- 'reconcileModels': []
+ id: id,
+ reconciled: false,
+ mode: 'inactive',
+ mv_lines: [],
+ offset: 0,
+ filter: "",
+ reconciliation_proposition: [],
+ reconcileModels: [],
};
});
});
@@ -849,6 +856,7 @@ var StatementModel = BasicModel.extend({
return l.id === data.st_line.id;
});
line.visible = true;
+ line.limitMoveLines = self.limitMoveLines;
_.extend(line, data);
self._formatLineProposition(line, line.reconciliation_proposition);
if (!line.reconciliation_proposition.length) {
@@ -987,7 +995,7 @@ var StatementModel = BasicModel.extend({
})));
var filter = line.filter || "";
var offset = line.offset;
- var limit = 6;
+ var limit = this.limitMoveLines+1;
return this._rpc({
model: 'account.bank.statement.line',
method: 'get_move_lines_for_reconciliation_widget',
@@ -1263,15 +1271,16 @@ var ManualModel = StatementModel.extend({
*/
_formatLine: function (type, data) {
var line = this.lines[_.uniqueId('rline')] = _.extend(data, {
- 'type': type,
- 'reconciled': false,
- 'mode': 'inactive',
- 'offset': 0,
- 'filter': "",
- 'reconcileModels': [],
- 'account_id': this._formatNameGet([data.account_id, data.account_name]),
- 'st_line': data,
- 'visible': true
+ type: type,
+ reconciled: false,
+ mode: 'inactive',
+ offset: 0,
+ limitMoveLines: this.limitMoveLines,
+ filter: "",
+ reconcileModels: [],
+ account_id: this._formatNameGet([data.account_id, data.account_name]),
+ st_line: data,
+ visible: true
});
this._formatLineProposition(line, line.reconciliation_proposition);
if (!line.reconciliation_proposition.length) {
@@ -1335,7 +1344,7 @@ var ManualModel = StatementModel.extend({
})));
var filter = line.filter || "";
var offset = line.offset;
- var limit = 6;
+ var limit = this.limitMoveLines+1;
var args = [line.account_id.id, line.partner_id, excluded_ids, filter, offset, limit];
return this._rpc({
model: 'account.move.line',
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index d9aa12967e1..cd87c3e72fb 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -378,7 +378,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
// mv_lines
var $mv_lines = this.$('.match table tbody').empty();
- _.each(state.mv_lines.slice(0,5), function (line) {
+ _.each(state.mv_lines.slice(0, state.limitMoveLines), function (line) {
var $line = $(qweb.render("reconciliation.line.mv_line", {'line': line, 'state': state}));
if (!isNaN(line.id)) {
$('')
@@ -387,7 +387,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
}
$mv_lines.append($line);
});
- this.$('.match .fa-chevron-right').toggleClass('disabled', state.mv_lines.length <= 5);
+ this.$('.match .fa-chevron-right').toggleClass('disabled', state.mv_lines.length <= state.limitMoveLines);
this.$('.match .fa-chevron-left').toggleClass('disabled', !state.offset);
this.$('.match').css('max-height', !state.mv_lines.length && !state.filter.length ? '0px' : '');
@@ -629,13 +629,13 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
* @private
*/
_onPrevious: function () {
- this.trigger_up('change_offset', {'data': -5});
+ this.trigger_up('change_offset', {'data': -1});
},
/**
* @private
*/
_onNext: function () {
- this.trigger_up('change_offset', {'data': 5});
+ this.trigger_up('change_offset', {'data': 1});
},
/**
* @private
diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml
index da27c44d882..3152fad74a4 100644
--- a/addons/account/static/src/xml/account_reconciliation.xml
+++ b/addons/account/static/src/xml/account_reconciliation.xml
@@ -25,7 +25,7 @@
-
+
Tip: Hit CTRL-Enter to reconcile all the balanced items in the sheet.
diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js
index 314f2ded8c6..3a4fed986a0 100644
--- a/addons/account/static/tests/reconciliation_tests.js
+++ b/addons/account/static/tests/reconciliation_tests.js
@@ -475,7 +475,10 @@ var session = {
var options = {
context: {
statement_ids: [4]
- }
+ },
+ params: {
+ limitMoveLines: 5,
+ },
};
Datas.params = {
@@ -1051,7 +1054,7 @@ QUnit.module('account', {
var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options);
testUtils.addMockEnvironment(clientAction, {
- 'data': this.params.data,
+ data: this.params.data,
session: this.params.session,
});
clientAction.appendTo($('#qunit-fixture'));
From eda976e63dcad01f4b65755f1e20fe695ba16a8c Mon Sep 17 00:00:00 2001
From: qsm-odoo
Date: Mon, 16 Oct 2017 15:15:08 +0200
Subject: [PATCH 035/153] [FIX] web_editor: do not break labels' texts into
When editing the content of a which has XML branding (first
DOM element which is editable in its hierarchy), a element was
added inside. This behavior is there to automatically add elements
in empty editable / elements on edition. This should
however not apply on elements.
---
addons/web_editor/static/src/js/rte.summernote.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/web_editor/static/src/js/rte.summernote.js b/addons/web_editor/static/src/js/rte.summernote.js
index df8aae48055..c5a6656ea28 100644
--- a/addons/web_editor/static/src/js/rte.summernote.js
+++ b/addons/web_editor/static/src/js/rte.summernote.js
@@ -428,7 +428,7 @@ $.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
if (($node.is('[data-oe-type="html"]') || $node.is('[data-oe-field="arch"]')) &&
$node.hasClass("o_editable") &&
!$node[0].children.length &&
- "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small pre th td".toUpperCase().indexOf($node[0].nodeName) === -1) {
+ "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small pre th td label".toUpperCase().indexOf($node[0].nodeName) === -1) {
var p = $('
')[0];
$node.append( p );
range.createFromNode(p.firstChild).select();
From df8590e562aa5dd8795420c3d4146d3090fc1880 Mon Sep 17 00:00:00 2001
From: qsm-odoo
Date: Mon, 16 Oct 2017 15:33:13 +0200
Subject: [PATCH 036/153] [FIX] web_editor: properly save multi-edited elements
When an element is placed multiple times in a page, it is saved only
once. Unfortunately, when a view was divided in multiple editable
parts (XML branding), only one of those parts were saved.
---
addons/web_editor/static/src/js/rte.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/addons/web_editor/static/src/js/rte.js b/addons/web_editor/static/src/js/rte.js
index 17de4698939..8eef4cfe3a6 100644
--- a/addons/web_editor/static/src/js/rte.js
+++ b/addons/web_editor/static/src/js/rte.js
@@ -48,7 +48,7 @@ var History = function History ($editable) {
$editable.html(oSnap.contents).scrollTop(oSnap.scrollTop);
$(".oe_overlay").remove();
$(".note-control-selection").hide();
-
+
$editable.trigger("content_changed");
try {
@@ -438,7 +438,7 @@ var RTE = Widget.extend({
saveElement: function ($el, context) {
// remove multi edition
if ($el.data('oe-model')) {
- var key = $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression');
+ var key = $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression')+":"+$el.data('oe-xpath');
if (this.__saved[key]) return true;
this.__saved[key] = true;
}
From 44b3491450691aded18597c1169c5652570c7358 Mon Sep 17 00:00:00 2001
From: alexkuhn
Date: Mon, 16 Oct 2017 16:30:54 +0200
Subject: [PATCH 037/153] [FIX][P3] base, tools: prevent crash when importing
translation .po file (#20191)
With Odoo v11 running on Python3:
1. Go to Settings -> Translation -> Import Translation
2. Choose a valid .po file (e.g. generated with the menu Export Translation)
3. Click Import
There is a server error.
There are two bugs behind this error:
1. we open a temporary file that cannot insert bytes ('w+')
2. the name of the temporary file is an integer (its file descriptor)
Python2 processed some implicit conversion between str and bytes, so that the 1st error doesn't occur. The name of a temporary file in Python2 is ''.
We made the following corrections to the code:
1. open the temporary file with w+b access (write bytes)
2. enhanced a conditional statement so that we skip instructions in case where the field name of the tempfile is a string
Related Ticket:
opw-775445
---
odoo/addons/base/module/wizard/base_import_language.py | 2 +-
odoo/tools/translate.py | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/odoo/addons/base/module/wizard/base_import_language.py b/odoo/addons/base/module/wizard/base_import_language.py
index c0aca1669bd..9f7338ed592 100644
--- a/odoo/addons/base/module/wizard/base_import_language.py
+++ b/odoo/addons/base/module/wizard/base_import_language.py
@@ -29,7 +29,7 @@ class BaseLanguageImport(models.TransientModel):
def import_lang(self):
this = self[0]
this = this.with_context(overwrite=this.overwrite)
- with TemporaryFile('w+') as buf:
+ with TemporaryFile('w+b') as buf:
try:
buf.write(base64.decodestring(this.data))
diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py
index dddd7c936cb..18657d674ae 100644
--- a/odoo/tools/translate.py
+++ b/odoo/tools/translate.py
@@ -1043,7 +1043,9 @@ def trans_load_data(cr, fileobj, fileformat, lang, lang_name=None, verbose=True,
# Make a reader for the POT file and be somewhat defensive for the
# stable branch.
- if fileobj.name.endswith('.po'):
+
+ # when fileobj is a TemporaryFile, its name is an interget in P3, a string in P2
+ if isinstance(fileobj.name, str) and fileobj.name.endswith('.po'):
try:
# Normally the path looks like /path/to/xxx/i18n/lang.po
# and we try to find the corresponding
From c822eb4809be87e28c650d64cf43dd710a97240d Mon Sep 17 00:00:00 2001
From: Fabien Meghazi
Date: Mon, 16 Oct 2017 16:32:24 +0200
Subject: [PATCH 038/153] [FIX] crm: mark(lost|won) routes calls removed
crm.lead functions (opw: 773907)
crm.lead#case_mark_won and case_mark_lost have been removed in 1414b3e56eb784cef935cdbf33c85a62ebcb1b0b
merged in saas-12 but later commit 0b87f7662892d60e03f1016e4150d53aef979a06
reference those function and was merged in saas-11
This fix also improved logging in the controllers in order to log
unexpected exception.
---
addons/crm/controllers/main.py | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/addons/crm/controllers/main.py b/addons/crm/controllers/main.py
index d92758e961b..4f1a6960645 100644
--- a/addons/crm/controllers/main.py
+++ b/addons/crm/controllers/main.py
@@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
from odoo.addons.mail.controllers.main import MailController
from odoo import http
+_logger = logging.getLogger(__name__)
+
class CrmController(http.Controller):
@@ -12,8 +15,9 @@ class CrmController(http.Controller):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('crm.lead', int(res_id), token)
if comparison and record:
try:
- record.case_mark_won()
+ record.action_set_won()
except Exception:
+ _logger.exception("Could not mark crm.lead as won")
return MailController._redirect_to_messaging()
return redirect
@@ -22,8 +26,9 @@ class CrmController(http.Controller):
comparison, record, redirect = MailController._check_token_and_record_or_redirect('crm.lead', int(res_id), token)
if comparison and record:
try:
- record.case_mark_lost()
+ record.action_set_lost()
except Exception:
+ _logger.exception("Could not mark crm.lead as lost")
return MailController._redirect_to_messaging()
return redirect
@@ -34,5 +39,6 @@ class CrmController(http.Controller):
try:
record.convert_opportunity(record.partner_id.id)
except Exception:
+ _logger.exception("Could not convert crm.lead to opportunity")
return MailController._redirect_to_messaging()
return redirect
From 9882c938741c4d2468d817698517a7174df9d051 Mon Sep 17 00:00:00 2001
From: Raphael Collet
Date: Mon, 16 Oct 2017 16:10:13 +0200
Subject: [PATCH 039/153] [FIX] website_slides: avoid NULL characters in text
field
OPW 772560
---
addons/website_slides/static/src/js/slides_upload.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/addons/website_slides/static/src/js/slides_upload.js b/addons/website_slides/static/src/js/slides_upload.js
index b8fbe164421..d451fd6eb64 100644
--- a/addons/website_slides/static/src/js/slides_upload.js
+++ b/addons/website_slides/static/src/js/slides_upload.js
@@ -150,6 +150,8 @@ var SlideDialog = Widget.extend({
_.each(data.items, function (obj) {
page_content = page_content + obj.str + " ";
});
+ // page_content may contain null characters
+ page_content = page_content.replace(/\0/g, "");
self.index_content = self.index_content + page_number + ". " + page_content + '\n';
if (maxPages === page_number) {
if (loaded) {
From d301d6391d169d32ecd6c4ed1e8212bb228f0282 Mon Sep 17 00:00:00 2001
From: Pierre Masereel
Date: Mon, 16 Oct 2017 16:24:35 +0200
Subject: [PATCH 040/153] [FIX] point_of_sale: rounding error when create
picking
When a return picking is created from a POS order, the methods check
that the value rounded to the unit of measure is not null.
The problem is that a rounding is passed to the function 'float_is_zero'
in the parameter precision_digits instead of precision rounding.
This bug has been introduced in rev: https://github.com/odoo/odoo/commit/61304087e7f48c677837dabdc8f1f8aeb18fb52e
opw-771282
---
addons/point_of_sale/models/pos_order.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index 840ff6869b9..044a98bb721 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -628,7 +628,7 @@ class PosOrder(models.Model):
return_picking = Picking.create(return_vals)
return_picking.message_post(body=message)
- for line in order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu'] and not float_is_zero(l.qty, precision_digits=l.product_id.uom_id.rounding)):
+ for line in order.lines.filtered(lambda l: l.product_id.type in ['product', 'consu'] and not float_is_zero(l.qty, precision_rounding=l.product_id.uom_id.rounding)):
moves |= Move.create({
'name': line.name,
'product_uom': line.product_id.uom_id.id,
From c477f0764e2ab088233a68c53fc66eabf01ce2ec Mon Sep 17 00:00:00 2001
From: Denis Vermylen
Date: Mon, 16 Oct 2017 16:33:58 +0200
Subject: [PATCH 041/153] [FIX] sale: pricelist price base
When setting a pricelist to show the public price discount, it ignored
the pricelist item's base field (if not 'Other Pricelist') and set it
by default on the lst_price instead.
This did not work when you based your discount on the standard_price
(= cost price) with public price discount.
Now it does.
opw: 775275
---
addons/sale/models/sale.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 1c644a9cdaf..84ac2bfa5e8 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -857,7 +857,7 @@ class SaleOrderLine(models.Model):
return price
else:
from_currency = self.order_id.company_id.currency_id
- return from_currency.compute(product.lst_price, self.order_id.pricelist_id.currency_id)
+ return from_currency.compute(product[pricelist_item.base], self.order_id.pricelist_id.currency_id)
@api.multi
@api.onchange('product_id')
From 3d5298aff974e7212b3ece9b96e68f1ae4a78009 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9rome=20Maes?=
Date: Wed, 11 Oct 2017 16:07:22 +0200
Subject: [PATCH 042/153] [FIX] hr_timesheet_attendance: only include timesheet
in attendance/timesheet report
Report mixed analytic line and timesheet, but
only timesheet should be included.
Since a timesheet is an analytic line with
a project, this fix do the job.
opw-772517
---
.../report/hr_timesheet_attendance_report.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report.py b/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report.py
index 582926ab89a..a0765b0d5cf 100644
--- a/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report.py
+++ b/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report.py
@@ -42,6 +42,7 @@ class TimesheetAttendance(models.Model):
ts.unit_amount AS timesheet,
date_trunc('day', ts.date) AS date
FROM account_analytic_line AS ts
+ WHERE ts.project_id IS NOT NULL
) AS t
GROUP BY t.user_id, t.date
ORDER BY t.date
From 25725eae578d126f79c69f72a7a4e3b4136b4e5d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9rome=20Maes?=
Date: Wed, 11 Oct 2017 16:00:03 +0200
Subject: [PATCH 043/153] [FIX] hr_timesheet_sheet: set domain on action to see
timesheet only
Analytic lines should not be included when
showing the timesheet line of a given
sheet. A domain is thus needed on the action.
opw-772517
---
.../hr_timesheet_attendance/views/hr_timesheet_sheet_views.xml | 1 +
1 file changed, 1 insertion(+)
diff --git a/addons/hr_timesheet_attendance/views/hr_timesheet_sheet_views.xml b/addons/hr_timesheet_attendance/views/hr_timesheet_sheet_views.xml
index 7aba1d01c33..b465567946c 100644
--- a/addons/hr_timesheet_attendance/views/hr_timesheet_sheet_views.xml
+++ b/addons/hr_timesheet_attendance/views/hr_timesheet_sheet_views.xml
@@ -29,6 +29,7 @@
+ [('project_id','!=',False)]
From 757e9e586ca2f21288a1756297651df145d7cfa1 Mon Sep 17 00:00:00 2001
From: Cedric Snauwaert
Date: Mon, 16 Oct 2017 15:51:26 +0200
Subject: [PATCH 044/153] [FIX] account: fix balance = 0 in dashboard
In some case, user set a currency on the journal which is the same as the company currency. In that case, the field taken was amount_currency which should not be the case.
OPW 775123
---
addons/account/models/account_journal_dashboard.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py
index edb2e5a01ff..eddad2c5437 100644
--- a/addons/account/models/account_journal_dashboard.py
+++ b/addons/account/models/account_journal_dashboard.py
@@ -162,7 +162,7 @@ class account_journal(models.Model):
# optimization to read sum of balance from account_move_line
account_ids = tuple(filter(None, [self.default_debit_account_id.id, self.default_credit_account_id.id]))
if account_ids:
- amount_field = 'balance' if not self.currency_id else 'amount_currency'
+ amount_field = 'balance' if (not self.currency_id or self.currency_id == self.company_id.currency_id) else 'amount_currency'
query = """SELECT sum(%s) FROM account_move_line WHERE account_id in %%s;""" % (amount_field,)
self.env.cr.execute(query, (account_ids,))
query_results = self.env.cr.dictfetchall()
From a222afb7d4a6c75f09adaaae306b002fedbc0a1c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9rome=20Maes?=
Date: Mon, 16 Oct 2017 16:34:36 +0200
Subject: [PATCH 045/153] [FIX] sale_timesheet: add onchange to set default
value
When changing the type of a product to 'service' the
new service policy is empty by default, and should not
be !
---
addons/sale_timesheet/models/product.py | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/addons/sale_timesheet/models/product.py b/addons/sale_timesheet/models/product.py
index 1eda2b9aa7a..3fe382701d3 100644
--- a/addons/sale_timesheet/models/product.py
+++ b/addons/sale_timesheet/models/product.py
@@ -52,3 +52,12 @@ class ProductTemplate(models.Model):
def _onchange_service_tracking(self):
if self.service_tracking != 'task_global_project':
self.project_id = False
+
+ @api.onchange('type')
+ def _onchange_type(self):
+ if self.type == 'service':
+ self.invoice_policy = 'order'
+ self.service_type = 'timesheet'
+ elif self.type == 'consu':
+ self.invoice_policy = 'order'
+ self.service_type = 'manual'
From a9fd3f0f2f2b3066db25b201d49dc171c1dfa63c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9rome=20Maes?=
Date: Mon, 16 Oct 2017 17:21:59 +0200
Subject: [PATCH 046/153] [FIX] sale_timesheet: rounding display on project
overview
---
addons/sale_timesheet/controllers/main.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/addons/sale_timesheet/controllers/main.py b/addons/sale_timesheet/controllers/main.py
index 2095abee936..1f3e9b01103 100644
--- a/addons/sale_timesheet/controllers/main.py
+++ b/addons/sale_timesheet/controllers/main.py
@@ -55,8 +55,8 @@ class SaleTimesheetController(http.Controller):
dashboard_values['hours'][billable_type] = float_round(data.get('unit_amount'), precision_rounding=hour_rounding)
dashboard_values['hours']['total'] += float_round(data.get('unit_amount'), precision_rounding=hour_rounding)
# rates
- dashboard_values['rates'][billable_type] = float_round(data.get('unit_amount') / dashboard_total_hours * 100, precision_rounding=hour_rounding)
- dashboard_values['rates']['total'] += float_round(data.get('unit_amount') / dashboard_total_hours * 100, precision_rounding=hour_rounding)
+ dashboard_values['rates'][billable_type] = round(data.get('unit_amount') / dashboard_total_hours * 100, 2)
+ dashboard_values['rates']['total'] += round(data.get('unit_amount') / dashboard_total_hours * 100, 2)
# money_amount
so_lines = values['timesheet_lines'].mapped('so_line')
From 81218a833c3c2a2f4db99489eb09843d8b203167 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9rome=20Maes?=
Date: Mon, 16 Oct 2017 16:17:01 +0200
Subject: [PATCH 047/153] [FIX] sale_timesheet: access right when validate
invoice
When validate an invoice, if you are timesheet
user and billing user, it is forbidden for you
to create an analytic line without a project set.
This commit allow billing user to create AAL.
opw-776391
---
addons/sale_timesheet/__manifest__.py | 1 +
.../security/sale_timesheet_security.xml | 19 +++++++++++++++++++
2 files changed, 20 insertions(+)
create mode 100644 addons/sale_timesheet/security/sale_timesheet_security.xml
diff --git a/addons/sale_timesheet/__manifest__.py b/addons/sale_timesheet/__manifest__.py
index e65f7580af7..0cb9db63bae 100644
--- a/addons/sale_timesheet/__manifest__.py
+++ b/addons/sale_timesheet/__manifest__.py
@@ -16,6 +16,7 @@ have real delivered quantities in sales orders.
'website': 'https://www.odoo.com/page/warehouse',
'depends': ['sale_management', 'hr_timesheet'],
'data': [
+ 'security/sale_timesheet_security.xml',
'views/account_invoice_views.xml',
'views/sale_order_views.xml',
'views/product_views.xml',
diff --git a/addons/sale_timesheet/security/sale_timesheet_security.xml b/addons/sale_timesheet/security/sale_timesheet_security.xml
new file mode 100644
index 00000000000..f038e964ad8
--- /dev/null
+++ b/addons/sale_timesheet/security/sale_timesheet_security.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+ account.analytic.line.billing.user
+
+ [('project_id', '=', False)]
+
+
+
+
+
+
+
+
+
+
From 3993528bacf2231010ab65fef70dd17f93b26466 Mon Sep 17 00:00:00 2001
From: fwi-odoo
Date: Mon, 16 Oct 2017 16:55:59 +0200
Subject: [PATCH 048/153] [FIX] web: polyfill to make the list editable work in
IE11
Internet Explorer doesn't implement 'contains' for document so we make a
polyfill for this method in order to be compatible.
---
addons/web/static/src/js/core/misc.js | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/addons/web/static/src/js/core/misc.js b/addons/web/static/src/js/core/misc.js
index f65276551cf..8ba3f635b9c 100644
--- a/addons/web/static/src/js/core/misc.js
+++ b/addons/web/static/src/js/core/misc.js
@@ -172,6 +172,23 @@ function ReloadContext (parent, action) {
}
core.action_registry.add("reload_context", ReloadContext);
+// In Internet Explorer, document doesn't have the contains method, so we make a
+// polyfill for the method in order to be compatible.
+if (!document.contains) {
+ document.contains = function contains (node) {
+ if (!(0 in arguments)) {
+ throw new TypeError('1 argument is required');
+ }
+
+ do {
+ if (this === node) {
+ return true;
+ }
+ } while (node = node && node.parentNode);
+
+ return false;
+ };
+}
return {
blockUI: blockUI,
From 48c17fd360b5936c104884e7a26e223c531c4691 Mon Sep 17 00:00:00 2001
From: fwi-odoo
Date: Mon, 16 Oct 2017 16:55:59 +0200
Subject: [PATCH 049/153] [FIX] web: polyfill to make the list editable work in
IE11
Internet Explorer doesn't implement 'contains' for document so we make a
polyfill for this method in order to be compatible.
---
addons/web/static/src/js/core/misc.js | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/addons/web/static/src/js/core/misc.js b/addons/web/static/src/js/core/misc.js
index 64ebeeb1794..983b26d3606 100644
--- a/addons/web/static/src/js/core/misc.js
+++ b/addons/web/static/src/js/core/misc.js
@@ -173,6 +173,23 @@ function ReloadContext (parent, action) {
}
core.action_registry.add("reload_context", ReloadContext);
+// In Internet Explorer, document doesn't have the contains method, so we make a
+// polyfill for the method in order to be compatible.
+if (!document.contains) {
+ document.contains = function contains (node) {
+ if (!(0 in arguments)) {
+ throw new TypeError('1 argument is required');
+ }
+
+ do {
+ if (this === node) {
+ return true;
+ }
+ } while (node = node && node.parentNode);
+
+ return false;
+ };
+}
return {
blockUI: blockUI,
From d5c0cae60b5cf7834ca9d669f9408a7c55694ff9 Mon Sep 17 00:00:00 2001
From: Lucas Perais
Date: Tue, 17 Oct 2017 09:37:08 +0200
Subject: [PATCH 050/153] [FIX] mass_mailing: background snippet option outlook
compatible (#20137)
Origin of the fix: the snippet background image did not change the background image.
This was due to commit a14f89c which wraps the snippet's content into a table for outlook display compliance
This issue is fixed here, with the modificaion of the background snippet option.
OPW 772442
---
.../mass_mailing/static/src/js/mass_mailing_editor.js | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/addons/mass_mailing/static/src/js/mass_mailing_editor.js b/addons/mass_mailing/static/src/js/mass_mailing_editor.js
index 651e0c979e4..e9aff44ec84 100644
--- a/addons/mass_mailing/static/src/js/mass_mailing_editor.js
+++ b/addons/mass_mailing/static/src/js/mass_mailing_editor.js
@@ -281,4 +281,15 @@ if ($("#editable_area").html().indexOf('on_change_model_and_list') !== -1) {
$("#editable_area").empty();
}
+// Adding compatibility for the outlook compliance of mailings.
+// Commit of such compatibility : a14f89c8663c9cafecb1cc26918055e023ecbe42
+options.registry.background.include({
+ start: function() {
+ this._super();
+ var $table_target = this.$target.find('table:first');
+ if ($table_target) {
+ this.$target = $table_target;
+ }
+ }
+});
});
From c2f59f08c70deaa4fcca605afa0062b68d1dbfdb Mon Sep 17 00:00:00 2001
From: Laurent Smet
Date: Tue, 10 Oct 2017 14:08:20 +0200
Subject: [PATCH 051/153] [IMP] base_vat_autocomplete: don't block the user
with VIES service
- When the VIES service is unreachable/unavailable, don't raise an error.
- Set the timeout to 5 seconds instead of default 120 seconds.
-task: 35043
---
.../base_vat_autocomplete/models/res_partner.py | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/addons/base_vat_autocomplete/models/res_partner.py b/addons/base_vat_autocomplete/models/res_partner.py
index 3b25f99a8f1..41b40ec9c1d 100644
--- a/addons/base_vat_autocomplete/models/res_partner.py
+++ b/addons/base_vat_autocomplete/models/res_partner.py
@@ -6,6 +6,8 @@ import re
from odoo import api, models
+from suds.client import Client
+
_logger = logging.getLogger(__name__)
try:
@@ -14,6 +16,7 @@ except ImportError:
_logger.warning('Python `stdnum` library not found, unable to call VIES service to detect address based on VAT number.')
stdnum_vat = None
+
class ResPartner(models.Model):
_inherit = 'res.partner'
@@ -40,7 +43,17 @@ class ResPartner(models.Model):
if not partner.vat:
return {}
if len(partner.vat) > 5 and partner.vat[:2].lower() in stdnum_vat.country_codes:
- result = stdnum_vat.check_vies(partner.vat)
+ # Equivalent to stdnum_vat.check_vies(partner.vat).
+ # However, we want to add a custom timeout to the suds.client
+ # because by default, it's 120 seconds and this is to long.
+ try:
+ client = Client(stdnum_vat.vies_wsdl, timeout=5)
+ partner_vat = stdnum_vat.compact(partner.vat)
+ result = client.service.checkVat(partner_vat[:2], partner_vat[2:])
+ except:
+ # Avoid blocking the client when the service is unreachable/unavailable
+ return {}
+
if not result['valid']:
return {}
From 55af66dcda83641c9de84db7befecbadb71c2eee Mon Sep 17 00:00:00 2001
From: Yannick Tivisse
Date: Tue, 17 Oct 2017 10:23:57 +0200
Subject: [PATCH 052/153] [FIX] mass_mailing: redefine the usable models in
mass mailing
Displaying all the model is not readable for a common user. We should only display models that are fit to be used in a mass mailing campaign.
---
addons/mass_mailing/models/mass_mailing.py | 12 +++++++++++-
addons/mass_mailing/views/mass_mailing_views.xml | 2 +-
2 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py
index ca2e739c35c..2f3666efc77 100644
--- a/addons/mass_mailing/models/mass_mailing.py
+++ b/addons/mass_mailing/models/mass_mailing.py
@@ -15,6 +15,16 @@ from odoo.tools.translate import html_translate
_logger = logging.getLogger(__name__)
+MASS_MAILING_BUSINESS_MODELS = [
+ 'crm.lead',
+ 'event.registration',
+ 'hr.applicant',
+ 'res.partner',
+ 'event.track',
+ 'sale.order',
+ 'mail.mass_mailing.list',
+]
+
class MassMailingTag(models.Model):
"""Model of categories of mass mailing, i.e. marketing, newsletter, ... """
_name = 'mail.mass_mailing.tag'
@@ -320,7 +330,7 @@ class MassMailing(models.Model):
default=lambda self: self.env['mail.message']._get_default_from())
# recipients
mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mail.mass_mailing.contact', required=True)
- mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain="[('is_mail_thread', '=', True)]",
+ mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain=[('model', 'in', MASS_MAILING_BUSINESS_MODELS)],
default=lambda self: self.env.ref('mass_mailing.model_mail_mass_mailing_list').id)
mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name')
mailing_domain = fields.Char(string='Domain', oldname='domain', default=[])
diff --git a/addons/mass_mailing/views/mass_mailing_views.xml b/addons/mass_mailing/views/mass_mailing_views.xml
index 076a6da0921..9cc4b63c97a 100644
--- a/addons/mass_mailing/views/mass_mailing_views.xml
+++ b/addons/mass_mailing/views/mass_mailing_views.xml
@@ -415,7 +415,7 @@
-
+
Date: Tue, 17 Oct 2017 11:11:34 +0200
Subject: [PATCH 053/153] [FIX] web_editor: fixed issue of chrome triple click
Partial backport of 11.0 commit (https://github.com/odoo/odoo/commit/6ad2ae30354a77c0f898cb9cb5a859713061d71a)
to solve issue 774152. Note: it has to be forward-ported as
implementation has been improved to handle mass mailing cases.
When an user triple-clicked on a paragraph and then typed text, the
paragraph was removed instead of its content (so the new text was
added at the beginning of the next paragraph).
See: http://stackoverflow.com/questions/38467334/why-does-google-chrome-always-add-space-after-selected-text
---
addons/web_editor/static/src/js/rte.js | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/addons/web_editor/static/src/js/rte.js b/addons/web_editor/static/src/js/rte.js
index 8eef4cfe3a6..37b87553506 100644
--- a/addons/web_editor/static/src/js/rte.js
+++ b/addons/web_editor/static/src/js/rte.js
@@ -586,6 +586,28 @@ var RTE = Widget.extend({
setTimeout(function () {
self.historyRecordUndo($target, 'activate', true);
},0);
+
+ // To Fix Google Chrome Tripleclick Issue, which selects the ending
+ // whitespace characters (so Tripleclicking then typing text will remove
+ // the whole paragraph instead of its content).
+ // http://stackoverflow.com/questions/38467334/why-does-google-chrome-always-add-space-after-selected-text
+ if ($.browser.chrome === true && event.originalEvent.detail === 3) {
+ var currentSelection = range.create();
+ if (currentSelection.sc.parentNode === currentSelection.ec) {
+ _selectSC(currentSelection);
+ } else if (currentSelection.eo === 0) {
+ var $hasNext = $(currentSelection.sc).parent();
+ while (!$hasNext.next().length && !$hasNext.is('body')) {
+ $hasNext = $hasNext.parent();
+ }
+ if ($hasNext.next()[0] === currentSelection.ec) {
+ _selectSC(currentSelection);
+ }
+ }
+ }
+ function _selectSC(selection) {
+ range.create(selection.sc, selection.so, selection.sc, selection.sc.length).select();
+ }
},
editable: function () {
From 61b63e9f81b8ddc64942b6f98b62b98d8946dce2 Mon Sep 17 00:00:00 2001
From: Wolfgang Taferner
Date: Mon, 9 Oct 2017 15:05:46 +0200
Subject: [PATCH 054/153] [FIX] stock: only if the type was changed we should
trigger an error
opw-776927
Closes #19973
Due to the fact that some products might be maintained in a module and
will write on the product on upgrade we need to take care that it will
allow to write not changed product types.
---
addons/stock/models/product.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/stock/models/product.py b/addons/stock/models/product.py
index a8445c68855..204e2548223 100644
--- a/addons/stock/models/product.py
+++ b/addons/stock/models/product.py
@@ -500,7 +500,7 @@ class ProductTemplate(models.Model):
done_moves = self.env['stock.move'].search([('product_id', 'in', updated.mapped('product_variant_ids').ids)], limit=1)
if done_moves:
raise UserError(_("You can not change the unit of measure of a product that has already been used in a done stock move. If you need to change the unit of measure, you may deactivate this product."))
- if 'type' in vals:
+ if any('type' in vals and vals['type'] != prod_tmpl.type for prod_tmpl in self):
existing_move_lines = self.env['stock.move.line'].search([
('product_id', 'in', self.mapped('product_variant_ids').ids),
('state', 'in', ['partially_available', 'assigned']),
From 541d2d736ed5d70762bc787f3497be6447f4b7d4 Mon Sep 17 00:00:00 2001
From: qsm-odoo
Date: Tue, 17 Oct 2017 13:04:17 +0200
Subject: [PATCH 055/153] [FIX] website: restore "Show Sign In" option
Splitting of website into website/portal made the "Show Sign In" option
disappear.
---
addons/website/views/website_templates.xml | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index b1c2227d8d3..8447cc766b1 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -226,6 +226,10 @@
+
+
+
+
From 9ae78328ec6fa0c1ba5c0a5f7583a565c7fa4e3f Mon Sep 17 00:00:00 2001
From: Raphael Collet
Date: Thu, 31 Aug 2017 14:38:23 +0200
Subject: [PATCH 056/153] [FIX] fields: writing on a one2many with a domain
should not remove all lines
Specifically, the command `(6, 0, ids)` should only unlink/detach the lines
that satisfy to the field's domain.
Test from #18440
opw-756983
Closes #18438
Closes #18440
---
.../test_new_api/tests/test_new_fields.py | 24 +++++++++++++++++++
openerp/osv/fields.py | 9 +++++--
2 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/openerp/addons/test_new_api/tests/test_new_fields.py b/openerp/addons/test_new_api/tests/test_new_fields.py
index fe118ba44ca..6ac223bd458 100644
--- a/openerp/addons/test_new_api/tests/test_new_fields.py
+++ b/openerp/addons/test_new_api/tests/test_new_fields.py
@@ -528,6 +528,30 @@ class TestNewFields(common.TransactionCase):
self.assertFalse(discussion.very_important_messages)
self.assertFalse(message.exists())
+ def test_70_x2many_write(self):
+ discussion = self.env.ref('test_new_api.discussion_0')
+ Message = self.env['test_new_api.message']
+ # There must be 3 messages, 0 important
+ self.assertEqual(len(discussion.messages), 3)
+ self.assertEqual(len(discussion.important_messages), 0)
+ self.assertEqual(len(discussion.very_important_messages), 0)
+ discussion.important_messages = [(0, 0, {
+ 'body': 'What is the answer?',
+ 'important': True,
+ })]
+ # There must be 4 messages, 1 important
+ self.assertEqual(len(discussion.messages), 4)
+ self.assertEqual(len(discussion.important_messages), 1)
+ self.assertEqual(len(discussion.very_important_messages), 1)
+ discussion.very_important_messages |= Message.new({
+ 'body': '42',
+ 'important': True,
+ })
+ # There must be 5 messages, 2 important
+ self.assertEqual(len(discussion.messages), 5)
+ self.assertEqual(len(discussion.important_messages), 2)
+ self.assertEqual(len(discussion.very_important_messages), 2)
+
class TestMagicFields(common.TransactionCase):
diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py
index b451da449e6..99a51c4e79b 100644
--- a/openerp/osv/fields.py
+++ b/openerp/osv/fields.py
@@ -861,8 +861,13 @@ class one2many(_column):
# Must use write() to recompute parent_store structure if needed
obj.write(cr, user, act[2], {self._fields_id:id}, context=context or {})
ids2 = act[2] or [0]
- cr.execute('select id from '+_table+' where '+self._fields_id+'=%s and id <> ALL (%s)', (id,ids2))
- ids3 = map(lambda x:x[0], cr.fetchall())
+ # if the o2m has a static domain we must respect it when unlinking
+ domain = (self._domain(original_obj)
+ if callable(self._domain) else self._domain)
+ extra_domain = domain or []
+ ids3 = obj.search(cr, user, [(self._fields_id,'=',id), ('id','not in',ids2)] + extra_domain, context=context)
+ # If the model has cascade deletion, we delete the rows because it is the intended behavior,
+ # otherwise we only nullify the reverse foreign key column.
inverse_field = obj._fields.get(self._fields_id)
if getattr(inverse_field, "ondelete", None) == "cascade":
obj.unlink(cr, user, ids3, context=context)
From 539a09a93ca9776c589f890d31bf132d5ed87dc4 Mon Sep 17 00:00:00 2001
From: Nicolas Martinelli
Date: Mon, 16 Oct 2017 08:42:06 +0200
Subject: [PATCH 057/153] [FIX] stock: use method return
In case MRP is used with kits (phantom BOMs), the method
`action_explode` is called through the override of `action_confirm`.
However, `action_explode` modifies the recordset by deleting the move
with the original product since it is not relevant anymore.
We take this use case into account by reusing the return value of
`action_confirm`.
opw-772644
---
addons/stock/models/stock_move.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 30c988954d7..3c359c93b25 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -971,7 +971,7 @@ class StockMove(models.Model):
self._propagate_split(new_move, qty)
# returning the first element of list returned by action_confirm is ok because we checked it wouldn't be exploded (and
# thus the result of action_confirm should always be a list of 1 element length)
- new_move.action_confirm()
+ new_move = new_move.action_confirm()
# TDE FIXME: due to action confirm change
return new_move.id
From a48b5c45aa8abef24da31104187399702533d1fe Mon Sep 17 00:00:00 2001
From: alexkuhn
Date: Tue, 17 Oct 2017 14:20:30 +0200
Subject: [PATCH 058/153] [FIX] crm: stage auto-probability not taken into
account when creating an opportunity from kanban view (#20209)
1. on CRM module -> Select Pipeline -> Create a column (e.g. 'Test')
2. Edit this Stage
3. Toggle on the field 'Change Probability Automatically'
4. Set a stage probability (e.g. 50%)
5. Create an opportunity on this stage, using the quick create widget of the kanban view
=> The probability of the newly created opportunity is not 50%, as it should be expected
It takes the default_probability, using a default_stage_id, which is the 'New' column having initially a probability of 10%. The stage_id is correctly assigned to the newly created opportunity thanks to environment variable 'default_stage_id', but not the probability.
We solved this bug by updating the probability of the opportunity, after creating the object. We cannot do it at the same time, because BaseObject.create parse the missing fields in an order where probability comes before stage_id.
This fix is limited to the quick creation of opportunity by means of its name only. In particular, it doesn't overwrite the input of the user in the form view.
opw-772310
---
addons/crm/models/crm_lead.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 5a506b0da20..879350cf55d 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -282,6 +282,14 @@ class Lead(FormatAddress, models.Model):
# ORM override (CRUD, fields_view_get, ...)
# ----------------------------------------
+ @api.model
+ def name_create(self, name):
+ res = super(Lead, self).name_create(name)
+
+ # update the probability of the lead if the stage is set to update it automatically
+ self.browse(res[0])._onchange_stage_id()
+ return res
+
@api.model
def create(self, vals):
# set up context used to find the lead's sales team which is needed
From a0fcd5c7815283e11b26eb821e23fc93ef001f44 Mon Sep 17 00:00:00 2001
From: fda-odoo
Date: Mon, 16 Oct 2017 17:03:17 +0200
Subject: [PATCH 059/153] [FIX] web: use "get_formview_id" when opening record
from calendar view
Opening a record in a calendar view always showed the default form view when it is not a popup.
Example in CRM: opening an opportunity will always show a lead view.
The fix aims to use the "get_formview_id" method to find the right id of the view that must be shown instead
of using a default id.
opw-774272
---
.../js/views/calendar/calendar_controller.js | 21 ++++++++++++-------
.../web/static/tests/views/calendar_tests.js | 14 ++++++++++++-
2 files changed, 27 insertions(+), 8 deletions(-)
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 4168d901065..08d03c037f0 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -257,13 +257,20 @@ var CalendarController = AbstractController.extend({
id = id && parseInt(id).toString() === id ? parseInt(id) : id;
if (!this.eventOpenPopup) {
- this.do_action({
- type: 'ir.actions.act_window',
- res_id: id,
- res_model: this.modelName,
- views: [[this.formViewId || false, 'form']],
- target: 'current',
- context: event.context || self.context,
+ this._rpc({
+ model: self.modelName,
+ method: 'get_formview_id',
+ //The event can be called by a view that can have another context than the default one.
+ args: [[id], event.context || self.context],
+ }).then(function (viewId) {
+ self.do_action({
+ type:'ir.actions.act_window',
+ res_id: id,
+ res_model: self.modelName,
+ views: [[viewId || false, 'form']],
+ target: 'current',
+ context: event.context || self.context,
+ });
});
return;
}
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index ecf02be9c4a..ed9e416a302 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -975,6 +975,12 @@ QUnit.module('Views', {
viewOptions: {
initialDate: initialDate,
},
+ mockRPC: function (route, args) {
+ if (args.method === "get_formview_id") {
+ return $.Deferred().resolve('A view');
+ }
+ return this._super(route, args);
+ },
});
// click on an existing event to open the form view
@@ -985,7 +991,7 @@ QUnit.module('Views', {
type: "ir.actions.act_window",
res_id: 4,
res_model: "event",
- views: [[false, "form"]],
+ views: [['A view', "form"]],
target: "current",
context: {}
},
@@ -1103,6 +1109,12 @@ QUnit.module('Views', {
viewOptions: {
initialDate: initialDate,
},
+ mockRPC: function (route, args) {
+ if (args.method === "get_formview_id") {
+ return $.when(false);
+ }
+ return this._super(route, args);
+ },
});
assert.strictEqual(calendar.$('.fc-resizer').length, 0, "should not have resize button");
From a1e15b9fced014e58185b161b80eb34692b1474a Mon Sep 17 00:00:00 2001
From: fda-odoo
Date: Mon, 16 Oct 2017 17:03:17 +0200
Subject: [PATCH 060/153] [FIX] web: use "get_formview_id" when opening record
from calendar view
Opening a record in a calendar view always showed the default form view when it is not a popup.
Example in CRM: opening an opportunity will always show a lead view.
The fix aims to use the "get_formview_id" method to find the right id of the view that must be shown instead
of using a default id.
opw-774272
---
.../js/views/calendar/calendar_controller.js | 21 ++++++++++++-------
.../web/static/tests/views/calendar_tests.js | 14 ++++++++++++-
2 files changed, 27 insertions(+), 8 deletions(-)
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 8d27acb43dd..11390793c68 100644
--- a/addons/web/static/src/js/views/calendar/calendar_controller.js
+++ b/addons/web/static/src/js/views/calendar/calendar_controller.js
@@ -247,13 +247,20 @@ var CalendarController = AbstractController.extend({
id = id && parseInt(id).toString() === id ? parseInt(id) : id;
if (!this.eventOpenPopup) {
- this.do_action({
- type: 'ir.actions.act_window',
- res_id: id,
- res_model: this.modelName,
- views: [[this.formViewId || false, 'form']],
- target: 'current',
- context: event.context || self.context,
+ this._rpc({
+ model: self.modelName,
+ method: 'get_formview_id',
+ //The event can be called by a view that can have another context than the default one.
+ args: [[id], event.context || self.context],
+ }).then(function (viewId) {
+ self.do_action({
+ type:'ir.actions.act_window',
+ res_id: id,
+ res_model: self.modelName,
+ views: [[viewId || false, 'form']],
+ target: 'current',
+ context: event.context || self.context,
+ });
});
return;
}
diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js
index f1caba9ed4a..d40e66317ed 100644
--- a/addons/web/static/tests/views/calendar_tests.js
+++ b/addons/web/static/tests/views/calendar_tests.js
@@ -975,6 +975,12 @@ QUnit.module('Views', {
viewOptions: {
initialDate: initialDate,
},
+ mockRPC: function (route, args) {
+ if (args.method === "get_formview_id") {
+ return $.Deferred().resolve('A view');
+ }
+ return this._super(route, args);
+ },
});
// click on an existing event to open the form view
@@ -985,7 +991,7 @@ QUnit.module('Views', {
type: "ir.actions.act_window",
res_id: 4,
res_model: "event",
- views: [[false, "form"]],
+ views: [['A view', "form"]],
target: "current",
context: {}
},
@@ -1103,6 +1109,12 @@ QUnit.module('Views', {
viewOptions: {
initialDate: initialDate,
},
+ mockRPC: function (route, args) {
+ if (args.method === "get_formview_id") {
+ return $.when(false);
+ }
+ return this._super(route, args);
+ },
});
assert.strictEqual(calendar.$('.fc-resizer').length, 0, "should not have resize button");
From 90d9605c86540ff446eac46fc368001c1808031a Mon Sep 17 00:00:00 2001
From: Xavier Morel
Date: Tue, 17 Oct 2017 13:54:16 +0200
Subject: [PATCH 061/153] [FIX] web: encoding issue with moment locales
I don't quite get how it can trigger on user systems (as it apparently
requires resetlocale() to fail to set up an UTF-8-encoded-locale
somehow, disabling resetlocale() is how I could get the issue
triggered on my system), but anyway it's apparently possible for Odoo
to run with an ASCII system encoding, and it turns out Python 3 will
not use UTF-8 everywhere but will rather use whatever
locale.getpreferredencoding(False) yields, which in theory could be
completely bonkers.
This is an issue when using text IO with an implicit encoding, which
is what load_locale (/web/webclient/locale/) was doing.
* Fix by using binary IO
* Fix (2) by using direct-passthrough IO with specified encoding, that
way we let the WSGI server handle the file streaming
Fixes #20075
---
addons/web/controllers/main.py | 36 +++++++++++++++++++---------------
1 file changed, 20 insertions(+), 16 deletions(-)
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 17ca031a48f..67db357ec05 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -18,13 +18,14 @@ import os
import re
import sys
import time
+import zlib
+
import werkzeug
import werkzeug.utils
import werkzeug.wrappers
-import zlib
+import werkzeug.wsgi
+from werkzeug.urls import url_decode, iri_to_uri
from xml.etree import ElementTree
-from werkzeug import url_decode
-from werkzeug import iri_to_uri
import odoo
@@ -33,11 +34,11 @@ from odoo.api import call_kw, Environment
from odoo.modules import get_resource_path
from odoo.tools import crop_image, topological_sort, html_escape, pycompat
from odoo.tools.translate import _
-from odoo.tools.misc import str2bool, xlwt
+from odoo.tools.misc import str2bool, xlwt, file_open
from odoo.tools.safe_eval import safe_eval
from odoo import http
from odoo.http import content_disposition, dispatch_rpc, request, \
- serialize_exception as _serialize_exception
+ serialize_exception as _serialize_exception, Response
from odoo.exceptions import AccessError, UserError
from odoo.models import check_method_name
@@ -513,21 +514,24 @@ class WebClient(http.Controller):
@http.route('/web/webclient/locale/', type='http', auth="none")
def load_locale(self, lang):
magic_file_finding = [lang.replace("_", '-').lower(), lang.split('_')[0]]
- addons_path = http.addons_manifest['web']['addons_path']
- # load momentjs locale
- momentjs_locale = ""
for code in magic_file_finding:
try:
- with open(os.path.join(addons_path, 'web', 'static', 'lib', 'moment', 'locale', code + '.js'), 'r') as f:
- momentjs_locale = f.read()
- # we found a locale matching so we can exit
- break
+ return http.Response(
+ werkzeug.wsgi.wrap_file(
+ request.httprequest.environ,
+ file_open('web/static/lib/moment/locale/%s.js' % code, 'rb')
+ ),
+ content_type='application/javascript; charset=utf-8',
+ headers=[('Cache-Control', 'max-age=36000')],
+ direct_passthrough=True,
+ )
except IOError:
- continue
+ _logger.debug("No moment locale for code %s", code)
- # return the content of the locale
- headers = [('Content-Type', 'application/javascript'), ('Cache-Control', 'max-age=%s' % (36000))]
- return request.make_response(momentjs_locale, headers)
+ return request.make_response("", headers=[
+ ('Content-Type', 'application/javascript'),
+ ('Cache-Control', 'max-age=36000'),
+ ])
@http.route('/web/webclient/qweb', type='http', auth="none", cors="*")
def qweb(self, mods=None, db=None):
From c30f8c50a3f4492c68f7cc4c2c1ce6cb5bf5350b Mon Sep 17 00:00:00 2001
From: Wolfgang Taferner
Date: Sun, 15 Oct 2017 16:06:35 +0200
Subject: [PATCH 062/153] [FIX] stock, sale_stock: blocking a process twice due
to cancelled moves
---
addons/sale_stock/models/sale_order.py | 2 +-
addons/stock/models/stock_move.py | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py
index aa55b213294..96f4b388c22 100644
--- a/addons/sale_stock/models/sale_order.py
+++ b/addons/sale_stock/models/sale_order.py
@@ -225,7 +225,7 @@ class SaleOrderLine(models.Model):
if line.state != 'sale' or not line.product_id.type in ('consu','product'):
continue
qty = 0.0
- for move in line.move_ids:
+ for move in line.move_ids.filtered(lambda r: r.state != 'cancel'):
qty += move.product_qty
if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0:
continue
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 7bfa320bb50..47f3a99637a 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -912,6 +912,8 @@ class StockMove(models.Model):
if any(move.state == 'done' for move in self):
raise UserError(_('You cannot cancel a stock move that has been set to \'Done\'.'))
for move in self:
+ if move.state == 'cancel':
+ continue
move._do_unreserve()
siblings_states = (move.move_dest_ids.mapped('move_orig_ids') - move).mapped('state')
if move.propagate:
From d7cfa8c502f27bee5c2fccb35db47b08e3b3804b Mon Sep 17 00:00:00 2001
From: jaredkipe
Date: Tue, 3 Oct 2017 17:51:24 +0200
Subject: [PATCH 063/153] =?UTF-8?q?[FIX]:=20safe=5Feval=E2=80=99ing=20some?=
=?UTF-8?q?=20large=20scripts=20yield=20illegal=20opcodes?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Following the 3.6 wordcode change, EXTENDED_ARGS gets used for jumps
offset by more than 256 bytes (128 opcodes), most commonly
absolute (so many script of more than 128 opcodes with conditionals
arer going to trigger the issue).
In bytecoded Python versions, EXTENDED_ARGS was only necessary above
65k opcodes: the opcode would get a separate 16-bits argument, while
wordcode opcodes get 8-bit intrinsic.
---
odoo/tools/safe_eval.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/odoo/tools/safe_eval.py b/odoo/tools/safe_eval.py
index fec89fac087..7f5d6e35e71 100644
--- a/odoo/tools/safe_eval.py
+++ b/odoo/tools/safe_eval.py
@@ -96,6 +96,7 @@ _SAFE_OPCODES = _EXPR_OPCODES.union(set(opmap[x] for x in [
'POP_BLOCK', 'POP_EXCEPT', # Seems to be a special-case of POP_BLOCK for P3
'SETUP_LOOP', 'BREAK_LOOP', 'CONTINUE_LOOP',
'MAKE_FUNCTION', 'CALL_FUNCTION',
+ 'EXTENDED_ARG', # P3.6 for long jump offsets.
# P3: https://bugs.python.org/issue27213
'CALL_FUNCTION_EX',
# Already in P2 but apparently the first one is used more aggressively in P3
From 4d56e76d52388e1ac8378cfd6eeff393d905364e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Agathe=20Moll=C3=A9?=
Date: Tue, 17 Oct 2017 10:18:13 -0400
Subject: [PATCH 064/153] [FIX] Fix canadian province state_ca_NL name
---
odoo/addons/base/res/res.country.state.csv | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/odoo/addons/base/res/res.country.state.csv b/odoo/addons/base/res/res.country.state.csv
index 1f8379f864d..a9f72ee3ae3 100644
--- a/odoo/addons/base/res/res.country.state.csv
+++ b/odoo/addons/base/res/res.country.state.csv
@@ -534,7 +534,7 @@ state_ca_ab,ca,"Alberta","AB"
state_ca_bc,ca,"British Columbia","BC"
state_ca_mb,ca,"Manitoba","MB"
state_ca_nb,ca,"New Brunswick","NB"
-state_ca_nl,ca,"Newfoundland","NL"
+state_ca_nl,ca,"Newfoundland and Labrador","NL"
state_ca_nt,ca,"Northwest Territories","NT"
state_ca_ns,ca,"Nova Scotia","NS"
state_ca_nu,ca,"Nunavut","NU"
From 6e46ba7b460b5f155735c91f957a4dca3dec8f96 Mon Sep 17 00:00:00 2001
From: Laurent Smet
Date: Tue, 17 Oct 2017 15:19:31 +0200
Subject: [PATCH 065/153] [FIX] point_of_sale: fix taxes rounding in POS
Don't round the base amount at each step during compute_all of taxes.
-opw: 776986
---
addons/point_of_sale/static/src/js/models.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index a18bd707da6..43c375937ed 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -1407,7 +1407,7 @@ exports.Orderline = Backbone.Model.extend({
var recompute_base = function(base_amount, fixed_amount, percent_amount){
if(fixed_amount === 0.0 && percent_amount === 0.0)
return base_amount;
- return round_pr((base_amount - fixed_amount) / (1.0 + percent_amount / 100.0), currency_rounding);
+ return (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0);
}
// 4) Iterate the taxes in the reversed sequence order to retrieve the initial base of the computation.
@@ -1461,7 +1461,6 @@ exports.Orderline = Backbone.Model.extend({
taxes_vals.push(tax_vals);
});
-
return {
taxes: taxes_vals,
total_excluded: round_pr(total_excluded, currency_rounding_bak),
From 4ebb5efac44fd4fe9066c32c533b9130e6e2e242 Mon Sep 17 00:00:00 2001
From: Martin Trigaux
Date: Tue, 17 Oct 2017 10:46:14 +0200
Subject: [PATCH 066/153] [FIX] crm: match filter result with count
At 9d2f48dc and abea8d35 the `not opp.date_closed` condtion was added when
counting the number of overdue opportunites.
The result filter was still considering the closed opportunities.
opw-774421
---
addons/crm/views/crm_lead_views.xml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml
index a51c141bb52..0e4b844105f 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -340,7 +340,8 @@
('activity_ids.date_deadline', '<=', (context_today()+datetime.timedelta(days=7)).strftime('%Y-%m-%d'))
]"/>
From a13120c0391157b5fb34767fb341c786b5d4b970 Mon Sep 17 00:00:00 2001
From: Nicolas Martinelli
Date: Tue, 17 Oct 2017 16:38:45 +0200
Subject: [PATCH 067/153] [FIX] resource: default TZ
If TZ is not set on a user, the creation of a `hr.leave` won't populate
the `tz` field of the associated `resource.calendar.leaves` record.
However, the method `_get_leave_intervals` assumes that field field is
always filled in.
Set the TZ to UTC as a fallback.
opw-777031
---
addons/resource/models/resource.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py
index 9469c3847c4..7002961fd21 100644
--- a/addons/resource/models/resource.py
+++ b/addons/resource/models/resource.py
@@ -699,7 +699,7 @@ class ResourceCalendarLeaves(models.Model):
date_from = fields.Datetime('Start Date', required=True)
date_to = fields.Datetime('End Date', required=True)
tz = fields.Selection(
- _tz_get, string='Timezone', default=lambda self: self._context.get('tz', self.env.user.tz),
+ _tz_get, string='Timezone', default=lambda self: self._context.get('tz', self.env.user.tz or 'UTC'),
help="Timezone used when encoding the leave. It is used to correctly"
"localize leave hours when computing time intervals.")
resource_id = fields.Many2one(
From 727a642f5c72ee34e9d6f3c7a507591d05d7d839 Mon Sep 17 00:00:00 2001
From: Martin Trigaux
Date: Tue, 17 Oct 2017 16:53:17 +0200
Subject: [PATCH 068/153] [FIX] crm: match count with result
Followup of 4ebb5efac
---
addons/crm/views/crm_lead_views.xml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml
index 0e4b844105f..fdc77ef4b80 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -584,10 +584,10 @@
('activity_ids.date_deadline', '<=', (context_today()+datetime.timedelta(days=7)).strftime('%Y-%m-%d'))
]"/>
From d18b1605053d8c9ef9f3fe5c6d68a646ac40833b Mon Sep 17 00:00:00 2001
From: Xavier Morel
Date: Tue, 17 Oct 2017 17:10:31 +0200
Subject: [PATCH 069/153] [FIX] website_forum: properly read the faq
On P3, "text" IO (open(f, mode='r')) will default to the encoding
provided by locale.getpreferredencoding(False) which apparently is
regularly cp1252.
Replace by misc.file_open which enforces UTF-8 decoding in text mode.
---
addons/website_forum/models/forum.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/addons/website_forum/models/forum.py b/addons/website_forum/models/forum.py
index e8349e0cb9e..b9da18ffee3 100644
--- a/addons/website_forum/models/forum.py
+++ b/addons/website_forum/models/forum.py
@@ -12,7 +12,7 @@ from werkzeug.exceptions import Forbidden
from odoo import api, fields, models, modules, tools, SUPERUSER_ID, _
from odoo.exceptions import UserError, ValidationError
-from odoo.tools import pycompat
+from odoo.tools import pycompat, misc
_logger = logging.getLogger(__name__)
@@ -38,10 +38,8 @@ class Forum(models.Model):
@api.model
def _get_default_faq(self):
- fname = modules.get_module_resource('website_forum', 'data', 'forum_default_faq.html')
- with open(fname, 'r') as f:
+ with misc.file_open('website_forum/data/forum_default_faq.html', 'r') as f:
return f.read()
- return False
# description and use
name = fields.Char('Forum Name', required=True, translate=True)
From 6c8e6fb2e9ee28b76795052b8213bd206e8cad6a Mon Sep 17 00:00:00 2001
From: Nicolas Martinelli
Date: Tue, 17 Oct 2017 16:58:30 +0200
Subject: [PATCH 070/153] [FIX] stock: save settings
This reverts commit 0e28db88d13255434abf90af01675734d129d798.
This commit should not be in saas-15.
opw-776890
---
addons/stock/models/stock_config_settings.py | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/addons/stock/models/stock_config_settings.py b/addons/stock/models/stock_config_settings.py
index aa8f639e90b..ba5deae264f 100644
--- a/addons/stock/models/stock_config_settings.py
+++ b/addons/stock/models/stock_config_settings.py
@@ -86,15 +86,3 @@ class StockConfigSettings(models.TransientModel):
warehouses.mapped('int_type_id').write({'active': active})
return True
-
- @api.model
- def get_default_decimal_precision(self, fields):
- # don't forward-port in v11.0, the API of config wizards changed.
- digits = self.env.ref('product.decimal_stock_weight').digits
- return {'decimal_precision': digits}
-
- @api.multi
- def set_decimal_precision(self):
- # don't forward-port in v11.0, the API of config wizards changed.
- for record in self:
- self.env.ref('product.decimal_stock_weight').write({'digits': record.decimal_precision})
From 3285a0384c1bdabc5d6c9c906a7ed54d3e595e7c Mon Sep 17 00:00:00 2001
From: HIRAL BHAVSAR
Date: Tue, 17 Oct 2017 20:53:30 +0530
Subject: [PATCH 071/153] [FIX] mail: Wrong codepoint for angry emoji.
---
addons/mail/data/mail_shortcode_data.xml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/addons/mail/data/mail_shortcode_data.xml b/addons/mail/data/mail_shortcode_data.xml
index 64f1aa7129f..afd0cf91146 100644
--- a/addons/mail/data/mail_shortcode_data.xml
+++ b/addons/mail/data/mail_shortcode_data.xml
@@ -103,8 +103,8 @@
3:(
- 😈
- <img src='/mail/static/src/img/smiley/angry.png' alt='😈'/>
+ 😠
+ <img src='/mail/static/src/img/smiley/angry.png' alt='😠'/>Angryimage
From e0fbe7b5becf608f76f02123f3076cf27c54c20e Mon Sep 17 00:00:00 2001
From: rde
Date: Mon, 16 Oct 2017 14:19:19 +0200
Subject: [PATCH 072/153] [FIX] website_sale: disable checkout btn if GTS
unchecked at load
opw-774286
*Before this commit:
On checkout, you can enable a step to force user to have the General Terms of
Sale checked to continue the checkout. Checking/Unchecking will correctly
disabled/enabled the button.
The checkbox is already checked at page load (on the template).
BUT, if the template has been modified to set the checkbox as unchecked at load
(eg the user removed 'checked="checked"' from the template), the button will
still be enabled even if the checkbox is unchecked.
*Now:
No matter if it is checked or not on the template, the button will
correctly be enabled/disabled regarding the checkbox state at load.
---
addons/website_sale/static/src/js/website_sale_payment.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/addons/website_sale/static/src/js/website_sale_payment.js b/addons/website_sale/static/src/js/website_sale_payment.js
index 5b02f5b7c61..266c69a9db5 100644
--- a/addons/website_sale/static/src/js/website_sale_payment.js
+++ b/addons/website_sale/static/src/js/website_sale_payment.js
@@ -6,9 +6,10 @@ var ajax = require('web.ajax');
$(document).ready(function () {
// If option is enable
if ($("#checkbox_cgv").length) {
- $("#checkbox_cgv").click(function() {
+ $("#checkbox_cgv").change(function() {
$("div.oe_sale_acquirer_button").find('input, button').prop("disabled", !this.checked);
});
+ $('#checkbox_cgv').trigger('change');
}
// When choosing an acquirer, display its Pay Now button
From d365a289d4ed3f557fa46155419f5f517860fe9c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A9ry=20Debongnie?=
Date: Tue, 17 Oct 2017 16:29:35 +0200
Subject: [PATCH 073/153] [FIX] web: generate proper x2many commands in some
cases
When creating a new record with a non empty default value for a one2many
field, we did send the command 0 (for each one2many line) instead of the
command 1 (update). This was a pretty big problem, because then the
server would try to create a line with possibly incomplete data.
For example, it was not possible to create expense sheets: go to expense
menu, select one expense in the list view, click on the action menu
'submit to manager', this opens a form view with a one2many with the
selected expense, then save. Before this commit, this would lead to a
server error.
---
.../static/src/js/views/basic/basic_model.js | 16 +++++++++++++---
.../tests/fields/relational_fields_tests.js | 17 ++++++++++++++---
2 files changed, 27 insertions(+), 6 deletions(-)
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 073d8599f06..8798e8cdb2e 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -875,7 +875,10 @@ var BasicModel = AbstractModel.extend({
// id never changes, and should not be written
delete record._changes.id;
}
- var changes = self._generateChanges(record, {viewType: options.viewType});
+ var changes = self._generateChanges(record, {viewType: options.viewType, changesOnly: method !== 'create'});
+
+ // id field should never be written/changed
+ delete changes.id;
if (method === 'create') {
var fieldNames = record.getFieldNames();
@@ -2438,7 +2441,7 @@ var BasicModel = AbstractModel.extend({
changes = _.extend({}, record.data, record._changes);
} else {
changes = _.extend({}, record._changes);
- };
+ }
var withReadonly = options.withReadonly || false;
var commands = this._generateX2ManyCommands(record, {
changesOnly: 'changesOnly' in options ? options.changesOnly : true,
@@ -2475,6 +2478,7 @@ var BasicModel = AbstractModel.extend({
changes[fieldName] = false;
}
}
+
return changes;
},
/**
@@ -2621,7 +2625,13 @@ var BasicModel = AbstractModel.extend({
// this is a new id
relRecord = _.findWhere(relRecordAdded, {res_id: list.res_ids[i]});
changes = this._generateChanges(relRecord, options);
- commands[fieldName].push(x2ManyCommands.create(changes));
+ if ('id' in changes) {
+ var id = changes.id;
+ delete changes.id;
+ commands[fieldName].push(x2ManyCommands.update(id, changes));
+ } else {
+ commands[fieldName].push(x2ManyCommands.create(changes));
+ }
}
}
if (options.changesOnly && !didChange && addedIds.length === 0 && removedIds.length === 0) {
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 5d5e22e386e..5677443aa0f 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -1062,14 +1062,14 @@ QUnit.module('relational_fields', {
});
QUnit.test('list in form: default_get with x2many create and onchange', function (assert) {
- assert.expect(1);
+ assert.expect(2);
this.data.partner.onchanges.turtles = function (obj) {
assert.deepEqual(
obj.turtles,
[
- [0, false, {turtle_foo: 'blip', id: 2}],
- [0, false, {turtle_foo: 'kawa', id: 3}]
+ [1, 2, {turtle_foo: 'blip'}],
+ [1, 3, {turtle_foo: 'kawa'}]
],
"should have properly created the x2many command list");
};
@@ -1092,10 +1092,21 @@ QUnit.module('relational_fields', {
if (args.method === 'default_get') {
return $.when({turtles: [[6, 0, [2,3]]]});
}
+ if (args.method === 'create') {
+ // it would be even better if we did not send the current
+ // unchanged state with the command 1, but this seems more
+ // difficult.
+ assert.deepEqual(args.args[0].turtles, [
+ [1, 2, {turtle_foo: 'blip'}],
+ [1, 3, {turtle_foo: 'kawa'}]
+ ], 'should send proper commands to create method');
+ }
return this._super.apply(this, arguments);
},
});
+ form.$buttons.find('.o_form_button_save').click();
+
form.destroy();
});
From f971ed704c93738fb89576e44756c9e70ce7881d Mon Sep 17 00:00:00 2001
From: Goffin Simon
Date: Wed, 18 Oct 2017 09:49:34 +0200
Subject: [PATCH 074/153] [FIX]sale: pricelist price base
Fine tuning of this commit: c477f0764e2ab088233a68c53fc66eabf01ce2ec
opw:775275
---
addons/sale/models/sale.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py
index 84ac2bfa5e8..3c1de5d79a6 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -857,7 +857,7 @@ class SaleOrderLine(models.Model):
return price
else:
from_currency = self.order_id.company_id.currency_id
- return from_currency.compute(product[pricelist_item.base], self.order_id.pricelist_id.currency_id)
+ return from_currency.compute(product[pricelist_item.base].lst_price, self.order_id.pricelist_id.currency_id)
@api.multi
@api.onchange('product_id')
From 0012eb4c9704dd3430e024d0ce0e7bcb39c3c416 Mon Sep 17 00:00:00 2001
From: Goffin Simon
Date: Tue, 17 Oct 2017 16:59:38 +0200
Subject: [PATCH 075/153] [FIX] website_sale: included taxes considered as
discount in the shop
Steps to reproduce the bug:
- In Sales settings, set "Tax display" as "Show line subtotals without taxes"
- Create an article A with a customer tax set as price-inclusive.
- associate on an other article B the pricelist P with a discount_policy
set as "without_discount"
- Ensure that the pricelist P is used in the shop.
-Go to the shop
Bug: The article A is considered as discounted but the price is full.
The prices with included taxes were considered as discounted because
lst_price - webstie_price were always greater than 0. When the
"Show line subtotals without taxes" is checked for Tax display,
the field website_price never includes the taxes. That's why the
included taxes are substracted from the website_price.
opw:776526
---
addons/website_sale/models/product.py | 6 ++++++
addons/website_sale/views/templates.xml | 6 +++---
2 files changed, 9 insertions(+), 3 deletions(-)
diff --git a/addons/website_sale/models/product.py b/addons/website_sale/models/product.py
index 6c50e86c2b1..8d33629e927 100644
--- a/addons/website_sale/models/product.py
+++ b/addons/website_sale/models/product.py
@@ -3,6 +3,7 @@
from odoo import api, fields, models, tools, _
import odoo.addons.decimal_precision as dp
from odoo.tools.translate import html_translate
+from odoo.tools import float_is_zero
class ProductStyle(models.Model):
@@ -139,6 +140,7 @@ class ProductTemplate(models.Model):
website_price = fields.Float('Website price', compute='_website_price', digits=dp.get_precision('Product Price'))
website_public_price = fields.Float('Website public price', compute='_website_price', digits=dp.get_precision('Product Price'))
+ website_price_difference = fields.Boolean('Website price difference', compute='_website_price')
def _website_price(self):
# First filter out the ones that have no variant:
@@ -148,6 +150,7 @@ class ProductTemplate(models.Model):
for template, product in zip(self, self.mapped('product_variant_id')):
template.website_price = product.website_price
template.website_public_price = product.website_public_price
+ template.website_price_difference = product.website_price_difference
def _default_website_sequence(self):
self._cr.execute("SELECT MIN(website_sequence) FROM %s" % self._table)
@@ -188,6 +191,7 @@ class Product(models.Model):
website_price = fields.Float('Website price', compute='_website_price', digits=dp.get_precision('Product Price'))
website_public_price = fields.Float('Website public price', compute='_website_price', digits=dp.get_precision('Product Price'))
+ website_price_difference = fields.Boolean('Website price difference', compute='_website_price')
def _website_price(self):
qty = self._context.get('quantity', 1.0)
@@ -204,6 +208,8 @@ class Product(models.Model):
for p, p2 in zip(self, self2):
taxes = partner.property_account_position_id.map_tax(p.taxes_id.sudo().filtered(lambda x: x.company_id == company_id))
p.website_price = taxes.compute_all(p2.price, pricelist.currency_id, quantity=qty, product=p2, partner=partner)[ret]
+ price_without_pricelist = taxes.compute_all(p.list_price, pricelist.currency_id)[ret]
+ p.website_price_difference = False if float_is_zero(price_without_pricelist - p.website_price, precision_rounding=pricelist.currency_id.rounding) else True
p.website_public_price = taxes.compute_all(p2.lst_price, quantity=qty, product=p2, partner=partner)[ret]
@api.multi
diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml
index 060206aaab5..381648690da 100644
--- a/addons/website_sale/views/templates.xml
+++ b/addons/website_sale/views/templates.xml
@@ -102,7 +102,7 @@
-
+
@@ -587,7 +587,7 @@
@@ -997,7 +997,7 @@
-
+
From 4618cbd513036220fc210fadb1024ec1facd6559 Mon Sep 17 00:00:00 2001
From: rde
Date: Sun, 15 Oct 2017 23:56:33 +0200
Subject: [PATCH 076/153] [FIX] website: new class to disable button after
first click
Before this commit, if you had a simple link (just a href, not handled
in JS), you couldn't simply make it clickable only once.
This could be a problem in some cases. (eg: a btn creating a record, it would
create as many record as many times you clicked on it)
The same behavior could occur for link/button redirecting to a route creating a
record. There is a window of time between the click on the btn and the moment
the page is getting redirected.
You have time to click multiple time on it and it would call the URL multiple
time.
Now, there is a class 'js_disable_on_click' that will be handled in
website_root.js and will disabled the button after the first click.
The fix was originally aimed to fix multiple click on "Create Page" btn on 404
---
addons/website/static/src/js/content/website_root.js | 11 +++++++++++
addons/website/views/website_templates.xml | 2 +-
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/addons/website/static/src/js/content/website_root.js b/addons/website/static/src/js/content/website_root.js
index cf837b9df7c..7104a323040 100644
--- a/addons/website/static/src/js/content/website_root.js
+++ b/addons/website/static/src/js/content/website_root.js
@@ -39,6 +39,7 @@ var WebsiteRoot = BodyManager.extend({
'click .js_change_lang': '_onLangChangeClick',
'click .js_publish_management .js_publish_btn': '_onPublishBtnClick',
'submit .js_website_submit_form': '_onWebsiteFormSubmit',
+ 'click .js_disable_on_click': '_onDisableOnClick',
}),
custom_events: _.extend({}, BodyManager.prototype.custom_events || {}, {
animation_start_demand: '_onAnimationStartDemand',
@@ -224,6 +225,16 @@ var WebsiteRoot = BodyManager.extend({
$btn.button('loading');
});
},
+ /**
+ * Called when the root is notified that the button should be
+ * disabled after the first click.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onDisableOnClick: function (ev) {
+ $(ev.currentTarget).addClass('disabled');
+ },
});
return {
diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml
index 8447cc766b1..3f2046739d2 100644
--- a/addons/website/views/website_templates.xml
+++ b/addons/website/views/website_templates.xml
@@ -571,7 +571,7 @@
This page does not exists, but you can create it as you are administrator of this site.
diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py
index 46139dd4e65..fb6041e544a 100644
--- a/addons/mail/models/res_partner.py
+++ b/addons/mail/models/res_partner.py
@@ -61,7 +61,6 @@ class Partner(models.Model):
website_url = 'http://%s' % user.company_id.website if not user.company_id.website.lower().startswith(('http:', 'https:')) else user.company_id.website
else:
website_url = False
- company_name = user.company_id.name
model_name = False
if message.model:
@@ -81,9 +80,15 @@ class Partner(models.Model):
if message.res_id and message.model in self.env:
record = self.env[message.model].browse(message.res_id)
+ company = user.company_id;
+ if record and hasattr(record, 'company_id'):
+ company = record.company_id;
+ company_name = company.name;
+
return {
'signature': signature,
'website_url': website_url,
+ 'company': company,
'company_name': company_name,
'model_name': model_name,
'record': record,
From 3e5189e62803d7580e367301e19b7e3f9664fd90 Mon Sep 17 00:00:00 2001
From: Nicolas Martinelli
Date: Thu, 19 Oct 2017 11:04:12 +0200
Subject: [PATCH 130/153] [FIX] product: allow multiple values for same
attribute
- Create a product attribute, e.g. 'Recommanded Age'
- Create several values for this attribute
- Try to associate more than attribute value to the same variant =>
contraint which prevents to do it
We should allow this use case as long as the product attribute doesn't
create a variant.
opw-776344
---
addons/product/models/product.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/addons/product/models/product.py b/addons/product/models/product.py
index 6b0c04275bc..c3c79628e5c 100644
--- a/addons/product/models/product.py
+++ b/addons/product/models/product.py
@@ -303,7 +303,8 @@ class ProductProduct(models.Model):
for value in product.attribute_value_ids:
if value.attribute_id in attributes:
raise ValidationError(_('Error! It is not allowed to choose more than one value for a given attribute.'))
- attributes |= value.attribute_id
+ if value.attribute_id.create_variant:
+ attributes |= value.attribute_id
return True
@api.onchange('uom_id', 'uom_po_id')
From 1312ee72cf35e3887721ac56472f1188f636ec61 Mon Sep 17 00:00:00 2001
From: Martin Geubelle
Date: Thu, 19 Oct 2017 12:00:50 +0200
Subject: [PATCH 131/153] [FIX] web: correctly render the field image
Before this commit, the image was directly built without using the corresponding
template, which set some properties on the image (class, width, etc.).
---
.../web/static/src/js/fields/basic_fields.js | 7 +---
addons/web/static/src/xml/base.xml | 4 +--
.../static/tests/fields/basic_fields_tests.js | 36 +++++++++++++++++++
3 files changed, 39 insertions(+), 8 deletions(-)
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 3985202fbd4..21205bfc033 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -1129,7 +1129,6 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
supportedFieldTypes: ['binary'],
_render: function () {
var self = this;
- var attrs = this.attrs;
var url = this.placeholder;
if (this.value) {
if (!utils.is_bin_size(this.value)) {
@@ -1144,11 +1143,7 @@ var FieldBinaryImage = AbstractFieldBinary.extend({
});
}
}
- var $img = $('').attr('src', url);
- $img.css({
- width: this.nodeOptions.size ? this.nodeOptions.size[0] : attrs.img_width || attrs.width,
- height: this.nodeOptions.size ? this.nodeOptions.size[1] : attrs.img_height || attrs.height,
- });
+ var $img = $(qweb.render("FieldBinaryImage-img", {widget: this, url: url}));
this.$('> img').remove();
this.$el.prepend($img);
$img.on('error', function () {
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index a397e8c148d..d14c1f008fd 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -836,8 +836,8 @@
t-att-src='url'
t-att-border="widget.readonly ? 0 : 1"
t-att-name="widget.name"
- t-att-width="widget.node.attrs.img_width || widget.node.attrs.width"
- t-att-height="widget.node.attrs.img_height || widget.node.attrs.height"/>
+ t-att-width="widget.nodeOptions.size ? widget.nodeOptions.size[0] : widget.attrs.img_width || widget.attrs.width"
+ t-att-height="widget.nodeOptions.size ? widget.nodeOptions.size[1] : widget.attrs.img_height || widget.attrs.height"/>
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index ada39fe3a29..1e3233128d6 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -1424,6 +1424,42 @@ QUnit.module('basic_fields', {
}
});
+ QUnit.module('FieldImage');
+
+ QUnit.test('image fields are correctly rendered', function (assert) {
+ assert.expect(5);
+
+ this.data.partner.records[0].document = 'myimage';
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ mockRPC: function (route) {
+ if (route === 'data:image/png;base64,myimage') {
+ assert.ok(true, "should called the correct route");
+ return $.when('wow');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.ok(form.$('div[name="document"]').hasClass('o_field_image'),
+ "the widget should have the correct class");
+ assert.strictEqual(form.$('div[name="document"] > img').length, 1,
+ "the widget should contain an image");
+ assert.ok(form.$('div[name="document"] > img').hasClass('img-responsive'),
+ "the image should have the correct class");
+ assert.strictEqual(form.$('div[name="document"] > img').attr('width'), "90",
+ "the image should correctly set its attributes");
+ form.destroy();
+
+ });
+
QUnit.module('JournalDashboardGraph');
QUnit.test('graph dashboard widget is rendered correctly', function (assert) {
From 2455350c40df86266eabd614ceab451159d67be3 Mon Sep 17 00:00:00 2001
From: Martin Geubelle
Date: Thu, 19 Oct 2017 18:05:17 +0200
Subject: [PATCH 132/153] [FIX] web: deal with `date` widget on datetime
This use case was not correctly managed as the function evaluating if the
value has changed compares datetime and date.
This triggered an issue if the widget `date` was set on the field `date_order`
on a purchase order for example ; it was not possible to create a record as
the datapoint was set `dirty`.
This rev. ensures that a `field_changed` is not triggered if the day is the same
on a date widget.
This rev. also adds support of `datetime` fields on date widget, which appears
to be a valid use case.
Fixes https://github.com/odoo/odoo/issues/20311
---
.../web/static/src/js/fields/basic_fields.js | 16 +++++++--
.../static/tests/fields/basic_fields_tests.js | 33 +++++++++++++++++++
2 files changed, 46 insertions(+), 3 deletions(-)
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 21205bfc033..7420c1bceff 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -352,7 +352,7 @@ var FieldChar = InputField.extend(TranslatableFieldMixin, {
var FieldDate = InputField.extend({
className: "o_field_date",
tagName: "span",
- supportedFieldTypes: ['date'],
+ supportedFieldTypes: ['date', 'datetime'],
/**
* @override
@@ -411,12 +411,13 @@ var FieldDate = InputField.extend({
* @override
* @private
* @param {Moment|false} value
+ * @returns {boolean}
*/
_isSameValue: function (value) {
if (value === false) {
return this.value === false;
}
- return value.isSame(this.value);
+ return value.isSame(this.value, 'day');
},
/**
* Instantiates a new DateWidget datepicker.
@@ -455,7 +456,16 @@ var FieldDateTime = FieldDate.extend({
var value = this.datewidget.getValue();
return value && value.add(-this.getSession().getTZOffset(value), 'minutes');
},
-
+ /**
+ * @override
+ * @private
+ */
+ _isSameValue: function (value) {
+ if (value === false) {
+ return this.value === false;
+ }
+ return value.isSame(this.value);
+ },
/**
* Instantiates a new DateTimeWidget datepicker rather than DateWidget.
*
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 1e3233128d6..f05d7abf08e 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -1823,6 +1823,39 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('do not trigger a field_changed for datetime field with date widget', function (assert) {
+ assert.expect(3);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'',
+ translateParameters: { // Avoid issues due to localization formats
+ date_format: '%m/%d/%Y',
+ time_format: '%H:%M:%S',
+ },
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ mockRPC: function (route, args) {
+ assert.step(args.method);
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('.o_datepicker_input').val(), '02/08/2017',
+ 'the date should be correct');
+
+ form.$('input[name="datetime"]').val('02/08/2017').trigger('input').trigger('change');
+ form.$buttons.find('.o_form_button_save').click();
+
+ assert.verifySteps(['read']); // should not have save as nothing changed
+
+ form.destroy();
+ });
+
QUnit.module('FieldDatetime');
QUnit.test('datetime field in form view', function (assert) {
From 3be3c6f532fafff30c243d849e04e99547b4cad5 Mon Sep 17 00:00:00 2001
From: Fabien Meghazi
Date: Tue, 28 Feb 2017 12:20:36 +0100
Subject: [PATCH 133/153] [FIX] qweb2.js: fix erratic object type detection in
foreach (Fixes #15655)
Could not add a test as the qweb js testsuite is broken and it's xml
test files have been modified in order to test qweb.py specific cases.
---
addons/web/static/lib/qweb/qweb2.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js
index b1bf280f516..25c3ef94d1b 100644
--- a/addons/web/static/lib/qweb/qweb2.js
+++ b/addons/web/static/lib/qweb/qweb2.js
@@ -147,14 +147,15 @@ var QWeb2 = {
foreach: function(context, enu, as, old_dict, callback) {
if (enu != null) {
var index, jlen, cur;
- var size, new_dict = this.extend({}, old_dict);
+ var new_dict = this.extend({}, old_dict);
new_dict[as + "_all"] = enu;
var as_value = as + "_value",
as_index = as + "_index",
as_first = as + "_first",
as_last = as + "_last",
as_parity = as + "_parity";
- if (size = enu.length) {
+ if (enu instanceof Array) {
+ var size = enu.length;
new_dict[as + "_size"] = size;
for (index = 0, jlen = enu.length; index < jlen; index++) {
cur = enu[index];
From 78dcbdee6e3842dcfd9488fcd926fe2bd9ca6017 Mon Sep 17 00:00:00 2001
From: Olivier Dony
Date: Fri, 20 Oct 2017 14:22:41 +0200
Subject: [PATCH 134/153] [FIX] mail: correct typo in
e5e549a4cf12e62fc72c846dc32828182fae37e6
---
addons/mail/models/mail_channel.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py
index 6012dc9c22b..cd57fc38bf8 100644
--- a/addons/mail/models/mail_channel.py
+++ b/addons/mail/models/mail_channel.py
@@ -251,7 +251,7 @@ class Channel(models.Model):
author = self.env['res.partner'].browse(message_dict.get('author_id', False))
if not author or author not in self.channel_partner_ids:
return {
- 'error_mesage': _('restricted to channel members'),
+ 'error_message': _('restricted to channel members'),
}
return True
return super(Channel, self)._alias_check_contact(message, message_dict, alias)
From c6a9bab074492beffeddcb320cfdc042f021a285 Mon Sep 17 00:00:00 2001
From: Nicolas Martinelli
Date: Thu, 19 Oct 2017 23:53:48 +0200
Subject: [PATCH 135/153] [FIX] account: tax included
- Create a tax included of 21 %
- Create a product sold 7.00, set the 21 % tax
- Add the product in a SO
Amount w/o tax: 5.79
Amount tax: 1.22
Total: 7.01
We defer the base rounding to the end of the method so that the rounding
doesn't impact the tax computation of included taxes.
Complement of 6e46ba7b460b5f155735c91f957a4dca3dec8f96
opw-777221
---
addons/account/models/account.py | 6 +++---
addons/account/tests/test_tax.py | 21 +++++++++++++++++++++
2 files changed, 24 insertions(+), 3 deletions(-)
diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 4630ece0eeb..02ff42a09f3 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -758,7 +758,7 @@ class AccountTax(models.Model):
# (145 - 15) / (1.0 + ((10 + 20) / 100.0)) = 130 / 1.3 = 100
if fixed_amount == 0.0 and percent_amount == 0.0:
return base_amount
- return round((base_amount - fixed_amount) / (1.0 + percent_amount / 100.0), prec)
+ return (base_amount - fixed_amount) / (1.0 + percent_amount / 100.0)
base = round(price_unit * quantity, prec)
@@ -822,7 +822,7 @@ class AccountTax(models.Model):
'id': tax.id,
'name': tax.with_context(**{'lang': partner.lang} if partner else {}).name,
'amount': sign * tax_amount,
- 'base': sign * tax_base,
+ 'base': round(sign * tax_base, prec),
'sequence': tax.sequence,
'account_id': tax.account_id.id,
'refund_account_id': tax.refund_account_id.id,
@@ -833,7 +833,7 @@ class AccountTax(models.Model):
'taxes': taxes_vals,
'total_excluded': sign * (currency.round(total_excluded) if round_total else total_excluded),
'total_included': sign * (currency.round(total_included) if round_total else total_included),
- 'base': sign * base,
+ 'base': round(sign * base, prec),
}
@api.v7
diff --git a/addons/account/tests/test_tax.py b/addons/account/tests/test_tax.py
index 5b6361bf69c..df12bb1dd3a 100644
--- a/addons/account/tests/test_tax.py
+++ b/addons/account/tests/test_tax.py
@@ -28,6 +28,12 @@ class TestTax(AccountTestUsers):
'amount': 10,
'sequence': 3,
})
+ self.percent_tax_bis = self.tax_model.create({
+ 'name': "Percent tax bis",
+ 'amount_type': 'percent',
+ 'amount': 21,
+ 'sequence': 3,
+ })
self.division_tax = self.tax_model.create({
'name': "Division tax",
'amount_type': 'division',
@@ -172,6 +178,21 @@ class TestTax(AccountTestUsers):
],
res_percent
)
+ self.percent_tax_bis.price_include = True
+ self.percent_tax_bis.include_base_amount = True
+ res_percent = self.percent_tax_bis.compute_all(7.0)
+ self._check_compute_all_results(
+ 7.0, # 'base'
+ 7.0, # 'total_included'
+ 5.79, # 'total_excluded'
+ [
+ # base , amount | seq | amount | incl | incl_base
+ # ---------------------------------------------------
+ (5.79, 1.21), # | 3 | 21% | t | t
+ # ---------------------------------------------------
+ ],
+ res_percent
+ )
def test_tax_sequence_normalized_set(self):
self.division_tax.sequence = 1
From 823cfd6ab1b30c0f86acd18e1de98c9bc988594d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?G=C3=A9ry=20Debongnie?=
Date: Fri, 20 Oct 2017 10:53:46 +0200
Subject: [PATCH 136/153] [FIX] web: make sure m2m_checkbox works twice in a
row
We had an interesting bug with the field many2many_checkbox: in some
cases, it could cause a crash, because it could not find an element in the
localdata.
To reproduce this, start with a non empty field many2many checkbox, then
uncheck them twice.
The reason is that the datapoint of type 'list' that represents the
current value is actually modified, and the replace_with operation
assumed that it could find the localID by looking into its data. I have
the feeling that a more complete fix is to make sure that the datapoint
is not modified (in its 'data' key), but is modified in its '_changes'
key. However, this would probably be a bigger change.
---
.../static/src/js/views/basic/basic_model.js | 3 +++
.../tests/fields/relational_fields_tests.js | 26 +++++++++++++++++++
2 files changed, 29 insertions(+)
diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js
index 3591ac3cf94..b4a0b999453 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1541,6 +1541,9 @@ var BasicModel = AbstractModel.extend({
removedDef = this._applyX2ManyChange(record, fieldName, {
operation: 'DELETE',
ids: _.map(removedIds, function (resID) {
+ if (resID in list._cache) {
+ return list._cache[resID];
+ }
return _.findWhere(listData, {res_id: resID}).id;
}),
});
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index b116f10de86..f8652b13341 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -7837,6 +7837,32 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('widget many2many_checkboxes: start non empty, then remove twice', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].timmy = [12,14];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'',
+ res_id: 1,
+ viewOptions: {mode: 'edit'},
+ });
+
+ form.$('div.o_field_widget div.o_checkbox input').eq(0).click();
+ form.$('div.o_field_widget div.o_checkbox input').eq(1).click();
+ form.$buttons.find('.o_form_button_save').click();
+ assert.notOk(form.$('div.o_field_widget div.o_checkbox input').eq(0).prop('checked'),
+ "first checkbox should not be checked");
+ assert.notOk(form.$('div.o_field_widget div.o_checkbox input').eq(1).prop('checked'),
+ "second checkbox should not be checked");
+
+ form.destroy();
+ });
+
QUnit.module('FieldMany2ManyBinaryMultiFiles');
QUnit.test('widget many2many_binary', function (assert) {
From 31c1be1fac29cb0298827e1d686d956b0339df24 Mon Sep 17 00:00:00 2001
From: rde
Date: Tue, 17 Oct 2017 11:27:20 +0200
Subject: [PATCH 137/153] [IMP] auth_oauth: prevent portal users to land on
/web after login
Before this commit, a portal user would land on /web after login in with oauth.
He would then just see the "Website" app.
He should then click on it to land on website instead of landing directly on it
after login in, which is not convenient, especially since theses users doesn't
know Odoo (in case of sale customers manually created for instance).
Now, if users has no 'base.group_user' right, it will be redirected to the
website directly instead of /web.
---
addons/auth_oauth/controllers/main.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py
index b7c1f13cf6b..3c3ddf84559 100644
--- a/addons/auth_oauth/controllers/main.py
+++ b/addons/auth_oauth/controllers/main.py
@@ -151,7 +151,11 @@ class OAuthController(http.Controller):
url = '/web#action=%s' % action
elif menu:
url = '/web#menu_id=%s' % menu
- return login_and_redirect(*credentials, redirect_url=url)
+ resp = login_and_redirect(*credentials, redirect_url=url)
+ #Since /web is hardcoded, verify user has right to land on it
+ if urlparse.urlparse(resp.location).path == '/web' and not request.registry['res.users'].has_group(request.cr, request.uid, 'base.group_user'):
+ resp.location = '/'
+ return resp
except AttributeError:
# auth_signup is not installed
_logger.error("auth_signup not installed on database %s: oauth sign up cancelled." % (dbname,))
From bdaa3b88e471c3c0ec686e7ef4f2fe2cca595da2 Mon Sep 17 00:00:00 2001
From: rde
Date: Fri, 20 Oct 2017 10:31:32 +0200
Subject: [PATCH 138/153] [FIX] website: settings-add enterprise label to
website_version
Enterprise label was missing on website_version
---
addons/website/views/res_config_views.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/website/views/res_config_views.xml b/addons/website/views/res_config_views.xml
index d21eb063a8b..55454059f37 100644
--- a/addons/website/views/res_config_views.xml
+++ b/addons/website/views/res_config_views.xml
@@ -131,7 +131,7 @@
-
+
From dd2a619ad55191e58fb7d7112009624910142390 Mon Sep 17 00:00:00 2001
From: rde
Date: Thu, 19 Oct 2017 18:18:07 +0200
Subject: [PATCH 139/153] [FIX] base, web, website: settings - prevent
duplicate enterprise labels
Before this commit:
Multiple widgets for the same field on the same view (V11.0 settings) would not
retrieve the correct label but all of them.
Now:
We now retrieve the correct label instead of all of them
---
addons/web/static/src/js/views/form/form_renderer.js | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/addons/web/static/src/js/views/form/form_renderer.js b/addons/web/static/src/js/views/form/form_renderer.js
index e5a0646dd5c..9fa4f570b65 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -811,7 +811,15 @@ var FormRenderer = BasicRenderer.extend({
// Attach the tooltips on the fields' label
_.each(this.allFieldWidgets[this.state.id], function (widget) {
var idForLabel = self.idsForLabels[widget.name];
+ // We usually don't support multiple widgets for the same field on the
+ // same view but it is the case with the new settings view on V11.0.
+ // Therefore, we need to retrieve the correct label since it could be
+ // displayed multiple times on the view, otherwise, for example the
+ // enterprise label will be displayed as many times as the field
+ // exists on settings.
+ var $widgets = self.$('.o_field_widget[name=' + widget.name + ']');
var $label = idForLabel ? self.$('label[for=' + idForLabel + ']') : $();
+ $label = $label.eq($widgets.index(widget.$el));
if (config.debug || widget.attrs.help || widget.field.help) {
self._addFieldTooltip(widget, $label);
}
From 0589ee8949e215e2b354a6abab9def783f8edc3e Mon Sep 17 00:00:00 2001
From: rde
Date: Fri, 20 Oct 2017 10:37:35 +0200
Subject: [PATCH 140/153] [FIX] base: prevent enterprise label to lose style on
search
Before this commit, when you were entering search mode en settings view,
enterprise label would lose its style. (even when leaving search mode by
removing search text)
Now, the style remains.
---
odoo/addons/base/static/src/js/res_config_settings.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/odoo/addons/base/static/src/js/res_config_settings.js b/odoo/addons/base/static/src/js/res_config_settings.js
index 47b4bf3b8c7..9af3e9c782b 100644
--- a/odoo/addons/base/static/src/js/res_config_settings.js
+++ b/odoo/addons/base/static/src/js/res_config_settings.js
@@ -294,7 +294,7 @@ var BaseSettingRenderer = FormRenderer.extend({
var settingBox = $(this).closest('.o_setting_box');
if (!settingBox.hasClass('o_invisible_modifier')) {
settingBox.removeClass('o_hidden');
- $(this).html(self._wordHighlighter($(this).text(), self.searchText));
+ $(this).html(self._wordHighlighter($(this).html(), self.searchText));
} else {
self.inVisibleCount++;
}
From 1a09bcc6221e1856ece4d176205f2aaa1a205a3a Mon Sep 17 00:00:00 2001
From: amoyaux
Date: Tue, 17 Oct 2017 14:46:56 +0200
Subject: [PATCH 141/153] [FIX] mrp: Handle by product tracked by SN/lot
Since mrp/stock refactoring by-product tracked
was not handled by the code. The best way to
handle it would be to add a section in the
product produce wizard. However we can't modify
the model in 11.0.
Thus this commit generate a move line for each by-product
but without lot/SN.
! This commit should be revert in future version once
the by product pass through the produce wizard
---
addons/mrp_byproduct/models/mrp_production.py | 38 +++++++++++++++++++
1 file changed, 38 insertions(+)
diff --git a/addons/mrp_byproduct/models/mrp_production.py b/addons/mrp_byproduct/models/mrp_production.py
index 91917da03cc..05584d0d019 100644
--- a/addons/mrp_byproduct/models/mrp_production.py
+++ b/addons/mrp_byproduct/models/mrp_production.py
@@ -3,6 +3,8 @@
from odoo import api, models
+from odoo.tools import float_round
+
class MrpProduction(models.Model):
_description = 'Production'
@@ -42,3 +44,39 @@ class MrpProduction(models.Model):
for sub_product in production.bom_id.sub_products:
production._create_byproduct_move(sub_product)
return res
+
+
+class MrpProductProduce(models.TransientModel):
+ _name = "mrp.product.produce"
+ _description = "Record Production"
+ _inherit = "mrp.product.produce"
+
+ @api.multi
+ def check_finished_move_lots(self):
+ """ Handle by product tracked """
+ by_product_moves = self.production_id.move_finished_ids.filtered(lambda m: m.product_id != self.product_id and m.product_id.tracking != 'none' and m.state not in ('done', 'cancel'))
+ for by_product_move in by_product_moves:
+ rounding = by_product_move.product_uom.rounding
+ quantity = float_round(self.product_qty * by_product_move.unit_factor, precision_rounding=rounding)
+ values = {
+ 'move_id': by_product_move.id,
+ 'product_id': by_product_move.product_id.id,
+ 'production_id': self.production_id.id,
+ 'product_uom_id': by_product_move.product_uom.id,
+ 'location_id': by_product_move.location_id.id,
+ 'location_dest_id': by_product_move.location_dest_id.id,
+ }
+ if by_product_move.product_id.tracking == 'lot':
+ values.update({
+ 'product_uom_qty': quantity,
+ 'qty_done': quantity,
+ })
+ self.env['stock.move.line'].create(values)
+ else:
+ values.update({
+ 'product_uom_qty': 1.0,
+ 'qty_done': 1.0,
+ })
+ for i in range(0, int(quantity)):
+ self.env['stock.move.line'].create(values)
+ return super(MrpProductProduce, self).check_finished_move_lots()
From 41d91e2c96e40e26a95998766148ba22f6ca4431 Mon Sep 17 00:00:00 2001
From: amoyaux
Date: Fri, 20 Oct 2017 11:21:18 +0200
Subject: [PATCH 142/153] [FIX] mrp: update MO qty with by_product fail
Update MO qty compute the quantity already processed
but it includes the by_product moves.
This commit add a filter in order to only consider the
move with the final product.
---
addons/mrp/wizard/change_production_qty.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py
index 526a12d8ffc..bf447feee61 100644
--- a/addons/mrp/wizard/change_production_qty.py
+++ b/addons/mrp/wizard/change_production_qty.py
@@ -39,7 +39,7 @@ class ChangeProductionQty(models.TransientModel):
def change_prod_qty(self):
for wizard in self:
production = wizard.mo_id
- produced = sum(production.move_finished_ids.mapped('quantity_done'))
+ produced = sum(production.move_finished_ids.filtered(lambda m: m.product_id == production.product_id).mapped('quantity_done'))
if wizard.product_qty < produced:
raise UserError(_("You have already processed %d. Please input a quantity higher than %d ")%(produced, produced))
production.write({'product_qty': wizard.product_qty})
From f6c89de81eb105eb916d5a5cdca1f44bddd65c36 Mon Sep 17 00:00:00 2001
From: amoyaux
Date: Thu, 12 Oct 2017 16:26:32 +0200
Subject: [PATCH 143/153] [FIX] mrp: finished move lines view does not show lot
The stock_move_line view for finished product on MO was
the classic one which shows lot_name and other useless stuff
This commit improve it by using the lot_id instead and a new
view that only contains relevant information for the end user.
---
addons/mrp/models/mrp_production.py | 2 +-
addons/mrp/views/mrp_production_views.xml | 2 +-
addons/mrp/views/stock_move_views.xml | 22 ++++++++++++++++++++++
3 files changed, 24 insertions(+), 2 deletions(-)
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 59bafc27487..4a56ef217bc 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -107,7 +107,7 @@ class MrpProduction(models.Model):
copy=False, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
domain=[('scrapped', '=', False)])
finished_move_line_ids = fields.One2many(
- 'stock.move.line', compute='_compute_lines', inverse='_inverse_lines',
+ 'stock.move.line', compute='_compute_lines', inverse='_inverse_lines', string="Finished Product"
)
workorder_ids = fields.One2many(
'mrp.workorder', 'production_id', 'Work Orders',
diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml
index 2188e912eab..e8a59d049ce 100644
--- a/addons/mrp/views/mrp_production_views.xml
+++ b/addons/mrp/views/mrp_production_views.xml
@@ -131,7 +131,7 @@
-
+
diff --git a/addons/mrp/views/stock_move_views.xml b/addons/mrp/views/stock_move_views.xml
index f1007b8d26b..a6c2a07417a 100644
--- a/addons/mrp/views/stock_move_views.xml
+++ b/addons/mrp/views/stock_move_views.xml
@@ -63,6 +63,28 @@
+
+ mrp.finished.move.line.form
+ stock.move.line
+
+
+
+
+