summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-11-28 17:44:46 +0100
committerChristophe Simonis <chs@odoo.com>2018-11-28 17:44:46 +0100
commitefe7ca16b77edce73bf538ccb70678a52afdfd1d (patch)
treead5af92d3bb397fd58fd861e6ce8e3abd2902ec6 /addons
parentb09f624f20781d65baf2060df40383898e6f86e5 (diff)
parentbb6f6c57f97b091f3f68161039de980eb9ce739d (diff)
[MERGE] forward port branch 11.0 up to bb6f6c57f97
Diffstat (limited to 'addons')
-rw-r--r--addons/account/models/account_invoice.py1
-rw-r--r--addons/account/models/reconciliation_widget.py6
-rw-r--r--addons/account/views/account_invoice_view.xml11
-rw-r--r--addons/hr_attendance/models/hr_employee.py4
-rw-r--r--addons/hr_expense/models/hr_expense.py1
-rw-r--r--addons/hr_expense/tests/test_expenses.py20
-rw-r--r--addons/hr_payroll/models/hr_payslip.py2
-rw-r--r--addons/mail/models/mail_mail.py13
-rw-r--r--addons/mail/models/mail_message.py14
-rw-r--r--addons/mail/models/mail_thread.py2
-rw-r--r--addons/mail/models/res_partner.py2
-rw-r--r--addons/mail/wizard/invite.py30
-rw-r--r--addons/purchase_requisition/models/purchase_requisition.py4
-rw-r--r--addons/resource/models/resource.py5
-rw-r--r--addons/resource/models/resource_mixin.py1
-rw-r--r--addons/sale/static/src/js/sale.js39
-rw-r--r--addons/sale/static/tests/sales_team_dashboard_tests.js56
-rw-r--r--addons/stock_account/models/stock.py3
-rw-r--r--addons/web/static/src/js/fields/basic_fields.js12
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js1
-rw-r--r--addons/web/static/src/js/views/abstract_view.js1
-rw-r--r--addons/web/static/src/js/views/basic/basic_model.js4
-rw-r--r--addons/web/static/tests/fields/basic_fields_tests.js42
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js77
-rw-r--r--addons/web/static/tests/views/view_dialogs_tests.js60
-rw-r--r--addons/website_sale/models/sale_order.py2
26 files changed, 367 insertions, 46 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index afd0fe1019b..1b385897262 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -1471,6 +1471,7 @@ class AccountInvoiceLine(models.Model):
help="Gives the sequence of this line when displaying the invoice.")
invoice_id = fields.Many2one('account.invoice', string='Invoice Reference',
ondelete='cascade', index=True)
+ invoice_type = fields.Selection(related='invoice_id.type', readonly=True)
uom_id = fields.Many2one('uom.uom', string='Unit of Measure',
ondelete='set null', index=True, oldname='uos_id')
product_id = fields.Many2one('product.product', string='Product',
diff --git a/addons/account/models/reconciliation_widget.py b/addons/account/models/reconciliation_widget.py
index af59be29d4f..c30c4232a02 100644
--- a/addons/account/models/reconciliation_widget.py
+++ b/addons/account/models/reconciliation_widget.py
@@ -339,7 +339,11 @@ class AccountReconciliation(models.AbstractModel):
rec_prop = self._get_move_line_reconciliation_proposition(account.id, partner_id)
row['reconciliation_proposition'] = self._prepare_move_lines(rec_prop, target_currency=currency)
row['company_id'] = account.company_id.id
- return rows
+
+ # Return the partners with a reconciliation proposition first, since they are most likely to
+ # be reconciled.
+ return [r for r in rows if r['reconciliation_proposition']] + [r for r in rows if not r['reconciliation_proposition']]
+
@api.model
def process_move_lines(self, data):
diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index e744f2214ac..11fdffcebcc 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -60,8 +60,11 @@
<form string="Invoice Line">
<group>
<group>
+ <field name="partner_id" invisible="1"/>
+ <field name="invoice_type" invisible="1"/>
+ <field name="company_id" invisible="1"/>
<field name="product_id"
- context="parent and {'partner_id': parent.partner_id}"/>
+ context="{'partner_id': partner_id}"/>
<label for="quantity"/>
<div>
<field name="quantity" class="oe_inline"/>
@@ -73,9 +76,9 @@
</group>
<group>
<field name="analytic_tag_ids" groups="analytic.group_analytic_accounting" widget="many2many_tags" options="{'color_field': 'color'}"/>
- <field domain="[('company_id', '=', parent.company_id)]" name="account_id" groups="account.group_account_user"/>
- <field name="invoice_line_tax_ids" context="{'type':parent.type}" domain="[('type_tax_use','!=','none'),('company_id', '=', parent.company_id)]" widget="many2many_tags" options="{'no_create': True}"/>
- <field domain="[('company_id', '=', parent.company_id)]" name="account_analytic_id" groups="analytic.group_analytic_accounting"/>
+ <field domain="[('company_id', '=', company_id)]" name="account_id" groups="account.group_account_user"/>
+ <field name="invoice_line_tax_ids" context="{'type': invoice_type}" domain="[('type_tax_use','!=','none'),('company_id', '=', company_id)]" widget="many2many_tags" options="{'no_create': True}"/>
+ <field domain="[('company_id', '=', company_id)]" name="account_analytic_id" groups="analytic.group_analytic_accounting"/>
<field name="analytic_tag_ids" widget="many2many_tags" groups="analytic.group_analytic_tags"/>
<field name="company_id" groups="base.group_multi_company" readonly="1"/>
</group>
diff --git a/addons/hr_attendance/models/hr_employee.py b/addons/hr_attendance/models/hr_employee.py
index 83e44105c00..388fe60a4c0 100644
--- a/addons/hr_attendance/models/hr_employee.py
+++ b/addons/hr_attendance/models/hr_employee.py
@@ -49,7 +49,9 @@ class HrEmployee(models.Model):
@api.depends('attendance_ids')
def _compute_last_attendance_id(self):
for employee in self:
- employee.last_attendance_id = employee.attendance_ids and employee.attendance_ids[0] or False
+ employee.last_attendance_id = self.env['hr.attendance'].search([
+ ('employee_id', '=', employee.id),
+ ], limit=1)
@api.depends('last_attendance_id.check_in', 'last_attendance_id.check_out', 'last_attendance_id')
def _compute_attendance_state(self):
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index 60fe5487083..92ff7d4b3ab 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -441,6 +441,7 @@ class HrExpense(models.Model):
'employee_id': employee.id,
'product_id': product.id,
'product_uom_id': product.uom_id.id,
+ 'tax_ids': [(4, tax.id, False) for tax in product.supplier_taxes_id],
'quantity': 1,
'unit_amount': price,
'company_id': employee.company_id.id,
diff --git a/addons/hr_expense/tests/test_expenses.py b/addons/hr_expense/tests/test_expenses.py
index 976ba9188ae..b25b968ce7b 100644
--- a/addons/hr_expense/tests/test_expenses.py
+++ b/addons/hr_expense/tests/test_expenses.py
@@ -72,3 +72,23 @@ class TestAccountEntry(TestExpenseCommon):
self.assertEquals(len(self.analytic_account.line_ids), 1, "Analytic Account should have only one line")
self.assertAlmostEquals(self.analytic_account.line_ids[0].amount, -636.36, "Amount on the only AAL is wrong")
self.assertEquals(self.analytic_account.line_ids[0].product_id, self.product_expense, "Product of AAL should be the one from the expense")
+
+ def test_expense_from_email(self):
+ user_demo = self.env.ref('base.user_demo')
+ self.tax.price_include = False
+
+ message_parsed = {
+ 'message_id': 'the-world-is-a-ghetto',
+ 'subject': '[AT] 9876',
+ 'email_from': 'demo@yourcompany.example.com',
+ 'to': 'catchall@yourcompany.com',
+ 'body': "Don't you know, that for me, and for you",
+ 'attachments': [],
+ }
+
+ expense = self.env['hr.expense'].message_new(message_parsed)
+
+ self.assertEquals(expense.product_id, self.product)
+ self.assertEquals(expense.tax_ids.ids, [self.tax.id])
+ self.assertEquals(expense.total_amount, 10863.60)
+ self.assertTrue(expense.employee_id in user_demo.employee_ids)
diff --git a/addons/hr_payroll/models/hr_payslip.py b/addons/hr_payroll/models/hr_payslip.py
index c9d14c47495..93ee4d42393 100644
--- a/addons/hr_payroll/models/hr_payslip.py
+++ b/addons/hr_payroll/models/hr_payslip.py
@@ -194,7 +194,7 @@ class HrPayslip(models.Model):
current_leave_struct['number_of_days'] += leave_time / work_hours
# compute worked days
- work_data = contract.employee_id.get_work_days_data(day_from, day_to, calendar=contract.resource_calendar_id)
+ work_data = contract.employee_id.with_context(no_tz_convert=True).get_work_days_data(day_from, day_to, calendar=contract.resource_calendar_id)
attendances = {
'name': _("Normal Working Days paid at 100%"),
'sequence': 1,
diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py
index 4671c71b1ba..1d53888c9f3 100644
--- a/addons/mail/models/mail_mail.py
+++ b/addons/mail/models/mail_mail.py
@@ -61,7 +61,18 @@ class MailMail(models.Model):
# notification field: if not set, set if mail comes from an existing mail.message
if 'notification' not in values and values.get('mail_message_id'):
values['notification'] = True
- return super(MailMail, self).create(values)
+ new_mail = super(MailMail, self).create(values)
+ if values.get('attachment_ids'):
+ new_mail.attachment_ids.check(mode='read')
+ return new_mail
+
+ @api.multi
+ def write(self, vals):
+ res = super(MailMail, self).write(vals)
+ if vals.get('attachment_ids'):
+ for mail in self:
+ mail.attachment_ids.check(mode='read')
+ return res
@api.multi
def unlink(self):
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index e71c4522d80..97a2b729d83 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -12,7 +12,7 @@ from odoo.exceptions import UserError, AccessError
from odoo.osv import expression
_logger = logging.getLogger(__name__)
-_image_dataurl = re.compile(r'(data:image/[a-z]+?);base64,([a-z0-9+/\n]{3,}=*)\n*([\'"])', re.I)
+_image_dataurl = re.compile(r'(data:image/[a-z]+?);base64,([a-z0-9+/\n]{3,}=*)\n*([\'"])(?: data-filename="([^"]*)")?', re.I)
class Message(models.Model):
@@ -764,12 +764,13 @@ class Message(models.Model):
def base64_to_boundary(match):
key = match.group(2)
if not data_to_url.get(key):
- name = 'image%s' % len(data_to_url)
+ name = match.group(4) if match.group(4) else 'image%s' % len(data_to_url)
attachment = Attachments.create({
'name': name,
'datas': match.group(2),
'datas_fname': name,
- 'res_model': 'mail.message',
+ 'res_model': values.get('model'),
+ 'res_id': values.get('res_id'),
})
attachment.generate_access_token()
values['attachment_ids'].append((4, attachment.id))
@@ -780,6 +781,10 @@ class Message(models.Model):
# delegate creation of tracking after the create as sudo to avoid access rights issues
tracking_values_cmd = values.pop('tracking_value_ids', False)
message = super(Message, self).create(values)
+
+ if values.get('attachment_ids'):
+ message.attachment_ids.check(mode='read')
+
if tracking_values_cmd:
message.sudo().write({'tracking_value_ids': tracking_values_cmd})
@@ -800,6 +805,9 @@ class Message(models.Model):
if 'model' in vals or 'res_id' in vals:
self._invalidate_documents()
res = super(Message, self).write(vals)
+ if vals.get('attachment_ids'):
+ for mail in self:
+ mail.attachment_ids.check(mode='read')
if 'notification_ids' in vals or 'model' in vals or 'res_id' in vals:
self._invalidate_documents()
return res
diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py
index f21de36c28f..25ba71e0e75 100644
--- a/addons/mail/models/mail_thread.py
+++ b/addons/mail/models/mail_thread.py
@@ -191,6 +191,8 @@ class MailThread(models.AbstractModel):
@api.multi
def _get_message_needaction(self):
res = dict((res_id, 0) for res_id in self.ids)
+ if not res:
+ return
# search for unread messages, directly in SQL to improve performances
self._cr.execute(""" SELECT msg.res_id FROM mail_message msg
diff --git a/addons/mail/models/res_partner.py b/addons/mail/models/res_partner.py
index ecf21840aff..ded7f8b7d43 100644
--- a/addons/mail/models/res_partner.py
+++ b/addons/mail/models/res_partner.py
@@ -78,7 +78,7 @@ class Partner(models.Model):
company = user.company_id
if record and hasattr(record, 'company_id'):
- company = record.company_id
+ company = record.company_id.sudo()
return {
'message': message,
diff --git a/addons/mail/wizard/invite.py b/addons/mail/wizard/invite.py
index 579c304b191..c7ae892bd3a 100644
--- a/addons/mail/wizard/invite.py
+++ b/addons/mail/wizard/invite.py
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from lxml import etree
+from lxml.html import builder as html
+
from odoo import _, api, fields, models
@@ -12,18 +15,27 @@ class Invite(models.TransientModel):
@api.model
def default_get(self, fields):
result = super(Invite, self).default_get(fields)
+ if self._context.get('mail_invite_follower_channel_only'):
+ result['send_mail'] = False
+ if 'message' not in fields:
+ return result
+
user_name = self.env.user.name_get()[0][1]
model = result.get('res_model')
res_id = result.get('res_id')
- if self._context.get('mail_invite_follower_channel_only'):
- result['send_mail'] = False
- if 'message' in fields and model and res_id:
- model_name = self.env['ir.model']._get(model).display_name
- document_name = self.env[model].browse(res_id).name_get()[0][1]
- message = _('<div><p>Hello,</p><p>%s invited you to follow %s document: %s.</p></div>') % (user_name, model_name, document_name)
- result['message'] = message
- elif 'message' in fields:
- result['message'] = _('<div><p>Hello,</p><p>%s invited you to follow a new document.</p></div>') % user_name
+ if model and res_id:
+ document = self.env['ir.model']._get(model).display_name
+ title = self.env[model].browse(res_id).display_name
+ msg_fmt = _('%(user_name)s invited you to follow %(document)s document: %(title)s')
+ else:
+ msg_fmt = _('%(user_name)s invited you to follow a new document.')
+
+ text = msg_fmt % locals()
+ message = html.DIV(
+ html.P(_('Hello,')),
+ html.P(text)
+ )
+ result['message'] = etree.tostring(message)
return result
res_model = fields.Char('Related Document Model', required=True, index=True, help='Model of the followed resource')
diff --git a/addons/purchase_requisition/models/purchase_requisition.py b/addons/purchase_requisition/models/purchase_requisition.py
index 06f53a446f4..a0514cc3509 100644
--- a/addons/purchase_requisition/models/purchase_requisition.py
+++ b/addons/purchase_requisition/models/purchase_requisition.py
@@ -363,8 +363,8 @@ class PurchaseOrder(models.Model):
self.order_line = order_lines
@api.multi
- def button_confirm(self):
- res = super(PurchaseOrder, self).button_confirm()
+ def button_approve(self, force=False):
+ res = super(PurchaseOrder, self).button_approve(force=force)
for po in self:
if not po.requisition_id:
continue
diff --git a/addons/resource/models/resource.py b/addons/resource/models/resource.py
index 198e2769c38..833ea6e8af3 100644
--- a/addons/resource/models/resource.py
+++ b/addons/resource/models/resource.py
@@ -437,8 +437,9 @@ class ResourceCalendar(models.Model):
if not end_dt:
end_dt = datetime.datetime.combine(start_dt.date(), datetime.time.max)
- start_dt = to_naive_user_tz(start_dt, self.env.user)
- end_dt = to_naive_user_tz(end_dt, self.env.user)
+ if not self.env.context.get('no_tz_convert', False):
+ start_dt = to_naive_user_tz(start_dt, self.env.user)
+ end_dt = to_naive_user_tz(end_dt, self.env.user)
for day in rrule.rrule(rrule.DAILY,
dtstart=start_dt,
diff --git a/addons/resource/models/resource_mixin.py b/addons/resource/models/resource_mixin.py
index c08bd66d628..9afef9ca40e 100644
--- a/addons/resource/models/resource_mixin.py
+++ b/addons/resource/models/resource_mixin.py
@@ -57,6 +57,7 @@ class ResourceMixin(models.AbstractModel):
days_count = 0.0
total_work_time = timedelta()
calendar = calendar or self.resource_calendar_id
+ calendar = calendar.with_context(no_tz_convert=self.env.context.get('no_tz_convert', False))
for day_intervals in calendar._iter_work_intervals(
from_datetime, to_datetime, self.resource_id.id,
compute_leaves=True, domain=domain):
diff --git a/addons/sale/static/src/js/sale.js b/addons/sale/static/src/js/sale.js
index 374508f721b..267b65c6069 100644
--- a/addons/sale/static/src/js/sale.js
+++ b/addons/sale/static/src/js/sale.js
@@ -19,30 +19,39 @@ KanbanRecord.include({
*/
_onSalesTeamTargetClick: function (ev) {
ev.preventDefault();
+ var self = this;
this.$target_input = $('<input>');
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();
+ this.$target_input.on({
+ blur: this._onSalesTeamTargetSet.bind(this),
+ keydown: function (ev) {
+ if (ev.keyCode === $.ui.keyCode.ENTER) {
+ self._onSalesTeamTargetSet();
+ }
+ },
+ });
+ },
+ /**
+ * Mostly a handler for what happens to the input "this.$target_input"
+ *
+ * @private
+ * @param {JqueryEvent} ev
+ *
+ */
+ _onSalesTeamTargetSet: function () {
var self = this;
- this.$target_input.blur(function() {
- var value = Number(self.$target_input.val());
- if (isNaN(value)) {
- self.do_warn(_t("Wrong value entered!"), _t("Only Integer Value should be valid."));
- } else {
- self._rpc({
- model: 'crm.team',
- method: 'write',
- args: [[self.id], { 'invoiced_target': value }],
- })
- .done(function() {
- self.trigger_up('kanban_record_update', {id: self.id});
- });
+ var value = Number(this.$target_input.val());
+ if (isNaN(value)) {
+ this.do_warn(_t("Wrong value entered!"), _t("Only Integer Value should be valid."));
+ } else {
+ this.trigger_up('kanban_record_update', {invoiced_target: value});
// TODO: previous lines can be refactored as follows (in master)
// self.trigger_up('kanban_record_update', {invoiced_target: value});
- }
- });
+ }
},
});
diff --git a/addons/sale/static/tests/sales_team_dashboard_tests.js b/addons/sale/static/tests/sales_team_dashboard_tests.js
index b1043dd64e1..e73b35a78c0 100644
--- a/addons/sale/static/tests/sales_team_dashboard_tests.js
+++ b/addons/sale/static/tests/sales_team_dashboard_tests.js
@@ -23,7 +23,7 @@ QUnit.module('Sales Team Dashboard', {
});
QUnit.test('edit target with several o_kanban_primary_bottom divs', function (assert) {
- assert.expect(4);
+ assert.expect(6);
var kanban = createView({
View: KanbanView,
@@ -33,6 +33,7 @@ QUnit.test('edit target with several o_kanban_primary_bottom divs', function (as
'<templates>' +
'<t t-name="kanban-box">' +
'<div class="container o_kanban_card_content">' +
+ '<field name="invoiced_target" />' +
'<a href="#" class="sales_team_target_definition o_inline_link">' +
'Click to define a target</a>' +
'<div class="col-12 o_kanban_primary_bottom"/>' +
@@ -46,9 +47,12 @@ QUnit.test('edit target with several o_kanban_primary_bottom divs', function (as
assert.strictEqual(args.args[1].invoiced_target, 123,
"new value is correctly saved");
}
+ if (args.method === 'read') { // Read happens after the write
+ assert.deepEqual(args.args[1], ['invoiced_target', 'display_name'],
+ 'the read (after write) should ask for invoiced_target');
+ }
return this._super.apply(this, arguments);
},
-
});
assert.strictEqual(kanban.$('.o_kanban_view .sales_team_target_definition').length, 1,
@@ -64,6 +68,54 @@ QUnit.test('edit target with several o_kanban_primary_bottom divs', function (as
kanban.$('.o_kanban_primary_bottom:last input').val('123');
kanban.$('.o_kanban_primary_bottom:last input').blur();
+ assert.strictEqual(kanban.$('.o_kanban_record').text(), "123Click to define a target",
+ 'The kanban record should display the updated target value');
+
+ kanban.destroy();
+});
+
+QUnit.test('edit target supports push Enter', function (assert) {
+ assert.expect(3);
+
+ var kanban = createView({
+ View: KanbanView,
+ model: 'crm.team',
+ data: this.data,
+ arch: '<kanban>' +
+ '<templates>' +
+ '<t t-name="kanban-box">' +
+ '<div class="container o_kanban_card_content">' +
+ '<field name="invoiced_target" />' +
+ '<a href="#" class="sales_team_target_definition o_inline_link">' +
+ 'Click to define a target</a>' +
+ '<div class="col-12 o_kanban_primary_bottom"/>' +
+ '<div class="col-12 o_kanban_primary_bottom bottom_block"/>' +
+ '</div>' +
+ '</t>' +
+ '</templates>' +
+ '</kanban>',
+ mockRPC: function (route, args) {
+ if (args.method === 'write') {
+ assert.strictEqual(args.args[1].invoiced_target, 123,
+ "new value is correctly saved");
+ }
+ if (args.method === 'read') { // Read happens after the write
+ assert.deepEqual(args.args[1], ['invoiced_target', 'display_name'],
+ 'the read (after write) should ask for invoiced_target');
+ }
+ return this._super.apply(this, arguments);
+ },
+ });
+
+ kanban.$('a.sales_team_target_definition').click();
+
+ kanban.$('.o_kanban_primary_bottom:last input').focus();
+ kanban.$('.o_kanban_primary_bottom:last input').val('123');
+ kanban.$('.o_kanban_primary_bottom:last input').trigger($.Event('keydown', {which: $.ui.keyCode.ENTER, keyCode: $.ui.keyCode.ENTER}));
+
+ assert.strictEqual(kanban.$('.o_kanban_record').text(), "123Click to define a target",
+ 'The kanban record should display the updated target value');
+
kanban.destroy();
});
diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py
index c42c193999c..409741aa0a5 100644
--- a/addons/stock_account/models/stock.py
+++ b/addons/stock_account/models/stock.py
@@ -268,7 +268,8 @@ class StockMove(models.Model):
# Update the standard price with the price of the last used candidate, if any.
if new_standard_price and move.product_id.cost_method == 'fifo':
- move.product_id.sudo().standard_price = new_standard_price
+ move.product_id.sudo().with_context(force_company=move.company_id.id) \
+ .standard_price = new_standard_price
# If there's still quantity to value but we're out of candidates, we fall in the
# negative stock use case. We chose to value the out move at the price of the
diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js
index c078069b074..b51fa49ed9a 100644
--- a/addons/web/static/src/js/fields/basic_fields.js
+++ b/addons/web/static/src/js/fields/basic_fields.js
@@ -2096,6 +2096,18 @@ var FieldProgressBar = AbstractField.extend({
this.$('.o_progressbar_value').focus().select();
}
},
+ /**
+ * The progress bar has more than one field/value to deal with
+ * i.e. max_value
+ *
+ * @override
+ * @private
+ */
+ _reset: function () {
+ this._super.apply(this, arguments);
+ var new_max_value = this.recordData[this.nodeOptions.max_value];
+ this.max_value = new_max_value !== undefined ? new_max_value : this.max_value;
+ },
isSet: function () {
return true;
},
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 2bb98932c68..b6fbc0cd924 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -468,6 +468,7 @@ var FieldMany2One = AbstractField.extend({
initial_ids: ids ? _.map(ids, function (x) { return x[0]; }) : undefined,
initial_view: view,
disable_multiple_selection: true,
+ no_create: !self.can_create,
on_selected: function (records) {
self.reinitialize(records[0]);
self.activate();
diff --git a/addons/web/static/src/js/views/abstract_view.js b/addons/web/static/src/js/views/abstract_view.js
index 9b6e5a5d192..2dc154d6eae 100644
--- a/addons/web/static/src/js/views/abstract_view.js
+++ b/addons/web/static/src/js/views/abstract_view.js
@@ -141,6 +141,7 @@ var AbstractView = Class.extend({
modelName: params.modelName,
res_id: params.currentId,
res_ids: params.ids,
+ orderedBy: params.context ? params.context.orderedBy : [],
};
if (params.modelName) {
this.loadParams.modelName = params.modelName;
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 d236673b0d5..69fbe44b14b 100644
--- a/addons/web/static/src/js/views/basic/basic_model.js
+++ b/addons/web/static/src/js/views/basic/basic_model.js
@@ -3365,7 +3365,7 @@ var BasicModel = AbstractModel.extend({
model: params.modelName,
offset: params.offset || (type === 'record' ? _.indexOf(res_ids, res_id) : 0),
openGroupByDefault: params.openGroupByDefault,
- orderedBy: params.orderedBy || (params.context && params.context.orderedBy) || [],
+ orderedBy: params.orderedBy || [],
orderedResIDs: params.orderedResIDs,
parentID: params.parentID,
rawContext: params.rawContext,
@@ -4279,7 +4279,7 @@ var BasicModel = AbstractModel.extend({
if (orderData1 > orderData2) {
return order.asc ? 1 : -1;
}
- return compareRecords(record1ID, record2ID, level + 1);
+ return compareRecords(resId1, resId2, level + 1);
};
utils.stableSort(list.res_ids, compareRecords);
}
diff --git a/addons/web/static/tests/fields/basic_fields_tests.js b/addons/web/static/tests/fields/basic_fields_tests.js
index c3cb0c3c404..9e94db14cb7 100644
--- a/addons/web/static/tests/fields/basic_fields_tests.js
+++ b/addons/web/static/tests/fields/basic_fields_tests.js
@@ -4888,6 +4888,48 @@ QUnit.module('basic_fields', {
form.destroy();
});
+
+ QUnit.module('FieldProgressBar');
+
+ QUnit.test('Field ProgressBar: max_value should update', function (assert) {
+ assert.expect(2);
+
+ this.data.partner.records = this.data.partner.records.slice(0,1);
+ this.data.partner.records[0].qux = 2;
+
+ this.data.partner.onchanges = {
+ display_name: function (obj) {
+ obj.int_field = 999;
+ obj.qux = 5;
+ }
+ };
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="display_name" />' +
+ '<field name="qux" invisible="1" />' +
+ '<field name="int_field" widget="progressbar" options="{\'current_value\': \'int_field\', \'max_value\': \'qux\'}" />' +
+ '</form>',
+ res_id: 1,
+ viewOptions: {
+ mode: 'edit',
+ },
+ });
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '10 / 2',
+ 'The initial value of the progress bar should be correct');
+
+ // trigger the onchange
+ form.$('.o_input[name=display_name]').val('new name').trigger('input');
+
+ assert.strictEqual(form.$('.o_progressbar_value').text(), '999 / 5',
+ 'The value of the progress bar should be correct after the update');
+
+ form.destroy();
+ });
});
});
});
diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js
index ca6cd680269..61aaa29a664 100644
--- a/addons/web/static/tests/fields/relational_fields_tests.js
+++ b/addons/web/static/tests/fields/relational_fields_tests.js
@@ -2733,6 +2733,48 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('x2many default_order multiple fields', function (assert) {
+ assert.expect(7);
+
+ this.data.partner.records = [
+ {int_field: 10, id: 1, display_name: "record1"},
+ {int_field: 12, id: 2, display_name: "record2"},
+ {int_field: 11, id: 3, display_name: "record3"},
+ {int_field: 12, id: 4, display_name: "record4"},
+ {int_field: 10, id: 5, display_name: "record5"},
+ {int_field: 10, id: 6, display_name: "record6"},
+ {int_field: 11, id: 7, display_name: "record7"},
+ ];
+
+ this.data.partner.records[0].p = [1, 7, 4, 5, 2, 6, 3];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="p" >' +
+ '<tree default_order="int_field,id">' +
+ '<field name="id"/>' +
+ '<field name="int_field"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ });
+
+ var $recordList = form.$('.o_field_x2many_list .o_data_row');
+ var expectedOrderId = ['1', '5', '6', '3', '7', '2', '4'];
+
+ _.each($recordList, function(record, index) {
+ var $record = $(record);
+ assert.strictEqual($record.find('.o_data_cell').eq(0).text(), expectedOrderId[index],
+ 'The record should be the right place. Index: ' + index);
+ });
+
+ form.destroy();
+ });
+
QUnit.test('many2many list add *many* records, remove, re-add', function (assert) {
assert.expect(5);
@@ -2873,6 +2915,41 @@ QUnit.module('relational_fields', {
form.destroy();
});
+ QUnit.test('one2many from a model that has been sorted', function (assert) {
+ assert.expect(1);
+
+ /* On a standard list view, sort your records by a field
+ * Click on a record which contains a x2m with multiple records in it
+ * The x2m shouldn't take the orderedBy of the parent record (the one on the form)
+ */
+
+ this.data.partner.records[0].turtles = [3, 2];
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch:'<form string="Partners">' +
+ '<field name="turtles">' +
+ '<tree>' +
+ '<field name="turtle_foo"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ res_id: 1,
+ context: {
+ orderedBy: [{
+ name: 'foo',
+ asc: false,
+ }]
+ },
+ });
+
+ assert.strictEqual(form.$('.o_field_one2many[name=turtles] tbody').text().trim(), "kawablip",
+ 'The o2m should not have been sorted.');
+
+ form.destroy();
+ });
+
QUnit.test('O2M List with pager, decoration and default_order: add and cancel adding', function (assert) {
assert.expect(3);
diff --git a/addons/web/static/tests/views/view_dialogs_tests.js b/addons/web/static/tests/views/view_dialogs_tests.js
index 66e5859a1bf..a6cf176468b 100644
--- a/addons/web/static/tests/views/view_dialogs_tests.js
+++ b/addons/web/static/tests/views/view_dialogs_tests.js
@@ -476,6 +476,66 @@ QUnit.module('Views', {
testUtils.unpatch(ListController);
parent.destroy();
});
+
+ QUnit.test('propagate can_create onto the search popup o2m', function (assert) {
+ assert.expect(3);
+
+ this.data.instrument.records = [
+ {id: 1, name: 'Tromblon1'},
+ {id: 2, name: 'Tromblon2'},
+ {id: 3, name: 'Tromblon3'},
+ {id: 4, name: 'Tromblon4'},
+ {id: 5, name: 'Tromblon5'},
+ {id: 6, name: 'Tromblon6'},
+ {id: 7, name: 'Tromblon7'},
+ {id: 8, name: 'Tromblon8'},
+ ];
+
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form>' +
+ '<field name="name"/>' +
+ '<field name="instrument" can_create="false"/>' +
+ '</form>',
+ res_id: 1,
+ archs: {
+ 'instrument,false,list': '<tree>'+
+ '<field name="name"/>'+
+ '</tree>',
+ 'instrument,false,search': '<search>'+
+ '<field name="name"/>'+
+ '</search>',
+ },
+ viewOptions: {
+ mode: 'edit',
+ },
+
+ mockRPC: function(route, args) {
+ if (args.method === 'get_formview_id') {
+ return $.when(false);
+ }
+ return this._super(route, args);
+ },
+ });
+
+ form.$('.o_field_widget[name="instrument"] .o_input').click();
+
+ assert.notOk($('.ui-autocomplete a:contains(Create and Edit)').length,
+ 'Create and edit not present in dropdown');
+
+ $('.ui-autocomplete a:contains(Search More)').trigger('mouseenter').trigger('click');
+
+ var $modal = $('.modal-dialog.modal-lg');
+
+ assert.strictEqual($modal.length, 1, 'Modal present');
+
+ assert.strictEqual($modal.find('.modal-footer button').text(), "Cancel",
+ 'Only the cancel button is present in modal');
+
+ form.destroy();
+ });
});
});
diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py
index f7de11fe485..6fb01886d20 100644
--- a/addons/website_sale/models/sale_order.py
+++ b/addons/website_sale/models/sale_order.py
@@ -54,7 +54,7 @@ class SaleOrder(models.Model):
abandoned_delay = float(self.env['ir.config_parameter'].sudo().get_param('website_sale.cart_abandoned_delay', '1.0'))
abandoned_datetime = fields.Datetime.to_string(datetime.utcnow() - relativedelta(hours=abandoned_delay))
for order in self:
- domain = order.date_order <= abandoned_datetime and order.team_id.team_type == 'website' and order.state == 'draft' and order.partner_id.id != self.env.ref('base.public_partner').id and order.order_line
+ domain = order.date_order and order.date_order <= abandoned_datetime and order.team_id.team_type == 'website' and order.state == 'draft' and order.partner_id.id != self.env.ref('base.public_partner').id and order.order_line
order.is_abandoned_cart = bool(domain)
def _search_abandoned_cart(self, operator, value):