diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-11-28 17:44:46 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-11-28 17:44:46 +0100 |
| commit | efe7ca16b77edce73bf538ccb70678a52afdfd1d (patch) | |
| tree | ad5af92d3bb397fd58fd861e6ce8e3abd2902ec6 /addons | |
| parent | b09f624f20781d65baf2060df40383898e6f86e5 (diff) | |
| parent | bb6f6c57f97b091f3f68161039de980eb9ce739d (diff) | |
[MERGE] forward port branch 11.0 up to bb6f6c57f97
Diffstat (limited to 'addons')
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): |
