diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py
index 922d55b0f68..aea14012767 100644
--- a/addons/mail/models/mail_mail.py
+++ b/addons/mail/models/mail_mail.py
@@ -119,6 +119,8 @@ class MailMail(models.Model):
ids = filtered_ids
else:
ids = list(set(filtered_ids) & set(ids))
+ ids.sort()
+
res = None
try:
# auto-commit except in testing mode
@@ -292,7 +294,6 @@ class MailMail(models.Model):
values['partner_id'] = partner
email_list.append(values)
-
# headers
headers = {}
ICP = self.env['ir.config_parameter'].sudo()
@@ -316,6 +317,22 @@ class MailMail(models.Model):
'state': 'exception',
'failure_reason': _('Error without exception. Probably due do sending an email without computed recipients.'),
})
+ # Update notification in a transient exception state to avoid concurrent
+ # update in case an email bounces while sending all emails related to current
+ # mail record.
+ notifs = self.env['mail.notification'].search([
+ ('is_email', '=', True),
+ ('mail_id', 'in', mail.ids),
+ ('email_status', 'not in', ('sent', 'canceled'))
+ ])
+ if notifs:
+ notif_msg = _('Error without exception. Probably due do concurrent access update of notification records. Please see with an administrator.')
+ notifs.write({
+ 'email_status': 'exception',
+ 'failure_type': 'UNKNOWN',
+ 'failure_reason': notif_msg,
+ })
+
# build an RFC2822 email.message.Message object and send it without queuing
res = None
for email in email_list:
diff --git a/addons/mail/models/mail_tracking_value.py b/addons/mail/models/mail_tracking_value.py
index 874ce4fde9a..4edeb10fa65 100644
--- a/addons/mail/models/mail_tracking_value.py
+++ b/addons/mail/models/mail_tracking_value.py
@@ -63,8 +63,8 @@ class MailTracking(models.Model):
values.update({
'old_value_integer': initial_value and initial_value.id or 0,
'new_value_integer': new_value and new_value.id or 0,
- 'old_value_char': initial_value and initial_value.name_get()[0][1] or '',
- 'new_value_char': new_value and new_value.name_get()[0][1] or ''
+ 'old_value_char': initial_value and initial_value.sudo().name_get()[0][1] or '',
+ 'new_value_char': new_value and new_value.sudo().name_get()[0][1] or ''
})
else:
tracked = False
diff --git a/addons/maintenance/models/maintenance.py b/addons/maintenance/models/maintenance.py
index 719c912e038..fe1335c1d31 100644
--- a/addons/maintenance/models/maintenance.py
+++ b/addons/maintenance/models/maintenance.py
@@ -349,8 +349,8 @@ class MaintenanceRequest(models.Model):
res = super(MaintenanceRequest, self).write(vals)
if vals.get('owner_user_id') or vals.get('user_id'):
self._add_followers()
- if self.stage_id.done and 'stage_id' in vals:
- self.write({'close_date': fields.Date.today()})
+ if 'stage_id' in vals:
+ self.filtered(lambda m: m.stage_id.done).write({'close_date': fields.Date.today()})
self.activity_feedback(['maintenance.mail_act_maintenance_request'])
if vals.get('user_id') or vals.get('schedule_date'):
self.activity_update()
diff --git a/addons/mass_mailing/static/src/scss/themes/theme_default.scss b/addons/mass_mailing/static/src/scss/themes/theme_default.scss
index 96d6a61e805..2fcc5bf2915 100644
--- a/addons/mass_mailing/static/src/scss/themes/theme_default.scss
+++ b/addons/mass_mailing/static/src/scss/themes/theme_default.scss
@@ -87,7 +87,6 @@ td {
}
.btn.btn-primary {
- -webkit-user-select: none;
color: $o-mm-def-btn-text;
border: none;
text-decoration: none;
diff --git a/addons/mrp/models/mrp_workorder.py b/addons/mrp/models/mrp_workorder.py
index b12d0eb6e0a..d7658edb70e 100644
--- a/addons/mrp/models/mrp_workorder.py
+++ b/addons/mrp/models/mrp_workorder.py
@@ -310,8 +310,10 @@ class MrpWorkorder(models.Model):
if self.product_id.tracking != 'none':
qty_to_add = float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)
move._generate_consumed_move_line(qty_to_add, self.final_lot_id)
- else:
+ elif len(move._get_move_lines()) < 2:
move.quantity_done += float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding)
+ else:
+ move._set_quantity_done(move.quantity_done + float_round(self.qty_producing * move.unit_factor, precision_rounding=rounding))
# Transfer quantities from temporary to final move lots or make them final
for move_line in self.active_move_line_ids:
diff --git a/addons/mrp/report/mrp_report_bom_structure.py b/addons/mrp/report/mrp_report_bom_structure.py
index 7a02ccbf205..0c4b5866e6a 100644
--- a/addons/mrp/report/mrp_report_bom_structure.py
+++ b/addons/mrp/report/mrp_report_bom_structure.py
@@ -55,7 +55,7 @@ class ReportBomStructure(models.AbstractModel):
@api.model
def get_operations(self, bom_id=False, qty=0, level=0):
bom = self.env['mrp.bom'].browse(bom_id)
- lines = self._get_operation_line(bom.routing_id, qty, level)
+ lines = self._get_operation_line(bom.routing_id, float_round(qty / bom.product_qty, precision_rounding=1, rounding_method='UP'), level)
values = {
'bom_id': bom_id,
'currency': self.env.user.company_id.currency_id,
@@ -109,7 +109,7 @@ class ReportBomStructure(models.AbstractModel):
else:
product = bom.product_tmpl_id
attachments = self.env['mrp.document'].search([('res_model', '=', 'product.template'), ('res_id', '=', product.id)])
- operations = self._get_operation_line(bom.routing_id, (bom_quantity / bom.product_qty), 0)
+ operations = self._get_operation_line(bom.routing_id, float_round(bom_quantity / bom.product_qty, precision_rounding=1, rounding_method='UP'), 0)
lines = {
'bom': bom,
'bom_qty': bom_quantity,
@@ -139,7 +139,7 @@ class ReportBomStructure(models.AbstractModel):
continue
price = line.product_id.uom_id._compute_price(line.product_id.standard_price, line.product_uom_id) * line_quantity
if line.child_bom_id:
- factor = line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id) * line.child_bom_id.product_qty
+ factor = float_round(line.product_uom_id._compute_quantity(line_quantity, line.child_bom_id.product_uom_id) / line.child_bom_id.product_qty, precision_rounding=1, rounding_method='UP')
sub_total = self._get_price(line.child_bom_id, factor)
else:
sub_total = price
@@ -167,7 +167,8 @@ class ReportBomStructure(models.AbstractModel):
operations = []
total = 0.0
for operation in routing.operation_ids:
- duration_expected = qty * operation.time_cycle + operation.workcenter_id.time_stop + operation.workcenter_id.time_start
+ operation_cycle = float_round(qty / operation.workcenter_id.capacity, precision_rounding=1, rounding_method='UP')
+ duration_expected = operation_cycle * operation.time_cycle + operation.workcenter_id.time_stop + operation.workcenter_id.time_start
total = ((duration_expected / 60.0) * operation.workcenter_id.costs_hour)
operations.append({
'level': level or 0,
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 1812d2f9cfa..914284459c5 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -1509,6 +1509,7 @@ exports.Orderline = Backbone.Model.extend({
// when we add an new orderline we want to merge it with the last line to see reduce the number of items
// in the orderline. This returns true if it makes sense to merge the two
can_be_merged_with: function(orderline){
+ var price = parseFloat(round_di(this.price || 0, this.pos.dp['Product Price']).toFixed(this.pos.dp['Product Price']));
if( this.get_product().id !== orderline.get_product().id){ //only orderline of the same product can be merged
return false;
}else if(!this.get_unit() || !this.get_unit().is_pos_groupable){
@@ -1517,7 +1518,7 @@ exports.Orderline = Backbone.Model.extend({
return false;
}else if(this.get_discount() > 0){ // we don't merge discounted orderlines
return false;
- }else if(this.price !== orderline.get_product().get_price(orderline.order.pricelist, this.get_quantity())){
+ }else if(price !== orderline.get_product().get_price(orderline.order.pricelist, this.get_quantity())){
return false;
}else if(this.product.tracking == 'lot') {
return false;
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 4e61ec32ce8..435c0cbe62c 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -34,6 +34,7 @@ var core = require('web.core');
var rpc = require('web.rpc');
var utils = require('web.utils');
var field_utils = require('web.field_utils');
+var BarcodeEvents = require('barcodes.BarcodeEvents').BarcodeEvents;
var QWeb = core.qweb;
var _t = core._t;
@@ -1689,6 +1690,13 @@ var PaymentScreenWidget = ScreenWidget.extend({
// also called explicitly to handle some keydown events that
// do not generate keypress events.
this.keyboard_handler = function(event){
+ // On mobile Chrome BarcodeEvents relies on an invisible
+ // input being filled by a barcode device. Let events go
+ // through when this input is focused.
+ if (BarcodeEvents.$barcodeInput && BarcodeEvents.$barcodeInput.is(":focus")) {
+ return;
+ }
+
var key = '';
if (event.type === "keypress") {
diff --git a/addons/product_margin/models/product_product.py b/addons/product_margin/models/product_product.py
index 46adfc5118b..9fc94ea6310 100644
--- a/addons/product_margin/models/product_product.py
+++ b/addons/product_margin/models/product_product.py
@@ -107,7 +107,7 @@ class ProductProduct(models.Model):
select
sum(l.price_unit * l.quantity)/nullif(sum(l.quantity),0) as avg_unit_price,
sum(l.quantity) as num_qty,
- sum(l.quantity * (l.price_subtotal/(nullif(l.quantity,0)))) as total,
+ sum(l.quantity * (l.price_subtotal_signed/(nullif(l.quantity,0)))) as total,
sum(l.quantity * pt.list_price) as sale_expected
from account_invoice_line l
left join account_invoice i on (l.invoice_id = i.id)
diff --git a/addons/purchase_mrp/models/purchase_mrp.py b/addons/purchase_mrp/models/purchase_mrp.py
index 086b9684e08..c554d64e152 100644
--- a/addons/purchase_mrp/models/purchase_mrp.py
+++ b/addons/purchase_mrp/models/purchase_mrp.py
@@ -27,7 +27,8 @@ class PurchaseOrderLine(models.Model):
# In the case of a kit, we need to check if all components are shipped. Since the BOM might
# have changed, we don't compute the quantities but verify the move state.
if bom:
- bom_delivered = all([move.state == 'done' for move in self.move_ids])
+ moves = self.move_ids.filtered(lambda m: m.picking_id and m.picking_id.state != 'cancel')
+ bom_delivered = all([move.state == 'done' for move in moves])
if bom_delivered:
return self.product_qty
else:
diff --git a/addons/sale/static/src/js/sale.js b/addons/sale/static/src/js/sale.js
index 289d26ecae3..1121a38501a 100644
--- a/addons/sale/static/src/js/sale.js
+++ b/addons/sale/static/src/js/sale.js
@@ -21,8 +21,8 @@ KanbanRecord.include({
ev.preventDefault();
this.$target_input = $('');
- this.$('.o_kanban_primary_bottom').html(this.$target_input);
- this.$('.o_kanban_primary_bottom').prepend(_t("Set an invoicing target: "));
+ this.$('.o_kanban_primary_bottom:last').html(this.$target_input);
+ this.$('.o_kanban_primary_bottom:last').prepend(_t("Set an invoicing target: "));
this.$target_input.focus();
var self = this;
diff --git a/addons/sale/views/sale_product_configurator_templates.xml b/addons/sale/views/sale_product_configurator_templates.xml
index 5dcb37fc8e1..5cd7317b09b 100644
--- a/addons/sale/views/sale_product_configurator_templates.xml
+++ b/addons/sale/views/sale_product_configurator_templates.xml
@@ -131,14 +131,12 @@
-
+
-
-
-
+
@@ -268,4 +266,4 @@
-
\ No newline at end of file
+
diff --git a/addons/sale_stock/models/sale_order.py b/addons/sale_stock/models/sale_order.py
index 8db9adc9916..6ee721f6f45 100644
--- a/addons/sale_stock/models/sale_order.py
+++ b/addons/sale_stock/models/sale_order.py
@@ -187,7 +187,7 @@ class SaleOrderLine(models.Model):
qty = 0.0
for move in line.move_ids.filtered(lambda r: r.state == 'done' and not r.scrapped):
if move.location_dest_id.usage == "customer":
- if not move.origin_returned_move_id:
+ if not move.origin_returned_move_id or (move.origin_returned_move_id and move.to_refund):
qty += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
elif move.location_dest_id.usage != "customer" and move.to_refund:
qty -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
diff --git a/addons/sale_timesheet/views/hr_timesheet_templates.xml b/addons/sale_timesheet/views/hr_timesheet_templates.xml
index ff7b2e71163..49a35ce4b7d 100644
--- a/addons/sale_timesheet/views/hr_timesheet_templates.xml
+++ b/addons/sale_timesheet/views/hr_timesheet_templates.xml
@@ -188,7 +188,7 @@
-
+
diff --git a/addons/stock/models/stock_inventory.py b/addons/stock/models/stock_inventory.py
index 1e66ccffe20..c63288d8b60 100644
--- a/addons/stock/models/stock_inventory.py
+++ b/addons/stock/models/stock_inventory.py
@@ -233,7 +233,7 @@ class Inventory(models.Model):
def _get_inventory_lines_values(self):
# TDE CLEANME: is sql really necessary ? I don't think so
locations = self.env['stock.location'].search([('id', 'child_of', [self.location_id.id])])
- domain = ' location_id in %s AND quantity != 0'
+ domain = ' location_id in %s AND quantity != 0 AND active = TRUE'
args = (tuple(locations.ids),)
vals = []
@@ -274,6 +274,8 @@ class Inventory(models.Model):
self.env.cr.execute("""SELECT product_id, sum(quantity) as product_qty, location_id, lot_id as prod_lot_id, package_id, owner_id as partner_id
FROM stock_quant
+ LEFT JOIN product_product
+ ON product_product.id = stock_quant.product_id
WHERE %s
GROUP BY product_id, location_id, lot_id, package_id, partner_id """ % domain, args)
diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py
index ccd2aea5a85..5f15e641ad7 100644
--- a/addons/stock/models/stock_move_line.py
+++ b/addons/stock/models/stock_move_line.py
@@ -182,6 +182,8 @@ class StockMoveLine(models.Model):
for ml in mls:
if ml.state == 'done':
+ if 'qty_done' in vals:
+ ml.move_id.product_uom_qty = ml.move_id.quantity_done
if ml.product_id.type == 'product':
Quant = self.env['stock.quant']
quantity = ml.product_uom_id._compute_quantity(ml.qty_done, ml.move_id.product_id.uom_id,rounding_method='HALF-UP')
@@ -507,6 +509,8 @@ class StockMoveLine(models.Model):
candidate.product_uom_qty = 0.0
else:
candidate.unlink()
+ if float_is_zero(quantity, precision_rounding=rounding):
+ break
else:
# split this move line and assign the new part to our extra move
quantity_split = float_round(
@@ -514,8 +518,6 @@ class StockMoveLine(models.Model):
precision_rounding=self.product_uom_id.rounding,
rounding_method='UP')
candidate.product_uom_qty = self.product_id.uom_id._compute_quantity(quantity_split, candidate.product_uom_id, rounding_method='HALF-UP')
- quantity -= quantity_split
move_to_recompute_state |= candidate.move_id
- if quantity == 0.0:
break
move_to_recompute_state._recompute_state()
diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py
index dcc379d9725..2037ec4f34e 100644
--- a/addons/stock/tests/test_move.py
+++ b/addons/stock/tests/test_move.py
@@ -2008,6 +2008,48 @@ class StockMove(TransactionCase):
self.assertAlmostEqual(move_pack_cust.reserved_availability, 1.0)
self.assertEqual(move_pack_cust.state, 'partially_available')
+ def test_use_reserved_move_line_1(self):
+ """ Test that _free_reservation work when quantity is only available on
+ reserved move lines.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product1, self.stock_location, 10.0)
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ move2 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 5.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+ move2._action_confirm()
+ move2._action_assign()
+ move3 = self.env['stock.move'].create({
+ 'name': 'test_use_unreserved_move_line_1_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product1.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 0.0,
+ 'quantity_done': 1.0,
+ })
+ move3._action_confirm()
+ move3._action_assign()
+ move3._action_done()
+ self.assertEqual(move3.state, 'done')
+ quant = self.env['stock.quant']._gather(self.product1, self.stock_location)
+ self.assertEqual(quant.quantity, 9.0)
+ self.assertEqual(quant.reserved_quantity, 9.0)
+
def test_use_unreserved_move_line_1(self):
""" Test that validating a stock move linked to an untracked product reserved by another one
correctly unreserves the other one.
diff --git a/addons/stock/views/stock_picking_views.xml b/addons/stock/views/stock_picking_views.xml
index c506c5f9722..f98e9507650 100644
--- a/addons/stock/views/stock_picking_views.xml
+++ b/addons/stock/views/stock_picking_views.xml
@@ -332,6 +332,7 @@
+
diff --git a/addons/stock/wizard/product_replenish.py b/addons/stock/wizard/product_replenish.py
index 509375599d5..ced229de126 100644
--- a/addons/stock/wizard/product_replenish.py
+++ b/addons/stock/wizard/product_replenish.py
@@ -4,6 +4,7 @@
import datetime
from odoo import api, fields, models
from odoo.exceptions import UserError
+from odoo.tools.misc import clean_context
class ProductReplenish(models.TransientModel):
@@ -51,7 +52,7 @@ class ProductReplenish(models.TransientModel):
uom_reference = self.product_id.uom_id
self.quantity = self.product_uom_id._compute_quantity(self.quantity, uom_reference)
try:
- self.env['procurement.group'].run(
+ self.env['procurement.group'].with_context(clean_context(self.env.context)).run(
self.product_id,
self.quantity,
uom_reference,
diff --git a/addons/stock_dropshipping/models/sale.py b/addons/stock_dropshipping/models/sale.py
index 81f975cbce6..b527701a049 100644
--- a/addons/stock_dropshipping/models/sale.py
+++ b/addons/stock_dropshipping/models/sale.py
@@ -11,9 +11,11 @@ class SaleOrderLine(models.Model):
@api.multi
def _get_qty_procurement(self):
- if not self.move_ids.filtered(lambda r: r.state != 'cancel') and self.purchase_line_ids.filtered(lambda r: r.state != 'cancel'):
+ # People without purchase rights should be able to do this operation
+ purchase_lines_sudo = self.sudo().purchase_line_ids
+ if not self.move_ids.filtered(lambda r: r.state != 'cancel') and purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
qty = 0.0
- for po_line in self.purchase_line_ids.filtered(lambda r: r.state != 'cancel'):
+ for po_line in purchase_lines_sudo.filtered(lambda r: r.state != 'cancel'):
qty += po_line.product_uom._compute_quantity(po_line.product_qty, self.product_uom, rounding_method='HALF-UP')
return qty
else:
diff --git a/addons/test_mail/tests/__init__.py b/addons/test_mail/tests/__init__.py
index 017a983b515..aeddfcd93dd 100644
--- a/addons/test_mail/tests/__init__.py
+++ b/addons/test_mail/tests/__init__.py
@@ -4,6 +4,7 @@ from . import test_mail_activity
from . import test_mail_followers
from . import test_mail_message
from . import test_mail_mail
+from . import test_mail_race
from . import test_mail_resend
from . import test_mail_channel
from . import test_mail_gateway
diff --git a/addons/test_mail/tests/test_mail_race.py b/addons/test_mail/tests/test_mail_race.py
new file mode 100644
index 00000000000..943a1b30922
--- /dev/null
+++ b/addons/test_mail/tests/test_mail_race.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from contextlib import closing
+import psycopg2
+from odoo import api
+from odoo.addons.test_mail.tests import common as mail_common
+from odoo.tests import common
+from odoo.tools import mute_logger
+
+class TestMailRace(common.TransactionCase, mail_common.MockEmails):
+
+ @mute_logger('odoo.addons.mail.models.mail_mail')
+ def test_mail_bounce_during_send(self):
+ self.partner = self.env['res.partner'].create({
+ 'name': 'Ernest Partner',
+ })
+ # we need to simulate a mail sent by the cron task, first create mail, message and notification by hand
+ mail = self.env['mail.mail'].create({
+ 'body_html': '
Test
',
+ 'notification': True,
+ 'state': 'outgoing',
+ 'recipient_ids': [(4, self.partner.id)]
+ })
+ message = self.env['mail.message'].create({
+ 'subject': 'S',
+ 'body': 'B',
+ 'subtype_id': self.ref('mail.mt_comment'),
+ 'needaction_partner_ids': [(6, 0, [self.partner.id])],
+ })
+ notif = self.env['mail.notification'].search([('res_partner_id', '=', self.partner.id)])
+ notif.write({
+ 'mail_id': mail.id,
+ 'is_email': True,
+ 'is_read': True,
+ 'email_status': 'ready',
+ })
+ # we need to commit transaction or cr will keep the lock on notif
+ self.cr.commit()
+
+ # patch send_email in order to create a concurent update and check the notif is already locked by _send()
+ this = self # coding in javascript ruinned my life
+ bounce_deferred = []
+ @api.model
+ def send_email(self, message, *args, **kwargs):
+ with this.registry.cursor() as cr, mute_logger('odoo.sql_db'):
+ try:
+ # try ro aquire lock (no wait) on notification (should fail)
+ cr.execute("SELECT email_status FROM mail_message_res_partner_needaction_rel WHERE id = %s FOR UPDATE NOWAIT", [notif.id])
+ except psycopg2.OperationalError:
+ # record already locked by send, all good
+ bounce_deferred.append(True)
+ else:
+ # this should trigger psycopg2.extensions.TransactionRollbackError in send().
+ # Only here to simulate the initial use case
+ # If the record is lock, this line would create a deadlock since we are in the same thread
+ # In practice, the update will wait the end of the send() transaction and set the notif as bounce, as expeced
+ cr.execute("UPDATE mail_message_res_partner_needaction_rel SET email_status='bounce' WHERE id = %s", [notif.id])
+ return message['Message-Id']
+ self.env['ir.mail_server']._patch_method('send_email', send_email)
+
+ mail.send()
+
+ self.assertTrue(bounce_deferred, "The bounce should have been deferred")
+ self.assertEqual(notif.email_status, 'sent')
+
+ # some cleaning since we commited the cr
+ notif.unlink()
+ message.unlink()
+ mail.unlink()
+ self.partner.unlink()
+ self.env.cr.commit()
diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py
index ef99b2cb3b3..9a2d82ac053 100644
--- a/addons/test_mail/tests/test_performance.py
+++ b/addons/test_mail/tests/test_performance.py
@@ -205,7 +205,7 @@ class TestAdvMailPerformance(TransactionCase):
self.user_test.write({'notification_type': 'email'})
record = self.env['mail.test.track'].create({'name': 'Test'})
- with self.assertQueryCount(__system__=56, emp=75): # com runbot: 56 - 75 // test_mail only: 56 - 75
+ with self.assertQueryCount(__system__=58, emp=77): # com runbot: 56 - 75 // test_mail only: 56 - 75
record.write({
'user_id': self.user_test.id,
})
@@ -259,7 +259,7 @@ class TestAdvMailPerformance(TransactionCase):
def test_message_post_one_email_notification(self):
record = self.env['mail.test.simple'].create({'name': 'Test'})
- with self.assertQueryCount(__system__=48, emp=68): # com runbot: 45 - 65 // test_mail only: 48 - 68
+ with self.assertQueryCount(__system__=50, emp=70): # com runbot: 45 - 65 // test_mail only: 48 - 68
record.message_post(
body='
')
@@ -395,7 +395,7 @@ class TestHeavyMailPerformance(TransactionCase):
self.umbrella.message_subscribe(self.user_portal.partner_id.ids)
record = self.umbrella.sudo(self.env.user)
- with self.assertQueryCount(__system__=80, emp=103): # com runbot 90 - 113 // test_mail only: 97 - 120
+ with self.assertQueryCount(__system__=84, emp=107): # com runbot 90 - 113 // test_mail only: 97 - 120
record.message_post(
body='
Test Post Performances
',
message_type='comment',
@@ -412,7 +412,7 @@ class TestHeavyMailPerformance(TransactionCase):
record = self.umbrella.sudo(self.env.user)
template_id = self.env.ref('test_mail.mail_test_tpl').id
- with self.assertQueryCount(__system__=99, emp=134): # com runbot 109 - 144 // test_mail only: 116 - 151
+ with self.assertQueryCount(__system__=103, emp=138): # com runbot 109 - 144 // test_mail only: 116 - 151
record.message_post_with_template(template_id, message_type='comment', composition_mode='comment')
self.assertEqual(record.message_ids[0].body, '
Adding stuff on %s
' % record.name)
@@ -482,7 +482,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id)
- with self.assertQueryCount(__system__=58, emp=77): # com runbot: 65 - 83 // test_mail only: 66 - 84
+ with self.assertQueryCount(__system__=60, emp=79): # com runbot: 65 - 83 // test_mail only: 66 - 84
rec.write({'user_id': self.user_portal.id})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
@@ -505,7 +505,7 @@ class TestHeavyMailPerformance(TransactionCase):
customer_id = self.customer.id
user_id = self.user_portal.id
- with self.assertQueryCount(__system__=156, emp=190): # com runbot: 156 - 190 // test_mail only: 156 - 190
+ with self.assertQueryCount(__system__=162, emp=198): # com runbot: 156 - 190 // test_mail only: 156 - 190
rec = self.env['mail.test.full'].create({
'name': 'Test',
'umbrella_id': umbrella_id,
@@ -534,7 +534,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
- with self.assertQueryCount(__system__=94, emp=111): # com runbot: 104 - 123 // test_mail only: 105 - 124
+ with self.assertQueryCount(__system__=99, emp=119): # com runbot: 104 - 123 // test_mail only: 105 - 124
rec.write({
'name': 'Test2',
'umbrella_id': self.umbrella.id,
@@ -572,7 +572,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.user_portal.partner_id | self.env.user.partner_id)
- with self.assertQueryCount(__system__=100, emp=122): # test_mail only: 111 - 134
+ with self.assertQueryCount(__system__=104, emp=126): # test_mail only: 111 - 134
rec.write({
'name': 'Test2',
'umbrella_id': umbrella_id,
@@ -606,7 +606,7 @@ class TestHeavyMailPerformance(TransactionCase):
})
self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id)
- with self.assertQueryCount(__system__=54, emp=76): # test_mail only: 54 - 75
+ with self.assertQueryCount(__system__=55, emp=77): # test_mail only: 54 - 75
rec.write({
'name': 'Test2',
'customer_id': customer_id,
diff --git a/addons/test_mass_mailing/tests/test_performance.py b/addons/test_mass_mailing/tests/test_performance.py
index b406474e224..9f47f3aabba 100644
--- a/addons/test_mass_mailing/tests/test_performance.py
+++ b/addons/test_mass_mailing/tests/test_performance.py
@@ -30,7 +30,7 @@ class TestMassMailPerformance(common.MassMailingCase):
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
- with self.assertQueryCount(__system__=2382, marketing=3038):
+ with self.assertQueryCount(__system__=2432, marketing=3088):
mailing.send_mail()
self.assertEqual(mailing.sent, 50)
@@ -67,7 +67,7 @@ class TestMassMailBlPerformance(common.MassMailingCase):
'mailing_domain': [('id', 'in', self.mm_recs.ids)],
})
- with self.assertQueryCount(__system__=2757, marketing=3509):
+ with self.assertQueryCount(__system__=2807, marketing=3559):
mailing.send_mail()
self.assertEqual(mailing.sent, 50)
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index 00b8874a6b1..b324d6f2efa 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -500,6 +500,10 @@ var FieldDate = InputField.extend({
this._super.apply(this, arguments);
// use the session timezone when formatting dates
this.formatOptions.timezone = true;
+ this.datepickerOptions = _.defaults(
+ this.nodeOptions.datepicker || {},
+ {defaultDate: this.value}
+ );
},
/**
* In edit mode, instantiates a DateWidget datepicker and listen to changes.
@@ -583,13 +587,7 @@ var FieldDate = InputField.extend({
* @private
*/
_makeDatePicker: function () {
- return new datepicker.DateWidget(
- this,
- _.defaults(
- this.nodeOptions.datepicker || {},
- {defaultDate: this.value}
- )
- );
+ return new datepicker.DateWidget(this, this.datepickerOptions);
},
/**
@@ -607,6 +605,17 @@ var FieldDate = InputField.extend({
var FieldDateTime = FieldDate.extend({
supportedFieldTypes: ['datetime'],
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ if (this.value) {
+ var offset = this.getSession().getTZOffset(this.value);
+ var displayedValue = this.value.clone().add(offset, 'minutes');
+ this.datepickerOptions.defaultDate = displayedValue;
+ }
+ },
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -637,10 +646,8 @@ var FieldDateTime = FieldDate.extend({
* @private
*/
_makeDatePicker: function () {
- var value = this.value && this.value.clone().add(this.getSession().getTZOffset(this.value), 'minutes');
- return new datepicker.DateTimeWidget(this, {defaultDate: value});
+ return new datepicker.DateTimeWidget(this, this.datepickerOptions);
},
-
/**
* Set the datepicker to the right value rather than the default one.
*
@@ -1171,6 +1178,9 @@ var HandleWidget = AbstractField.extend({
var FieldEmail = InputField.extend({
className: 'o_field_email',
+ events: _.extend({}, InputField.prototype.events, {
+ 'click': '_onClick',
+ }),
prefix: 'mailto',
supportedFieldTypes: ['char'],
@@ -1211,7 +1221,21 @@ var FieldEmail = InputField.extend({
this.$el.text(this.value)
.addClass('o_form_uri o_text_overflow')
.attr('href', this.prefix + ':' + this.value);
- }
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Prevent the URL click from opening the record (when used on a list).
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClick: function (ev) {
+ ev.stopPropagation();
+ },
});
var FieldPhone = FieldEmail.extend({
@@ -1238,6 +1262,9 @@ var FieldPhone = FieldEmail.extend({
var UrlWidget = InputField.extend({
className: 'o_field_url',
+ events: _.extend({}, InputField.prototype.events, {
+ 'click': '_onClick',
+ }),
supportedFieldTypes: ['char'],
/**
@@ -1279,7 +1306,21 @@ var UrlWidget = InputField.extend({
.addClass('o_form_uri o_text_overflow')
.attr('target', '_blank')
.attr('href', this.value);
- }
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Prevent the URL click from opening the record (when used on a list).
+ *
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onClick: function (ev) {
+ ev.stopPropagation();
+ },
});
var CopyClipboard = {
@@ -1645,19 +1686,23 @@ var FieldPdfViewer = FieldBinaryFile.extend({
},
/**
* @private
+ * @param {string} [fileURI] file URI if specified
* @returns {string} the pdf viewer URI
*/
- _getURI: function () {
- var queryObj = {
- model: this.model,
- field: this.name,
- id: this.res_id,
- };
+ _getURI: function (fileURI) {
var page = this.recordData[this.name + '_page'] || 1;
- var queryString = $.param(queryObj);
- var url = encodeURIComponent('/web/image?' + queryString);
+ if (!fileURI) {
+ var queryObj = {
+ model: this.model,
+ field: this.name,
+ id: this.res_id,
+ };
+ var queryString = $.param(queryObj);
+ fileURI = '/web/image?' + queryString
+ }
+ fileURI = encodeURIComponent(fileURI);
var viewerURL = '/web/static/lib/pdfjs/web/viewer.html?file=';
- return viewerURL + url + '#page=' + page;
+ return viewerURL + fileURI + '#page=' + page;
},
/**
* @private
@@ -1701,14 +1746,16 @@ var FieldPdfViewer = FieldBinaryFile.extend({
*/
on_file_change: function (ev) {
this._super.apply(this, arguments);
+ var files = ev.target.files;
+ if (!files || files.length === 0) {
+ return;
+ }
+ // TOCheck: is there requirement to fallback on FileReader if browser don't support URL
+ var fileURI = URL.createObjectURL(files[0]);
if (this.PDFViewerApplication) {
- var files = ev.target.files;
- if (!files || files.length === 0) {
- return;
- }
- var file = files[0];
- // TOCheck: is there requirement to fallback on FileReader if browser don't support URL
- this.PDFViewerApplication.open(URL.createObjectURL(file), 0);
+ this.PDFViewerApplication.open(fileURI, 0);
+ } else {
+ this.$('.o_pdfview_iframe').attr('src', this._getURI(fileURI));
}
},
/**
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index e27128a1e3b..bf46c538c23 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -2584,10 +2584,12 @@ var FieldSelectionBadge = FieldSelection.extend({
* a FieldMany2one for its value.
* Its intern representation is similar to the many2one (a datapoint with a
* `name_get` as data).
+ * Note that there is some logic to support char field because of one use in our
+ * codebase, but this use should be removed along with this note.
*/
var FieldReference = FieldMany2One.extend({
specialData: "_fetchSpecialReference",
- supportedFieldTypes: ['char', 'reference'],
+ supportedFieldTypes: ['reference'],
template: 'FieldReference',
events: _.extend({}, FieldMany2One.prototype.events, {
'change select': '_onSelectionChange',
diff --git a/addons/web/static/src/js/services/notification_service.js b/addons/web/static/src/js/services/notification_service.js
index cc87305a6a2..a0d2720604b 100644
--- a/addons/web/static/src/js/services/notification_service.js
+++ b/addons/web/static/src/js/services/notification_service.js
@@ -58,6 +58,8 @@ var NotificationService = AbstractService.extend({
* Note that this method does not wait for the appendTo method to complete.
*
* @param {Object} params
+ * @param {function} [params.Notification] javascript class of a notification
+ * to instantiate by default use 'web.Notification'
* @param {string} params.title notification title
* @param {string} params.message notification main message
* @param {string} params.type 'notification' or 'warning'
@@ -78,7 +80,8 @@ var NotificationService = AbstractService.extend({
this.$el = $('');
this.$el.prependTo('body');
}
- var notification = this.notifications[++id] = new Notification(this, params);
+ var NotificationWidget = params.Notification || Notification;
+ var notification = this.notifications[++id] = new NotificationWidget(this, params);
notification.appendTo(this.$el);
return id;
},
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index 7705ee5972e..f3277d394a3 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -308,3 +308,55 @@ var AbstractView = Class.extend({
return AbstractView;
});
+
+odoo.define('web.viewUtils', function () {
+"use strict";
+
+/**
+ * FIXME: move this module to its own file in master
+ */
+
+var utils = {
+ /**
+ * Returns the value of a group dataPoint, i.e. the value of the groupBy
+ * field for the records in that group.
+ *
+ * @param {Object} group dataPoint of type list, corresponding to a group
+ * @param {string} groupByField the name of the groupBy field
+ * @returns {string | integer | false}
+ */
+ getGroupValue: function (group, groupByField) {
+ var groupedByField = group.fields[groupByField];
+ switch (groupedByField.type) {
+ case 'many2one':
+ return group.res_id || false;
+ case 'selection':
+ var descriptor = _.findWhere(groupedByField.selection, group.value);
+ return descriptor && descriptor[0];
+ default:
+ return group.value;
+ }
+ },
+ /**
+ * States whether or not the quick create feature is available for the given
+ * datapoint, depending on its groupBy field.
+ *
+ * @param {Object} list dataPoint of type list
+ * @returns {Boolean} true iff the kanban quick create feature is available
+ */
+ isQuickCreateEnabled: function (list) {
+ var groupByField = list.groupedBy[0] && list.groupedBy[0].split(':')[0];
+ if (!groupByField) {
+ return false;
+ }
+ var availableTypes = ['char', 'boolean', 'many2one', 'selection'];
+ if (!_.contains(availableTypes, list.fields[groupByField].type)) {
+ return false;
+ }
+ return true;
+ },
+};
+
+return utils;
+
+});
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 0afb65a9a88..de879018ad5 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -1016,27 +1016,51 @@ var BasicModel = AbstractModel.extend({
route: '/web/dataset/resequence',
params: params,
})
- .then(function () {
- var offset = options.offset ? options.offset : 0;
- var old_data = data.data.slice();
- data.data = _.sortBy(data.data, function (d) {
- if (_.contains(resIDs, self.localData[d].res_id)) {
- return _.indexOf(resIDs, self.localData[d].res_id) + offset;
- } else {
- return _.indexOf(old_data, d);
+ .then(function (wasResequenced) {
+ if (!wasResequenced) {
+ // the field on which the resequence was triggered does not
+ // exist, so no resequence happened server-side
+ return $.when();
+ }
+ var field = params.field ? params.field : 'sequence';
+
+ return self._rpc({
+ model: modelName,
+ method: 'read',
+ args: [resIDs, [field]],
+ }).then(function (records) {
+ if (data.data.length) {
+ var dataType = self.localData[data.data[0]].type;
+ if (dataType === 'record') {
+ _.each(data.data, function (dataPoint) {
+ var recordData = self.localData[dataPoint].data;
+ var inRecords = _.findWhere(records, {id: recordData.id});
+ if (inRecords) {
+ recordData[field] = inRecords[field];
+ }
+ });
+ data.data = _.sortBy(data.data, function (d) {
+ return self.localData[d].data[field];
+ });
+ }
+ if (dataType === 'list') {
+ data.data = _.sortBy(data.data, function (d) {
+ return _.indexOf(resIDs, self.localData[d].res_id)
+ });
+ }
}
- });
- data.res_ids = [];
- _.each(data.data, function (d) {
- var dataPoint = self.localData[d];
- if (dataPoint.type === 'record') {
- data.res_ids.push(dataPoint.res_id);
- } else {
- data.res_ids = data.res_ids.concat(dataPoint.res_ids);
- }
- });
- self._updateParentResIDs(data);
- return parentID;
+ data.res_ids = [];
+ _.each(data.data, function (d) {
+ var dataPoint = self.localData[d];
+ if (dataPoint.type === 'record') {
+ data.res_ids.push(dataPoint.res_id);
+ } else {
+ data.res_ids = data.res_ids.concat(dataPoint.res_ids);
+ }
+ });
+ self._updateParentResIDs(data);
+ return parentID;
+ })
});
},
/**
diff --git a/addons/web/static/src/js/views/basic/basic_view.js b/addons/web/static/src/js/views/basic/basic_view.js
index 7bc5e95bfc8..9e1cde2aada 100644
--- a/addons/web/static/src/js/views/basic/basic_view.js
+++ b/addons/web/static/src/js/views/basic/basic_view.js
@@ -225,7 +225,7 @@ var BasicView = AbstractView.extend({
attrs.options = attrs.options ? pyUtils.py_eval(attrs.options) : {};
}
- if (attrs.on_change && !field.onChange) {
+ if (attrs.on_change && attrs.on_change !== "0" && !field.onChange) {
field.onChange = "1";
}
@@ -378,6 +378,10 @@ var BasicView = AbstractView.extend({
if (!(dependency_name in fields)) {
fields[dependency_name] = dependency_dict;
}
+
+ if (fv.fields && !(dependency_name in fv.fields)) {
+ fv.fields[dependency_name] = dependency_dict;
+ }
}
}
return false;
diff --git a/addons/web/static/src/js/views/calendar/calendar_renderer.js b/addons/web/static/src/js/views/calendar/calendar_renderer.js
index e76a487b73b..c2c90ea7e62 100644
--- a/addons/web/static/src/js/views/calendar/calendar_renderer.js
+++ b/addons/web/static/src/js/views/calendar/calendar_renderer.js
@@ -77,9 +77,9 @@ var SidebarFilter = Widget.extend(FieldManagerMixin, {
self.model.get(recordID),
{
mode: 'edit',
- can_create: false,
attrs: {
- 'placeholder': _.str.sprintf(_t("Add %s"), self.title),
+ placeholder: _.str.sprintf(_t("Add %s"), self.title),
+ can_create: false
},
});
});
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 190fbe88ad2..c7a32b72ba9 100644
--- a/addons/web/static/src/js/views/form/form_renderer.js
+++ b/addons/web/static/src/js/views/form/form_renderer.js
@@ -995,10 +995,7 @@ var FormRenderer = BasicRenderer.extend({
* @param {OdooEvent} ev
*/
_onNavigationMove: function (ev) {
- if (ev.data.direction !== "cancel") {
- ev.stopPropagation();
- }
-
+ ev.stopPropagation();
var index;
if (ev.data.direction === "next") {
index = this.allFieldWidgets[this.state.id].indexOf(ev.data.target || ev.target);
diff --git a/addons/web/static/src/js/views/kanban/kanban_column.js b/addons/web/static/src/js/views/kanban/kanban_column.js
index ef75caca8c0..ffb63ad9083 100644
--- a/addons/web/static/src/js/views/kanban/kanban_column.js
+++ b/addons/web/static/src/js/views/kanban/kanban_column.js
@@ -7,6 +7,7 @@ var Dialog = require('web.Dialog');
var KanbanRecord = require('web.KanbanRecord');
var RecordQuickCreate = require('web.kanban_record_quick_create');
var view_dialogs = require('web.view_dialogs');
+var viewUtils = require('web.viewUtils');
var Widget = require('web.Widget');
var KanbanColumnProgressBar = require('web.KanbanColumnProgressBar');
@@ -201,7 +202,7 @@ var KanbanColumn = Widget.extend({
this.trigger_up('close_quick_create'); // close other quick create widgets
this.trigger_up('start_quick_create');
var context = this.data.getContext();
- context['default_' + this.groupedBy] = this.id;
+ context['default_' + this.groupedBy] = viewUtils.getGroupValue(this.data, this.groupedBy);
this.quickCreateWidget = new RecordQuickCreate(this, {
context: context,
formViewRef: this.quickCreateView,
diff --git a/addons/web/static/src/js/views/kanban/kanban_controller.js b/addons/web/static/src/js/views/kanban/kanban_controller.js
index 9599d29db26..e195b33068c 100644
--- a/addons/web/static/src/js/views/kanban/kanban_controller.js
+++ b/addons/web/static/src/js/views/kanban/kanban_controller.js
@@ -12,6 +12,7 @@ var Context = require('web.Context');
var core = require('web.core');
var Domain = require('web.Domain');
var view_dialogs = require('web.view_dialogs');
+var viewUtils = require('web.viewUtils');
var _t = core._t;
var qweb = core.qweb;
@@ -35,12 +36,15 @@ var KanbanController = BasicController.extend({
/**
* @override
* @param {Object} params
+ * @param {boolean} params.quickCreateEnabled set to false to disable the
+ * quick create feature
*/
init: function (parent, model, renderer, params) {
this._super.apply(this, arguments);
this.on_create = params.on_create;
this.hasButtons = params.hasButtons;
+ this.quickCreateEnabled = params.quickCreateEnabled;
},
//--------------------------------------------------------------------------
@@ -268,8 +272,8 @@ var KanbanController = BasicController.extend({
*/
_onButtonNew: function () {
var state = this.model.get(this.handle, {raw: true});
- var hasColumns = state.groupedBy.length > 0 && state.data.length > 0;
- if (hasColumns && this.on_create === 'quick_create') {
+ var quickCreateEnabled = this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state);
+ if (this.on_create === 'quick_create' && quickCreateEnabled) {
// Activate the quick create in the first column
this.renderer.addQuickCreate();
} else if (this.on_create && this.on_create !== 'quick_create') {
@@ -396,10 +400,11 @@ var KanbanController = BasicController.extend({
var columnState = self.model.get(column.db_id, {raw: true});
var context = columnState.getContext();
var state = self.model.get(self.handle, {raw: true});
- context['default_' + state.groupedBy[0]] = columnState.res_id;
+ var groupedBy = state.groupedBy[0];
+ context['default_' + groupedBy] = viewUtils.getGroupValue(columnState, groupedBy);
new view_dialogs.FormViewDialog(self, {
res_model: state.model,
- context: _.extend({default_name: name}, context),
+ context: _.extend({default_name: values.name || values.display_name}, context),
title: _t("Create"),
disable_multiple_selection: true,
on_saved: function (record) {
diff --git a/addons/web/static/src/js/views/kanban/kanban_model.js b/addons/web/static/src/js/views/kanban/kanban_model.js
index 905ea05ac1e..fd21ed4df86 100644
--- a/addons/web/static/src/js/views/kanban/kanban_model.js
+++ b/addons/web/static/src/js/views/kanban/kanban_model.js
@@ -7,6 +7,7 @@ odoo.define('web.KanbanModel', function (require) {
*/
var BasicModel = require('web.BasicModel');
+var viewUtils = require('web.viewUtils');
var KanbanModel = BasicModel.extend({
@@ -105,7 +106,8 @@ var KanbanModel = BasicModel.extend({
var group = this.localData[groupID];
var context = this._getContext(group);
var parent = this.localData[group.parentID];
- context['default_' + parent.groupedBy[0]] = group.res_id;
+ var groupedBy = parent.groupedBy;
+ context['default_' + groupedBy] = viewUtils.getGroupValue(group, groupedBy);
var def;
if (Object.keys(values).length === 1 && 'display_name' in values) {
// only 'display_name is given, perform a 'name_create'
@@ -132,7 +134,7 @@ var KanbanModel = BasicModel.extend({
},
/**
* Add the following (kanban specific) keys when performing a `get`:
- *
+ *
* - tooltipData
* - progressBarValues
* - isGroupedByM2ONoColumn
diff --git a/addons/web/static/src/js/views/kanban/kanban_record.js b/addons/web/static/src/js/views/kanban/kanban_record.js
index 7013464ea87..d9d7ac77fe2 100644
--- a/addons/web/static/src/js/views/kanban/kanban_record.js
+++ b/addons/web/static/src/js/views/kanban/kanban_record.js
@@ -509,10 +509,12 @@ var KanbanRecord = Widget.extend({
ischild = false;
}
var test_event = events && events.click && (events.click.length > 1 || events.click[0].namespace !== 'bs.tooltip');
+ var testLinkWithHref = elem.nodeName.toLowerCase() === 'a' && elem.href;
if (ischild) {
children.push(elem);
- if (test_event) {
- // do not trigger global click if one child has a click event registered
+ if (test_event || testLinkWithHref) {
+ // Do not trigger global click if one child has a click
+ // event registered (or it is a link with href)
trigger = false;
}
}
diff --git a/addons/web/static/src/js/views/kanban/kanban_renderer.js b/addons/web/static/src/js/views/kanban/kanban_renderer.js
index c51ac945de1..9356311d51c 100644
--- a/addons/web/static/src/js/views/kanban/kanban_renderer.js
+++ b/addons/web/static/src/js/views/kanban/kanban_renderer.js
@@ -9,6 +9,7 @@ var ColumnQuickCreate = require('web.kanban_column_quick_create');
var QWeb = require('web.QWeb');
var session = require('web.session');
var utils = require('web.utils');
+var viewUtils = require('web.viewUtils');
var qweb = core.qweb;
@@ -98,6 +99,9 @@ var KanbanRenderer = BasicRenderer.extend({
/**
* @override
+ * @param {Object} params
+ * @param {boolean} params.quickCreateEnabled set to false to disable the
+ * quick create feature
*/
init: function (parent, state, params) {
this._super.apply(this, arguments);
@@ -116,6 +120,7 @@ var KanbanRenderer = BasicRenderer.extend({
if (this.columnOptions.hasProgressBar) {
this.columnOptions.progressBarStates = {};
}
+ this.quickCreateEnabled = params.quickCreateEnabled;
this._setState(state);
},
/**
@@ -453,6 +458,7 @@ var KanbanRenderer = BasicRenderer.extend({
groupedBy: groupByField,
grouped_by_m2o: this.groupedByM2O,
relation: relation,
+ quick_create: this.quickCreateEnabled && viewUtils.isQuickCreateEnabled(state),
});
this.createColumnEnabled = this.groupedByM2O && this.columnOptions.group_creatable;
},
diff --git a/addons/web/static/src/js/views/kanban/kanban_view.js b/addons/web/static/src/js/views/kanban/kanban_view.js
index 791e3664668..523f0ef1c57 100644
--- a/addons/web/static/src/js/views/kanban/kanban_view.js
+++ b/addons/web/static/src/js/views/kanban/kanban_view.js
@@ -63,7 +63,6 @@ var KanbanView = BasicView.extend({
deletable: activeActions.group_delete,
archivable: this.arch.attrs.archivable ? JSON.parse(this.arch.attrs.archivable) : true,
group_creatable: activeActions.group_create && !config.device.isMobile,
- quick_create: params.isQuickCreateEnabled || this._isQuickCreateEnabled(),
quickCreateView: this.arch.attrs.quick_create_view || null,
hasProgressBar: !!progressBar,
};
@@ -72,6 +71,7 @@ var KanbanView = BasicView.extend({
deletable: activeActions.delete,
read_only_mode: params.readOnlyMode,
};
+ this.rendererParams.quickCreateEnabled = this._isQuickCreateEnabled();
var examples = this.arch.attrs.examples;
if (examples) {
this.rendererParams.examples = kanbanExamplesRegistry.get(examples);
@@ -80,6 +80,7 @@ var KanbanView = BasicView.extend({
this.controllerParams.on_create = this.arch.attrs.on_create;
this.controllerParams.readOnlyMode = false;
this.controllerParams.hasButtons = true;
+ this.controllerParams.quickCreateEnabled = this.rendererParams.quickCreateEnabled;
if (config.device.isMobile) {
this.jsLibs.push('/web/static/lib/jquery.touchSwipe/jquery.touchSwipe.js');
@@ -93,13 +94,10 @@ var KanbanView = BasicView.extend({
/**
* @private
* @param {Object} viewInfo
+ * @returns {boolean} true iff the quick create feature is not explicitely
+ * disabled (with create="False" or quick_create="False" in the arch)
*/
_isQuickCreateEnabled: function () {
- var groupBy = this.loadParams.groupBy[0];
- groupBy = groupBy !== undefined ? groupBy.split(':')[0] : undefined;
- if (groupBy !== undefined && !_.contains(['char', 'boolean', 'many2one'], this.fields[groupBy].type)) {
- return false;
- }
if (!this.controllerParams.activeActions.create) {
return false;
}
diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml
index 907a02c0237..1eef5047f46 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -810,7 +810,7 @@
-
+
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index 7db578b5d4e..81c21cf6052 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -10,6 +10,7 @@ var KanbanView = require('web.KanbanView');
var ListView = require('web.ListView');
var session = require('web.session');
var testUtils = require('web.test_utils');
+var field_registry = require('web.field_registry');
var createView = testUtils.createView;
var createAsyncView = testUtils.createAsyncView;
@@ -1907,6 +1908,49 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test("pdf_viewer: upload rendering", function (assert) {
+ assert.expect(6);
+
+ testUtils.patch(field_registry.map.pdf_viewer, {
+ on_file_change: function (ev) {
+ ev.target = {files: [new Blob()]};
+ this._super.apply(this, arguments);
+ },
+ _getURI: function (fileURI) {
+ var res = this._super.apply(this, arguments);
+ assert.step('_getURI');
+ assert.ok(_.str.startsWith(fileURI, 'blob:'));
+ this.PDFViewerApplication = {
+ open: function (URI) {
+ assert.step('open');
+ assert.ok(_.str.startsWith(URI, 'blob:'));
+ },
+ };
+ return 'about:blank';
+ },
+ });
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:
+ '',
+ });
+
+ // first upload initialize iframe
+ form.$('input[type="file"]').trigger('change');
+ assert.verifySteps(['_getURI']);
+ // second upload call pdfjs method inside iframe
+ form.$('input[type="file"]').trigger('change');
+ assert.verifySteps(['_getURI', 'open']);
+
+ testUtils.unpatch(field_registry.map.pdf_viewer);
+ form.destroy();
+ });
+
QUnit.test('text field rendering in list view', function (assert) {
assert.expect(1);
@@ -2106,6 +2150,40 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('image fields in x2many list are loaded correctly', function (assert) {
+ assert.expect(2);
+
+ this.data.partner_type.fields.image = {name: 'image', type: 'binary'};
+ this.data.partner_type.records[0].image = 'product_image';
+ this.data.partner.records[0].timmy = [12];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '',
+ res_id: 1,
+ mockRPC: function (route, args) {
+ if (route === 'data:image/png;base64,product_image') {
+ assert.ok(true, "The list's image should have been fetched");
+ return $.when();
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(form.$('tr.o_data_row').length, 1,
+ 'There should be one record in the many2many');
+
+ form.destroy();
+ });
+
QUnit.test('image fields with required attribute', function (assert) {
assert.expect(2);
@@ -2779,8 +2857,7 @@ QUnit.module('basic_fields', {
form.$buttons.find('.o_form_button_edit').click();
assert.strictEqual(form.$('.o_datepicker_input').val(), expectedDateString,
'the datetime should be correct in edit mode');
- // click on the input and select 22 February at 8:23:33
- form.$('.o_datepicker_input').click();
+ // select 22 February at 8:23:33
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
@@ -2900,8 +2977,7 @@ QUnit.module('basic_fields', {
assert.strictEqual(list.$('input.o_datepicker_input').val(), expectedDateString,
'the date should be correct in edit mode');
- // click on the input and select 22 February at 8:23:33
- list.$('input.o_datepicker_input').click();
+ // select 22 February at 8:23:33
assert.ok($('.bootstrap-datetimepicker-widget').length, 'datepicker should be open');
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Month selection
$('.bootstrap-datetimepicker-widget .picker-switch').first().click(); // Year selection
@@ -3061,6 +3137,32 @@ QUnit.module('basic_fields', {
form.destroy();
});
+ QUnit.test('datepicker option: daysOfWeekDisabled', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.fields.datetime.default = "2017-08-02 12:00:05";
+ this.data.partner.fields.datetime.required = true;
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'',
+ res_id: 1,
+ });
+
+ form.$buttons.find('.o_form_button_create').click();
+ assert.ok($('.day:last-child(),.day:nth-child(2)').hasClass('disabled'),
+ 'first and last days must be disabled');
+ assert.notOk($('.day:not(:last-child()):not(:nth-child(2))').hasClass('disabled'),
+ 'other days must stay clickable');
+
+ form.destroy();
+ });
+
QUnit.module('FieldMonetary');
QUnit.test('monetary field in form view', function (assert) {
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index 99f97fbda54..e1e91863448 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -5580,7 +5580,7 @@ QUnit.module('relational_fields', {
form.destroy();
});
- QUnit.test('pressing enter in a o2m with a required empty m2o', function (assert) {
+ QUnit.test('pressing enter in a o2m with a required empty field', function (assert) {
assert.expect(4);
this.data.turtle.fields.turtle_foo.required = true;
@@ -10259,6 +10259,81 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('click on URL should not open the record', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records[0].turtles = [1];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'',
+ res_id: 1,
+ });
+
+ form.$('.o_email_cell a').click();
+ assert.strictEqual($('.modal .o_form_view').length, 0,
+ 'click should not open the modal');
+
+ form.$('.o_url_cell a').click();
+ assert.strictEqual($('.modal .o_form_view').length, 0,
+ 'click should not open the modal');
+ form.destroy();
+ });
+
+ QUnit.test('create and edit on m2o in o2m, and press ESCAPE', function (assert) {
+ assert.expect(4);
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '
' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
',
+ archs: {
+ 'partner,false,form': '
',
+ },
+ });
+
+ form.$('.o_field_x2many_list_row_add a').click();
+
+ assert.strictEqual(form.$('.o_selected_row').length, 1,
+ "should have create a new row in edition");
+
+ var $input = form.$('.o_field_widget[name="turtle_trululu"] input');
+ $input.click();
+ $input.autocomplete('widget').find('.o_m2o_dropdown_option').focus().click();
+
+ assert.strictEqual($('.modal .o_form_view').length, 1,
+ "should have opened a form view in a dialog");
+
+ $('.modal .o_form_view .o_field_widget[name=display_name]').trigger($.Event('keydown', {
+ which: $.ui.keyCode.ESCAPE,
+ keyCode: $.ui.keyCode.ESCAPE,
+ }));
+
+ assert.strictEqual($('.modal .o_form_view').length, 0,
+ "should have closed the dialog");
+ assert.strictEqual(form.$('.o_selected_row').length, 1,
+ "new row should still be present");
+
+ form.destroy();
+ });
+
QUnit.test('one2many add a line should not crash if orderedResIDs is not set', function (assert) {
// There is no assertion, the code will just crash before the bugfix.
assert.expect(0);
@@ -13679,6 +13754,59 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('when creating a new many2one on a x2many then discarding it immediately with ESCAPE, it should not crash', function (assert) {
+ var done = assert.async();
+ assert.expect(1);
+
+ this.data.partner.records[0].turtles = [];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ viewOptions: {
+ mode: 'edit',
+ },
+ data: this.data,
+ arch:'
',
+ },
+ mockRPC: function (route, args) {
+ if (args.method === 'name_create') {
+ return $.Deferred().reject({
+ code: 200,
+ data: {},
+ message: "Odoo server error",
+ }, $.Event());
+ }
+ if (args.method === 'create') {
+ assert.deepEqual(args.args[0], {state: 'abc'},
+ "should write the correct value for state");
+ assert.deepEqual(args.kwargs.context, {default_state: 'abc', default_name: 'test'},
+ "should send the correct default value for state");
+ }
+ return this._super.apply(this, arguments);
+ },
+ groupBy: ['state'],
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
+ "there should be 1 record in first column");
+
+ kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click();
+ kanban.$('.o_kanban_quick_create input').val('test').trigger('input');
+ kanban.$('.o_kanban_add').click();
+
+ assert.strictEqual($('.modal .o_form_view.o_form_editable').length, 1,
+ "a form view dialog should have been opened (in edit)");
+ assert.strictEqual($('.modal .o_field_widget[name=state]').val(), '"abc"',
+ "the correct default value for state should already be set");
+
+ $('.modal-footer .btn-primary').click();
+
+ assert.strictEqual($('.modal').length, 0, "the modal should be closed");
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
+ "there should be 2 records in first column");
+
+ kanban.destroy();
+ });
+
QUnit.test('quick create record in empty grouped kanban', function (assert) {
assert.expect(3);
@@ -1726,6 +1840,327 @@ QUnit.module('Views', {
kanban.destroy();
});
+ QUnit.test('quick create record in grouped on date(time) field', function (assert) {
+ assert.expect(6);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '' +
+ '' +
+ '
' +
+ '' +
+ '',
+ groupBy: ['date'],
+ intercepts: {
+ switch_view: function (ev) {
+ assert.deepEqual(_.pick(ev.data, 'res_id', 'view_type'), {
+ res_id: undefined,
+ view_type: 'form',
+ }, "should trigger an event to open the form view (twice)");
+ },
+ },
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 0,
+ "quick create should be disabled when grouped on a date field");
+
+ // clicking on CREATE in control panel should not open a quick create
+ kanban.$buttons.find('.o-kanban-button-new').click();
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
+ "should not have opened the quick create widget");
+
+ kanban.reload({groupBy: ['datetime']});
+
+ assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 0,
+ "quick create should be disabled when grouped on a datetime field");
+
+ // clicking on CREATE in control panel should not open a quick create
+ kanban.$buttons.find('.o-kanban-button-new').click();
+ assert.strictEqual(kanban.$('.o_kanban_quick_create').length, 0,
+ "should not have opened the quick create widget");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create record feature is properly enabled/disabled at reload', function (assert) {
+ assert.expect(3);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '' +
+ '' +
+ '
' +
+ '' +
+ '',
+ groupBy: ['foo'],
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 3,
+ "quick create should be enabled when grouped on a char field");
+
+ kanban.reload({groupBy: ['date']});
+
+ assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 0,
+ "quick create should now be disabled (grouped on date field)");
+
+ kanban.reload({groupBy: ['bar']});
+
+ assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 2,
+ "quick create should be enabled again (grouped on boolean field)");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create record in grouped by char field', function (assert) {
+ assert.expect(4);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '' +
+ '' +
+ '
' +
+ '' +
+ '',
+ groupBy: ['foo'],
+ mockRPC: function (route, args) {
+ if (args.method === 'name_create') {
+ assert.deepEqual(args.kwargs.context, {default_foo: 'yop'},
+ "should send the correct default value for foo");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 3,
+ "quick create should be enabled when grouped on a char field");
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
+ "first column should contain 1 record");
+
+ kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click();
+ kanban.$('.o_kanban_quick_create input').val('new record').trigger('input');
+ kanban.$('.o_kanban_add').click();
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
+ "first column should now contain 2 records");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create record in grouped by boolean field', function (assert) {
+ assert.expect(4);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '' +
+ '' +
+ '
' +
+ '' +
+ '',
+ groupBy: ['bar'],
+ mockRPC: function (route, args) {
+ if (args.method === 'name_create') {
+ assert.deepEqual(args.kwargs.context, {default_bar: true},
+ "should send the correct default value for bar");
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 2,
+ "quick create should be enabled when grouped on a boolean field");
+ assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 3,
+ "second column (true) should contain 3 records");
+
+ kanban.$('.o_kanban_header:nth(1) .o_kanban_quick_add i').click();
+ kanban.$('.o_kanban_quick_create input').val('new record').trigger('input');
+ kanban.$('.o_kanban_add').click();
+
+ assert.strictEqual(kanban.$('.o_kanban_group:nth(1) .o_kanban_record').length, 4,
+ "second column (true) should now contain 4 records");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create record in grouped on selection field', function (assert) {
+ assert.expect(4);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '' +
+ '' +
+ '
' +
+ '' +
+ '',
+ mockRPC: function (route, args) {
+ if (args.method === 'name_create') {
+ assert.deepEqual(args.kwargs.context, {default_state: 'abc'},
+ "should send the correct default value for bar");
+ }
+ return this._super.apply(this, arguments);
+ },
+ groupBy: ['state'],
+ });
+
+ assert.strictEqual(kanban.$('.o_kanban_header .o_kanban_quick_add i').length, 3,
+ "quick create should be enabled when grouped on a selection field");
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 1,
+ "first column (abc) should contain 1 record");
+
+ kanban.$('.o_kanban_header:first .o_kanban_quick_add i').click();
+ kanban.$('.o_kanban_quick_create input').val('new record').trigger('input');
+ kanban.$('.o_kanban_add').click();
+
+ assert.strictEqual(kanban.$('.o_kanban_group:first .o_kanban_record').length, 2,
+ "first column (abc) should contain 2 records");
+
+ kanban.destroy();
+ });
+
+ QUnit.test('quick create record in grouped by char field (within quick_create_view)', function (assert) {
+ assert.expect(6);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'partner',
+ data: this.data,
+ arch: '' +
+ '' +
+ '