[MERGE] forward port branch saas-12.3 up to 6f55fd65da

This commit is contained in:
Christophe Simonis
2019-08-20 12:17:04 +02:00
91 changed files with 689 additions and 144 deletions
+2 -2
View File
@@ -2173,9 +2173,9 @@ class AccountMoveLine(models.Model):
help="The bank statement used for bank reconciliation")
# ==== Tax fields ====
tax_ids = fields.Many2many('account.tax', string='Taxes')
tax_ids = fields.Many2many('account.tax', string='Taxes', help="Taxes that apply on the base amount")
tax_line_id = fields.Many2one('account.tax', string='Originator tax', ondelete='restrict', store=True,
compute='_compute_tax_line_id')
compute='_compute_tax_line_id', help="Indicates that this journal item is a tax line")
tax_base_amount = fields.Monetary(string="Base Amount", store=True,
currency_field='company_currency_id')
tax_exigible = fields.Boolean(string='Appears in VAT report', default=True,
+4 -1
View File
@@ -454,7 +454,10 @@ class account_payment(models.Model):
move.line_ids.remove_move_reconcile()
move.button_cancel()
move.unlink()
rec.state = 'cancelled'
rec.write({
'state': 'cancelled',
'move_name': False,
})
@api.multi
def unlink(self):
@@ -580,6 +580,8 @@ class AccountReconcileModel(models.Model):
'''
if not self.match_total_amount:
return True
if not candidates:
return False
# Match total residual amount.
total_residual = 0.0
@@ -331,7 +331,7 @@
<field name="module_account_sepa_direct_debit" class="oe_inline" widget="upgrade_boolean"/>
</div>
<div class="o_setting_right_pane" name="sepa_direct_debit_right_pane">
<label string="SEPA Direct Debit (SDD)" for="module_account_sepa"/>
<label string="SEPA Direct Debit (SDD)" for="module_account_sepa_direct_debit"/>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
<div class="text-muted">
Collect customer payments in one-click using Euro SEPA Service
@@ -163,6 +163,7 @@ _map_iban_template = {
'sm': 'SMkk KBBB BBSS SSSC CCCC CCCC CCC', # San Marino
'tn': 'TNkk BBSS SCCC CCCC CCCC CCCC', # Tunisia
'tr': 'TRkk BBBB BRCC CCCC CCCC CCCC CC', # Turkey
'ua': 'UAkk BBBB BBCC CCCC CCCC CCCC CCCC C', # Ukraine
'vg': 'VGkk BBBB CCCC CCCC CCCC CCCC', # Virgin Islands
'xk': 'XKkk BBBB CCCC CCCC CCCC', # Kosovo
}
+3
View File
@@ -446,6 +446,9 @@ class Import(models.TransientModel):
# Or a date/datetime if it matches the pattern
date_patterns = [options['date_format']] if options.get(
'date_format') else []
user_date_format = self.env['res.lang']._lang_get(self.env.user.lang).date_format
if user_date_format:
date_patterns.append(user_date_format)
date_patterns.extend(DATE_PATTERNS)
match = check_patterns(date_patterns, preview_values)
if match:
+15
View File
@@ -14,6 +14,13 @@ except ImportError:
"Install it to support more countries, for example with `easy_install vatnumber`.")
vatnumber = None
# Although stdnum is a dependency of vatnumber, the import of the latter is surrounded by a try/except
# if it is not installed. Therefore, we cannot be sure stdnum is installed in all cases.
try:
import stdnum
except ImportError:
stdnum = None
from odoo import api, models, _
from odoo.tools.misc import ustr
from odoo.exceptions import ValidationError
@@ -63,6 +70,8 @@ _ref_vat = {
'be': 'BE0477472701',
'bg': 'BG1234567892',
'ch': 'CHE-123.456.788 TVA or CH TVA 123456', # Swiss by Yannick Vaucher @ Camptocamp
'cl': 'CL76086428-5',
'co': 'CO213123432-1 or CO213.123.432-1',
'cy': 'CY12345678F',
'cz': 'CZ12345679',
'de': 'DE123456788',
@@ -398,6 +407,12 @@ class ResPartner(models.Model):
except ImportError:
return True
def check_vat_cl(self, vat):
return stdnum.util.get_cc_module('cl', 'vat').is_valid(vat) if stdnum else True
def check_vat_co(self, vat):
return stdnum.util.get_cc_module('co', 'vat').is_valid(vat) if stdnum else True
def default_compact(self, vat):
return vat
+3 -3
View File
@@ -122,8 +122,8 @@
<field name="stop" attrs="{'invisible': True}"/>
<field name="id" attrs="{'invisible': True}"/>
<field name="start_date" string="Starting at" attrs="{'required': [('allday','=',True)], 'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
<field name="stop_date" string="Ending at" attrs="{'required': [('allday','=',True)],'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
<field name="start_date" string="Starting at" attrs="{'required': [('allday','=',True)], 'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}" force_save="1"/>
<field name="stop_date" string="Ending at" attrs="{'required': [('allday','=',True)],'invisible': [('allday','=',False)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}" force_save="1"/>
<field name="start_datetime" string="Starting at" attrs="{'required': [('allday','=',False)], 'invisible': [('allday','=',True)], 'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
<field name="stop_datetime" invisible="1"/>
@@ -132,7 +132,7 @@
<field name="duration" widget="float_time" string="Duration" class="oe_inline" attrs="{'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
<span> hours</span>
</div>
<field name="allday" attrs="{'readonly': [('id', '!=', False), ('recurrency','=',True)]}"/>
<field name="allday" attrs="{'readonly': [('id', '!=', False), ('recurrency','=',True)]}" force_save="1"/>
</group>
<group>
<field name="categ_ids" widget="many2many_tags" options="
+1 -1
View File
@@ -391,7 +391,7 @@
<div t-attf-class="#{kanban_color(record.color.raw_value)} oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown">
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
+1 -1
View File
@@ -318,7 +318,7 @@
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown">
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
@@ -192,7 +192,7 @@
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown" groups="base.group_user">
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<a class="dropdown-toggle o-no-caret btn" role="button" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
+11 -14
View File
@@ -893,24 +893,21 @@ class Escpos:
self._raw(PAPER_FULL_CUT)
def cashdraw(self, pin, tries=5):
def cashdraw(self, pin):
""" Send pulse to kick the cash drawer
With some printers the drawer will not open after one pulse, for this reason we will check
the drawer status up to 'tries' times and send a new pulse if the drawer is still closed. If the
drawer status is open, we will stop sending pulses.
For some reason, with some printers (ex: Epson TM-m30), the cash drawer
only opens 50% of the time if you just send the pulse. But if you read
the status afterwards, it opens all the time.
"""
for i in range(tries):
if pin == 2:
self._raw(CD_KICK_2)
elif pin == 5:
self._raw(CD_KICK_5)
else:
raise CashDrawerError()
if i != tries - 1 and not self.get_printer_status()['printer']['drawer_pin_high']:
break
if pin == 2:
self._raw(CD_KICK_2)
elif pin == 5:
self._raw(CD_KICK_5)
else:
raise CashDrawerError()
self.get_printer_status()
def hw(self, hw):
""" Hardware operations """
+4 -2
View File
@@ -7,7 +7,7 @@ import uuid
import werkzeug.urls
import requests
from odoo import api, fields, models, exceptions
from odoo import api, fields, models, exceptions, _
from odoo.tools import pycompat
_logger = logging.getLogger(__name__)
@@ -68,7 +68,9 @@ def jsonrpc(url, method='call', params=None, timeout=15):
raise e
return response.get('result')
except (ValueError, requests.exceptions.ConnectionError, requests.exceptions.MissingSchema, requests.exceptions.Timeout, requests.exceptions.HTTPError) as e:
raise exceptions.AccessError('The url that this service requested returned an error. Please contact the author of the app. The url it tried to contact was ' + url)
raise exceptions.AccessError(
_('The url that this service requested returned an error. Please contact the author of the app. The url it tried to contact was %s') % url
)
#----------------------------------------------------------
# Helpers for proxy
@@ -828,6 +828,17 @@
<field name="tax_group_id" ref="ae_tax_group_5"/>
<field name="chart_template_id" ref="uae_chart_template_standard"/>
</record>
<record id="uae_purchase_tax_5_child_reverse" model="account.tax.template">
<field name="name">Reverse Charge TVA +5%(Purchase)</field>
<field name="type_tax_use">none</field>
<field name="amount">5</field>
<field name="amount_type">percent</field>
<field name="description">TVA 5%</field>
<field name="account_id" ref="uae_account_3726"/>
<field name="refund_account_id" ref="uae_account_3726"/>
<field name="tax_group_id" ref="ae_tax_group_5"/>
<field name="chart_template_id" ref="uae_chart_template_standard"/>
</record>
<record id="uae_purchase_tax_reverse_charge" model="account.tax.template">
<field name="name">Reverse Charge Provision</field>
<field name="type_tax_use">purchase</field>
+3 -3
View File
@@ -95,7 +95,7 @@ class AccountMove(models.Model):
# <1.2.1.8>
if not seller.l10n_it_tax_system:
raise UserError("The seller's company must have a tax system.")
raise UserError(_("The seller's company must have a tax system."))
# <1.2.2>
if not seller.street and not seller.street2:
@@ -110,7 +110,7 @@ class AccountMove(models.Model):
raise UserError(_("%s must have a country.") % (seller.display_name))
# <1.4.1>
if not buyer.vat and not buyer.l10n_it_codice_fiscale:
if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code != 'IT':
raise UserError(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).") % (buyer.display_name))
# <1.4.2>
@@ -730,7 +730,7 @@ class AccountTax(models.Model):
for tax in self:
if tax.l10n_it_has_exoneration:
if not tax.l10n_it_kind_exoneration or not tax.l10n_it_law_reference or tax.amount != 0:
raise ValidationError("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0.")
raise ValidationError(_("If the tax has exoneration, you must enter a kind of exoneration, a law reference and the amount of the tax must be 0.0."))
if tax.l10n_it_kind_exoneration == 'N6' and tax.l10n_it_vat_due_date == 'S':
raise UserError(_("'Scissione dei pagamenti' is not compatible with exoneration of kind 'N6'"))
+1 -1
View File
@@ -35,7 +35,7 @@ class FetchmailServer(models.Model):
def _check_pec(self):
for record in self:
if record.l10n_it_is_pec and record.type != 'imap':
raise ValidationError("PEC mail server must be of type IMAP.")
raise ValidationError(_("PEC mail server must be of type IMAP."))
@api.multi
def fetch_mail(self):
+5 -5
View File
@@ -1,7 +1,7 @@
# -*- coding:utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
TAX_SYSTEM = [
@@ -94,7 +94,7 @@ class ResCompany(models.Model):
or not record.l10n_it_eco_index_share_capital\
or not record.l10n_it_eco_index_sole_shareholder\
or not record.l10n_it_eco_index_liquidation_state:
raise ValidationError("All fields about the Economic and Administrative Index must be completed.")
raise ValidationError(_("All fields about the Economic and Administrative Index must be completed."))
@api.constrains('l10n_it_has_tax_representative',
'l10n_it_tax_representative_partner_id')
@@ -103,8 +103,8 @@ class ResCompany(models.Model):
if not record.l10n_it_has_tax_representative:
continue
if not record.l10n_it_tax_representative_partner_id:
raise ValidationError("You must select a tax representative.")
raise ValidationError(_("You must select a tax representative."))
if not record.l10n_it_tax_representative_partner_id.vat:
raise ValidationError("Your tax representative partner must have a tax number.")
raise ValidationError(_("Your tax representative partner must have a tax number."))
if not record.l10n_it_tax_representative_partner_id.country_id:
raise ValidationError("Your tax representative partner must have a country.")
raise ValidationError(_("Your tax representative partner must have a country."))
+7 -7
View File
@@ -151,23 +151,23 @@ class Channel(models.Model):
@api.constrains('moderator_ids')
def _check_moderator_email(self):
if any(not moderator.email for channel in self for moderator in channel.moderator_ids):
raise ValidationError("Moderators must have an email address.")
raise ValidationError(_("Moderators must have an email address."))
@api.constrains('moderator_ids', 'channel_partner_ids', 'channel_last_seen_partner_ids')
def _check_moderator_is_member(self):
for channel in self:
if not (channel.mapped('moderator_ids.partner_id') <= channel.sudo().channel_partner_ids):
raise ValidationError("Moderators should be members of the channel they moderate.")
raise ValidationError(_("Moderators should be members of the channel they moderate."))
@api.constrains('moderation', 'email_send')
def _check_moderation_parameters(self):
if any(not channel.email_send and channel.moderation for channel in self):
raise ValidationError('Only mailing lists can be moderated.')
raise ValidationError(_('Only mailing lists can be moderated.'))
@api.constrains('moderator_ids')
def _check_moderator_existence(self):
if any(not channel.moderator_ids for channel in self if channel.moderation):
raise ValidationError('Moderated channels must have moderators.')
raise ValidationError(_('Moderated channels must have moderators.'))
@api.multi
def _compute_is_member(self):
@@ -254,7 +254,7 @@ class Channel(models.Model):
# First checks if user tries to modify moderation fields and has not the right to do it.
if any(key for key in MODERATION_FIELDS if vals.get(key)) and any(self.env.user not in channel.moderator_ids for channel in self if channel.moderation):
if not self.env.user.has_group('base.group_system'):
raise UserError("You do not possess the rights to modify fields related to moderation on one of the channels you are modifying.")
raise UserError(_("You do not have the rights to modify fields related to moderation on one of the channels you are modifying."))
tools.image_resize_images(vals)
result = super(Channel, self).write(vals)
@@ -426,9 +426,9 @@ class Channel(models.Model):
if self.env.user in self.moderator_ids or self.env.user.has_group('base.group_system'):
success = self._send_guidelines(self.channel_partner_ids)
if not success:
raise UserError('View "mail.mail_channel_send_guidelines" was not found. No email has been sent. Please contact an administrator to fix this issue.')
raise UserError(_('View "mail.mail_channel_send_guidelines" was not found. No email has been sent. Please contact an administrator to fix this issue.'))
else:
raise UserError("Only an administrator or a moderator can send guidelines to channel members!")
raise UserError(_("Only an administrator or a moderator can send guidelines to channel members!"))
@api.multi
def _send_guidelines(self, partners):
+2 -2
View File
@@ -937,7 +937,7 @@ class MailThread(models.AbstractModel):
# 1. Check if message is a reply on a thread
msg_references = [ref for ref in tools.mail_header_msgid_re.findall(thread_references) if 'reply_to' not in ref]
mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1)
mail_messages = MailMessage.sudo().search([('message_id', 'in', msg_references)], limit=1, order='id desc, message_id')
is_a_reply = bool(mail_messages)
# 1.1 Handle forward to an alias with a different model: do not consider it as a reply
@@ -1365,7 +1365,7 @@ class MailThread(models.AbstractModel):
# Very unusual situation, be we should be fault-tolerant here
message_id = "<%s@localhost>" % time.time()
_logger.debug('Parsing Message without message-id, generating a random one: %s', message_id)
msg_dict['message_id'] = message_id
msg_dict['message_id'] = message_id.strip()
if message.get('Subject'):
msg_dict['subject'] = tools.decode_smtp_header(message.get('Subject'))
@@ -97,7 +97,6 @@ var DocumentThread = Thread.extend({
fetchMessages: function (options) {
var self = this;
return this._fetchMessages(options).then(function () {
self.call('mail_service', 'markMessagesAsRead', self._messageIDs);
return self._messages;
});
},
@@ -85,7 +85,7 @@ var MailManager = AbstractService.extend({
addMessage: function (data, options) {
options = options || {};
var message = this.getMessage(data.id);
var prom = Promise.resolve();
var prom;
if (!message) {
prom = this._addNewMessage(data, options);
} else {
@@ -95,6 +95,7 @@ var MailManager = AbstractService.extend({
});
}
this._addMessageToThreads(message, options);
prom = Promise.resolve(message);
}
return prom;
},
@@ -106,7 +106,7 @@ MailManager.include({
// This may happen due to concurrent calls to this method from
// messaging menu preview click and handling of longpolling chat
// window state.
return;
return Promise.resolve();
}
var threadWindow = this._getThreadWindow(threadID);
var prom = Promise.resolve();
@@ -108,6 +108,7 @@ var ActivityMenu = Widget.extend({
*/
_onActivityActionClick: function (ev) {
ev.stopPropagation();
this.$('.dropdown-toggle').dropdown('toggle');
var targetAction = $(ev.currentTarget);
var actionXmlid = targetAction.data('action_xmlid');
if (actionXmlid) {
+1
View File
@@ -110,6 +110,7 @@
}
.o_mail_thread {
position: relative;
flex: 1 0 0;
overflow: auto;
-webkit-overflow-scrolling: touch; // smooth scrolling in iOS app (Safari)
+65
View File
@@ -1144,6 +1144,71 @@ QUnit.test('chatter: access document with some notifs', async function (assert)
form.destroy();
});
QUnit.test('chatter: new message notification from document', async function (assert) {
// when receiving new messages from a document that is open, it should
// not mark the messages are read
assert.expect(4);
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
services: this.services,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="foo"/>' +
'</sheet>' +
'<div class="oe_chatter">' +
'<field name="message_ids" widget="mail_thread"/>' +
'</div>' +
'</form>',
res_id: 2,
session: {
partner_id: 3,
},
mockRPC: function (route, args) {
if (args.method === 'set_message_done') {
throw new Error("should not mark message as read");
}
return this._super.apply(this, arguments);
},
});
var thread = form.call('mail_service', 'getDocumentThread', 'partner', 2);
assert.strictEqual(
thread.getUnreadCounter(),
0,
"document thread should have no unread messages initially");
assert.containsNone(
form,
'.o_thread_message',
"should have no message in the chatter");
// Simulate received needaction`` message on this document
var message = {
author_id: [1, "Me"],
body: '<p>test</p>',
channel_ids: [],
id: 2,
model: 'partner',
needaction_partner_ids: [3],
res_id: 2,
};
const notifications = [ [['myDB', 'ir.needaction'], message] ];
form.call('bus_service', 'trigger', 'notification', notifications);
await testUtils.nextTick();
assert.strictEqual(
thread.getUnreadCounter(),
1,
"document thread should have one unread message (not marked as read)");
assert.containsOnce(
form,
'.o_thread_message',
"should have one message in the chatter");
form.destroy();
});
QUnit.test('chatter: post a message and switch in edit mode', async function (assert) {
assert.expect(5);
+65
View File
@@ -59,6 +59,7 @@ QUnit.module('Discuss', {
type: 'integer',
},
},
records: [],
},
'res.partner': {
fields: {
@@ -1490,6 +1491,70 @@ QUnit.test('custom-named DM conversation', async function (assert) {
discuss.destroy();
});
QUnit.test('receive channel message notification then delayed needaction notification', async function (assert) {
assert.expect(3);
const message = {
author_id: [5, 'Demo User'],
body: '<p>test</p>',
channel_ids: [1],
id: 100,
model: 'mail.channel',
needaction: true,
needaction_partner_ids: [3],
res_id: 1,
};
this.data.initMessaging = {
channel_slots: {
channel_channel: [{
id: 1,
channel_type: 'channel',
name: "general",
}],
},
};
const discuss = await createDiscuss({
context: {},
params: {},
data: this.data,
services: this.services,
session: {
partner_id: 3
},
});
const $inbox = discuss.$('.o_mail_discuss_item[data-thread-id="mailbox_inbox"]');
assert.hasClass(
$inbox,
'o_active',
"'Inbox' should be the currently active thread");
assert.containsNone(
discuss,
'.o_thread_message',
"inbox should contain no messages initially");
// simulate new needaction message posted on channnel
this.data['mail.message'].records.push(message);
// simulate receiving channel notification
discuss.call('bus_service', 'trigger', 'notification', [
[['myDB', 'mail.channel', 1], message]
]);
// short delay after receiving needaction notification
await testUtils.nextTick();
// simulate receiving needaction message notification after a short delay
discuss.call('bus_service', 'trigger', 'notification', [
[['myDB', 'ir.needaction', 3], message]
]);
await testUtils.nextTick();
assert.containsOnce(
discuss,
'.o_thread_message',
"inbox should contain one message");
discuss.destroy();
});
QUnit.test('input not cleared on unresolved message_post rpc', async function (assert) {
assert.expect(2);
@@ -153,7 +153,7 @@ QUnit.test('activity menu widget: activity menu with 3 records', async function
});
QUnit.test('activity menu widget: activity view icon', async function (assert) {
assert.expect(10);
assert.expect(12);
var self = this;
var activityMenu = new ActivityMenu();
testUtils.mock.addMockEnvironment(activityMenu, {
@@ -193,9 +193,15 @@ QUnit.test('activity menu widget: activity view icon', async function (assert) {
// click on the "Issue" activity icon
await testUtils.dom.click(activityMenu.$('.dropdown-toggle'));
assert.hasClass(activityMenu.$('.dropdown-menu'), 'show',
"dropdown should be expanded");
await testUtils.dom.click(activityMenu.$(".o_mail_activity_action[data-model_name='Issue']"));
assert.doesNotHaveClass(activityMenu.$('.dropdown-menu'), 'show',
"dropdown should be collapsed");
// click on the "Note" activity icon
await testUtils.dom.click(activityMenu.$('.dropdown-toggle'));
await testUtils.dom.click(activityMenu.$(".o_mail_activity_action[data-model_name='Note']"));
assert.verifySteps([
@@ -920,5 +920,62 @@ QUnit.test('messaging menu widget: expand on thread preview', async function (as
messagingMenu.destroy();
});
QUnit.test('messaging menu widget: click twice preview on slow message_fetch should open chat window once', async function (assert) {
// This test assumes that a condition for opening chat window is to
// successfully fetch messages beforehand.
assert.expect(1);
const self = this;
// Used to pause `message_fetch` after opening the messaging menu.
// This is necessary `message_fetch` on mailbox_inbox is required to
// display the previews.
let lockMessageFetch = false;
const messageFetchProm = testUtils.makeTestPromise();
const messagingMenu = new MessagingMenu();
testUtils.addMockEnvironment(messagingMenu, {
services: this.services,
data: this.data,
session: { partner_id: 1 },
async mockRPC(route, args) {
if (args.method === 'message_fetch' && lockMessageFetch) {
const _super = this._super.bind(this);
await messageFetchProm;
return _super(route, args);
}
if (args.method === 'channel_minimize') {
// called to detach thread in chat window
// simulate longpolling response with new chat window state
const channelInfo = {
...self.data['mail.channel'].records[0],
is_minimized: true,
state: 'open',
};
const notifications = [ [['myDB', 'res.partner'], channelInfo] ];
messagingMenu.call('bus_service', 'trigger', 'notification', notifications);
}
return this._super(...arguments);
},
});
await messagingMenu.appendTo($('#qunit-fixture'));
// Opening chat window 1st time from messaging menu (pending from `messageFetchDef`)
await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
lockMessageFetch = true;
await testUtils.dom.click(messagingMenu.$('.o_mail_preview'));
// Click again on preview to open chat window
await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
await testUtils.dom.click(messagingMenu.$('.o_mail_preview'));
messageFetchProm.resolve();
await testUtils.nextTick();
assert.containsOnce(
$,
'.o_thread_window',
"should only display a single chat window");
messagingMenu.destroy();
});
});
});
+1 -1
View File
@@ -48,7 +48,7 @@ class MailResendMessage(models.TransientModel):
rec['mail_message_id'] = mail_message_id.id
rec['partner_ids'] = partner_ids
else:
raise UserError('No message_id found in context')
raise UserError(_('No message_id found in context'))
return rec
@api.multi
+1 -1
View File
@@ -43,7 +43,7 @@ class MaintenanceEquipmentCategory(models.Model):
alias_id = fields.Many2one(
'mail.alias', 'Alias', ondelete='restrict', required=True,
help="Email alias for this equipment category. New emails will automatically "
"create new maintenance request for this equipment category.")
"create a new equipment under this category.")
fold = fields.Boolean(string='Folded in Maintenance Pipe', compute='_compute_fold', store=True)
@api.multi
@@ -136,7 +136,7 @@
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_semantic_html_override">
<div class="o_dropdown_kanban dropdown">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
@@ -435,7 +435,7 @@
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click">
<div class="o_dropdown_kanban dropdown">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
+1 -1
View File
@@ -252,7 +252,7 @@ class MassMailingContact(models.Model):
contacts = self.env['mail.mass_mailing.list_contact_rel'].search([('list_id', '=', active_list_id)])
return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])]
else:
raise UserError('Search opt out cannot be executed without a unique and valid active mailing list context.')
raise UserError(_('Search opt out cannot be executed without a unique and valid active mailing list context.'))
@api.depends('subscription_list_ids')
def _compute_opt_out(self):
@@ -386,7 +386,7 @@
</div>
<div class="o_kanban_card_manage_pane">
<div class="o_kanban_card_manage_section o_dropdown_kanban dropdown">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
@@ -685,7 +685,7 @@
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing">
<div class="o_dropdown_kanban dropdown">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" data-display="static" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
@@ -1041,7 +1041,7 @@
<div t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click oe_kanban_mass_mailing_campaign">
<div class="o_dropdown_kanban dropdown">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" data-display="static" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
@@ -58,7 +58,7 @@
</div>
</div>
</div>
<h2>Mailing Lists</h2>
<h2 id="mailing_list_settings_title" invisible="1">Mailing Lists</h2>
<div id="mailing_list" class="row mt16 o_settings_container"/>
</div>
</xpath>
+5
View File
@@ -249,6 +249,11 @@ class MrpWorkorder(models.Model):
for order in (self - late_orders):
order.color = 2
@api.onchange('date_planned_start', 'duration_expected')
def _onchange_date_planned_finished(self):
if self.date_planned_start and self.duration_expected:
self.date_planned_finished = self.date_planned_start + relativedelta(minutes=self.duration_expected)
@api.multi
def write(self, values):
if list(values.keys()) != ['time_ids'] and any(workorder.state == 'done' for workorder in self):
+1 -1
View File
@@ -98,7 +98,7 @@
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
<div class="o_dropdown_kanban dropdown">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
@@ -110,7 +110,7 @@ class ResPartner(models.Model):
if suggestions:
results = []
for suggestion in suggestions:
results.append(suggestion)
results.append(self._format_data_company(suggestion))
return results
else:
return []
@@ -76,7 +76,10 @@ var ScreenWidget = PosBaseWidget.extend({
barcode_client_action: function(code){
var partner = this.pos.db.get_partner_by_barcode(code.code);
if(partner){
this.pos.get_order().set_client(partner);
if (this.pos.get_order().get_client() !== partner) {
this.pos.get_order().set_client(partner);
this.pos.get_order().set_pricelist(_.findWhere(this.pos.pricelists, {'id': partner.property_product_pricelist[0]}) || this.pos.default_pricelist);
}
return true;
}
this.barcode_error_action(code);
+1 -1
View File
@@ -10,7 +10,7 @@
<field name="end_date"/>
</group>
<group>
<field name="pos_config_ids" colspan="4" nolabel="1" />
<field name="pos_config_ids" mode="tree" colspan="4" nolabel="1" />
</group>
<footer>
<button name="generate_report" string="Print" type="object" class="btn-primary"/>
+3 -3
View File
@@ -244,7 +244,7 @@ class CustomerPortal(Controller):
document = request.env[model_name].browse([document_id])
document_sudo = document.sudo().exists()
if not document_sudo:
raise MissingError("This document does not exist.")
raise MissingError(_("This document does not exist."))
try:
document.check_access_rights('read')
document.check_access_rule('read')
@@ -279,12 +279,12 @@ class CustomerPortal(Controller):
def _show_report(self, model, report_type, report_ref, download=False):
if report_type not in ('html', 'pdf', 'text'):
raise UserError("Invalid report type: %s" % report_type)
raise UserError(_("Invalid report type: %s") % report_type)
report_sudo = request.env.ref(report_ref).sudo()
if not isinstance(report_sudo, type(request.env['ir.actions.report'])):
raise UserError("%s is not the reference of a report" % report_ref)
raise UserError(_("%s is not the reference of a report") % report_ref)
method_name = 'render_qweb_%s' % (report_type)
report = getattr(report_sudo, method_name)([model.id], data={'report_type': report_type})[0]
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="pos_config_view_form_inherit_pos_mercury" model="ir.ui.view">
<field name="name">pos.config.form.inherit.mercury</field>
<record id="res_config_view_form_inherit_pos_mercury" model="ir.ui.view">
<field name="name">res.config.form.inherit.mercury</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="point_of_sale.res_config_settings_view_form"/>
<field name="arch" type="xml">
@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="pos_config_view_form_inherit_pos_mercury" model="ir.ui.view">
<!-- TODO the inherit for
point_of_sale.res_config_settings_view_form in this module
uses the pos_config_view_form_inherit_pos_mercury xml id
already. It should be renamed to
res_config_settings_view_form_inherit_pos_mercury. -->
<record id="pos_config_view_form_inherit_pos_mercury_2" model="ir.ui.view">
<field name="name">pos.config.form.view.inherit.mercury</field>
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
+1 -1
View File
@@ -577,7 +577,7 @@
</small>
</div>
<div class="o_dropdown_kanban dropdown" groups="base.group_user">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
+2
View File
@@ -405,6 +405,8 @@ class PurchaseOrder(models.Model):
'default_type': 'in_invoice',
'default_purchase_id': self.id,
}
if self.user_id:
result['context']['default_user_id'] = self.user_id.id
# choose the view_mode accordingly
if len(self.invoice_ids) > 1 and not create_bill:
result['domain'] = "[('id', 'in', " + str(self.invoice_ids.ids) + ")]"
+2 -1
View File
@@ -199,7 +199,8 @@ class ProductionLot(models.Model):
stock_moves = self.env['stock.move.line'].search([
('lot_id', '=', lot.id),
('state', '=', 'done')
]).mapped('move_id').filtered(
]).mapped('move_id')
stock_moves = stock_moves.search([('id', 'in', stock_moves.ids)]).filtered(
lambda move: move.picking_id.location_id.usage == 'supplier' and move.state == 'done')
lot.purchase_order_ids = stock_moves.mapped('purchase_line_id.order_id')
lot.purchase_order_count = len(lot.purchase_order_ids)
@@ -314,6 +314,68 @@ class TestCreatePicking(common.TestProductCommon):
self.assertEqual(sum(customer_picking.move_lines.mapped('reserved_availability')), 100.0, 'The total quantity for the customer move should be available and reserved.')
def test_04_rounding(self):
""" We set the Unit(s) rounding to 1.0 and ensure buying 1.2 units in a PO is rounded to 1.0
at reception.
"""
uom_unit = self.env.ref('uom.product_uom_unit')
uom_unit.rounding = 1.0
# buy a dozen
po = self.env['purchase.order'].create(self.po_vals)
po.order_line.product_qty = 1.2
po.button_confirm()
# the move should be 1.0 units
move1 = po.picking_ids.move_lines[0]
self.assertEqual(move1.product_uom_qty, 1.0)
self.assertEqual(move1.product_uom.id, uom_unit.id)
self.assertEqual(move1.product_qty, 1.0)
# edit the so line, buy 2.4 units, the move should now be 2.0 units
po.order_line.product_qty = 2.0
self.assertEqual(move1.product_uom_qty, 2.0)
self.assertEqual(move1.product_uom.id, uom_unit.id)
self.assertEqual(move1.product_qty, 2.0)
# deliver everything
move1.quantity_done = 2.0
po.picking_ids.button_validate()
# check the delivered quantity
self.assertEqual(po.order_line.qty_received, 2.0)
def test_05_uom_rounding(self):
""" We set the Unit(s) and Dozen(s) rounding to 1.0 and ensure buying 1.3 dozens in a PO is
rounded to 1.0 at reception.
"""
uom_unit = self.env.ref('uom.product_uom_unit')
uom_dozen = self.env.ref('uom.product_uom_dozen')
uom_unit.rounding = 1.0
uom_dozen.rounding = 1.0
# buy 1.3 dozen
po = self.env['purchase.order'].create(self.po_vals)
po.order_line.product_qty = 1.3
po.order_line.product_uom = uom_dozen.id
po.button_confirm()
# the move should be 16.0 units
move1 = po.picking_ids.move_lines[0]
self.assertEqual(move1.product_uom_qty, 16.0)
self.assertEqual(move1.product_uom.id, uom_unit.id)
self.assertEqual(move1.product_qty, 16.0)
# force the propagation of the uom, buy 2.6 dozens, the move 2 should have 2 dozens
self.env['ir.config_parameter'].sudo().set_param('stock.propagate_uom', '1')
po.order_line.product_qty = 2.6
move2 = po.picking_ids.move_lines.filtered(lambda m: m.product_uom.id == uom_dozen.id)
self.assertEqual(move2.product_uom_qty, 2)
self.assertEqual(move2.product_uom.id, uom_dozen.id)
self.assertEqual(move2.product_qty, 24)
def create_delivery_order(self, propagate_date, propagate_date_minimum_delta):
stock_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_stock')
customer_location = self.env['ir.model.data'].xmlid_to_object('stock.stock_location_customers')
+1 -1
View File
@@ -1083,7 +1083,7 @@ class SaleOrderLine(models.Model):
@api.multi
def write(self, values):
if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')):
raise UserError("You cannot change the type of a sale order line. Instead you should delete the current line and create a new line of the proper type.")
raise UserError(_("You cannot change the type of a sale order line. Instead you should delete the current line and create a new line of the proper type."))
if 'product_uom_qty' in values:
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+2 -2
View File
@@ -305,14 +305,14 @@ class SaleOrder(models.Model):
# Check commit 6bb42904a03 for next if/else
# Remove reward line if price or qty equal to 0
if values['product_uom_qty'] and values['price_unit']:
order.write({'order_line': [(1, line.id, values) for line in lines]})
lines.write(values)
else:
if program.reward_type != 'free_shipping':
# Can't remove the lines directly as we might be in a recordset loop
lines_to_remove += lines
else:
values.update(price_unit=0.0)
order.write({'order_line': [(1, line.id, values) for line in lines]})
lines.write(values)
return lines_to_remove
self.ensure_one()
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo import api, fields, models, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError
@@ -36,7 +36,7 @@ class SaleOrderTemplate(models.Model):
template_id = self.env['ir.default'].get('sale.order', 'sale_order_template_id')
for template in self:
if template_id and template_id == template.id:
raise UserError('Before archiving "%s" please select another default template in the settings.' % template.name)
raise UserError(_('Before archiving "%s" please select another default template in the settings.') % template.name)
return super(SaleOrderTemplate, self).write(vals)
@@ -87,7 +87,7 @@ class SaleOrderTemplateLine(models.Model):
@api.multi
def write(self, values):
if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')):
raise UserError("You cannot change the type of a sale quote line. Instead you should delete the current line and create a new line of the proper type.")
raise UserError(_("You cannot change the type of a sale quote line. Instead you should delete the current line and create a new line of the proper type."))
return super(SaleOrderTemplateLine, self).write(values)
_sql_constraints = [
@@ -98,12 +98,13 @@
</field>
</record>
<!-- TODO remove me after saas-12.4 to be replaced by sale.prod_config_main -->
<menuitem id="menu_product_settings" name="Products" sequence="80"
parent="sale.menu_sale_config"/>
<menuitem id="menu_product_attribute_action"
action="product.attribute_action"
parent="menu_product_settings" groups="product.group_product_variant" sequence="1"/>
parent="sale.prod_config_main" groups="product.group_product_variant" sequence="1"/>
<menuitem id="menu_catalog_variants_action"
action="product.variants_action"
parent="menu_product_settings" groups="product.group_product_variant" sequence="2"/>
parent="sale.prod_config_main" groups="product.group_product_variant" sequence="2"/>
</odoo>
+2 -1
View File
@@ -123,7 +123,8 @@ class ProductionLot(models.Model):
stock_moves = self.env['stock.move.line'].search([
('lot_id', '=', lot.id),
('state', '=', 'done')
]).mapped('move_id').filtered(
]).mapped('move_id')
stock_moves = stock_moves.search([('id', 'in', stock_moves.ids)]).filtered(
lambda move: move.picking_id.location_dest_id.usage == 'customer' and move.state == 'done')
lot.sale_order_ids = stock_moves.mapped('sale_line_id.order_id')
lot.sale_order_count = len(lot.sale_order_ids)
+1 -1
View File
@@ -63,7 +63,7 @@ class AccountMoveLine(models.Model):
sale_line_delivery = line.sale_line_ids.filtered(lambda sol: sol.product_id.invoice_policy == 'delivery' and sol.product_id.service_type == 'timesheet')
if sale_line_delivery:
domain = self._timesheet_domain_get_invoiced_lines(sale_line_delivery)
timesheets = self.env['account.analytic.line'].search(domain)
timesheets = self.env['account.analytic.line'].search(domain).sudo()
timesheets.write({
'timesheet_invoice_id': line.move_id.id,
})
+2 -1
View File
@@ -90,7 +90,8 @@ class SaleOrder(models.Model):
task_projects = self.tasks_ids.mapped('project_id')
if len(task_projects) == 1 and len(self.tasks_ids) > 1: # redirect to task of the project (with kanban stage, ...)
action = self.env.ref('project.act_project_project_2_project_task_all').read()[0]
action = self.with_context(active_id=task_projects.id).env.ref(
'project.act_project_project_2_project_task_all').read()[0]
if action.get('context'):
eval_context = self.env['ir.actions.actions']._get_eval_context()
eval_context.update({'active_id': task_projects.id})
+3 -1
View File
@@ -751,7 +751,9 @@ class UoM(models.Model):
procurement_uom = self
computed_qty = qty
get_param = self.env['ir.config_parameter'].sudo().get_param
if procurement_uom.id != quant_uom.id and get_param('stock.propagate_uom') != '1':
if get_param('stock.propagate_uom') != '1':
computed_qty = self._compute_quantity(qty, quant_uom, rounding_method='HALF-UP')
procurement_uom = quant_uom
else:
computed_qty = self._compute_quantity(qty, procurement_uom, rounding_method='HALF-UP')
return (computed_qty, procurement_uom)
+1 -9
View File
@@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
@@ -54,13 +53,6 @@ class ResConfigSettings(models.TransientModel):
def _onchange_group_stock_production_lot(self):
if not self.group_stock_production_lot:
self.group_lot_on_delivery_slip = False
tracked_products = self.env['product.template'].search([('tracking', 'in', ['lot', 'serial']),])
if not self.group_stock_production_lot and tracked_products:
names = ", ".join(tracked_products.mapped('display_name') if len(tracked_products) <= 10
else tracked_products[:10].mapped('display_name') + ["..."])
raise UserError(_("You should not remove the 'lots and serial numbers' "
"option while the following products are still tracked by lot "
"or serial number:\n %s") % names)
@api.onchange('group_stock_adv_location')
def onchange_adv_location(self):
+1 -1
View File
@@ -54,7 +54,7 @@ class ProductionLot(models.Model):
@api.multi
def write(self, vals):
if 'product_id' in vals and any([vals['product_id'] != lot.product_id.id for lot in self]):
move_lines = self.env['stock.move.line'].search([('lot_id', 'in', self.ids)])
move_lines = self.env['stock.move.line'].search([('lot_id', 'in', self.ids), ('product_id', '!=', vals['product_id'])])
if move_lines:
raise UserError(_(
'You are not allowed to change the product linked to a serial or lot number ' +
@@ -32,7 +32,7 @@
<span t-field="pick.name"/>
</td>
<td>
<img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', pick.name, 300, 50)" alt="Barcode"/>
<img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('Code128', pick.name, 600, 100)" style="width:300px;height:50px" alt="Barcode"/>
</td>
<td>
<span t-field="pick.state"/>
@@ -15,12 +15,15 @@
<h2>
<span t-esc="user_input.partner_id.name or user_input.email"/>
</h2>
<p>Successfully achieved</p>
<!-- You should not print a certificate if failed :) -->
<p t-if="user_input.quizz_passed">Successfully achieved</p>
<p t-if="not user_input.quizz_passed">Successfully failed</p>
<h3>
<span t-field="user_input.survey_id.display_name"/>
</h3>
<p>
<strong>Date of Certification:</strong>
<strong t-if="user_input.quizz_passed">Date of Certification:</strong>
<strong t-if="not user_input.quizz_passed">Date of Failure:</strong>
<span t-field="user_input.create_date"/>
</p>
</div>
+1 -1
View File
@@ -146,7 +146,7 @@
<div t-name="kanban-box" t-attf-class="oe_kanban_color_#{kanban_getcolor(record.color.raw_value)} oe_kanban_card oe_kanban_global_click o_kanban_card_survey">
<div class="o_dropdown_kanban dropdown" t-if="widget.editable">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu">
+1 -1
View File
@@ -522,7 +522,7 @@ var utils = {
}
var normalized_value = value / precision;
var epsilon_magnitude = Math.log(Math.abs(normalized_value))/Math.log(2);
var epsilon = Math.pow(2, epsilon_magnitude - 53);
var epsilon = Math.pow(2, epsilon_magnitude - 52);
normalized_value += normalized_value >= 0 ? epsilon : -epsilon;
/**
@@ -1362,9 +1362,9 @@ var BasicModel = AbstractModel.extend({
// apply changes to local data
for (var fieldName in changes) {
field = record.fields[fieldName];
if (field.type === 'one2many' || field.type === 'many2many') {
if (field && (field.type === 'one2many' || field.type === 'many2many')) {
defs.push(this._applyX2ManyChange(record, fieldName, changes[fieldName], options));
} else if (field.type === 'many2one' || field.type === 'reference') {
} else if (field && (field.type === 'many2one' || field.type === 'reference')) {
defs.push(this._applyX2OneChange(record, fieldName, changes[fieldName]));
} else {
record._changes[fieldName] = changes[fieldName];
@@ -1379,7 +1379,7 @@ var BasicModel = AbstractModel.extend({
var onChangeFields = []; // the fields that have changed and that have an on_change
for (var fieldName in changes) {
field = record.fields[fieldName];
if (field.onChange) {
if (field && field.onChange) {
var isX2Many = field.type === 'one2many' || field.type === 'many2many';
if (!isX2Many || (self._isX2ManyValid(record._changes[fieldName] || record.data[fieldName]))) {
onChangeFields.push(fieldName);
@@ -91,10 +91,15 @@ KanbanRenderer.include({
var index = _.findIndex(this.widgets, {db_id: localID});
var $column = this.widgets[index].$el;
var left = $column.css('left');
var right = $column.css('right');
var scrollTop = $column.scrollTop();
return this._super.apply(this, arguments).then(function () {
$column = self.widgets[index].$el;
$column.css({left: left});
if (_t.database.parameters.direction === 'rtl') {
$column.css({right: right});
} else {
$column.css({left: left});
}
$column.scrollTop(scrollTop); // required when clicking on 'Load More'
self._enableSwipe();
});
@@ -115,19 +120,40 @@ KanbanRenderer.include({
var self = this;
var moveToIndex = this.activeColumnIndex;
var updateFunc = animate ? 'animate' : 'css';
var rtl = _t.database.parameters.direction === 'rtl';
_.each(this.widgets, function (column, index) {
var columnID = column.id || column.db_id;
var $column = self.$('.o_kanban_group[data-id="' + columnID + '"]');
if (index === moveToIndex - 1) {
$column[updateFunc]({left: '-100%'});
if (rtl) {
$column[updateFunc]({right: '-100%'});
} else {
$column[updateFunc]({left: '-100%'});
}
} else if (index === moveToIndex + 1) {
$column[updateFunc]({left: '100%'});
if (rtl) {
$column[updateFunc]({right: '100%'});
} else {
$column[updateFunc]({left: '100%'});
}
} else if (index === moveToIndex) {
$column[updateFunc]({left: '0%'});
if (rtl) {
$column[updateFunc]({right: '0%'});
} else {
$column[updateFunc]({left: '0%'});
}
} else if (index < moveToIndex) {
$column.css({left: '-100%'});
if (rtl) {
$column.css({right: '-100%'});
} else {
$column.css({left: '-100%'});
}
} else if (index > moveToIndex) {
$column.css({left: '100%'});
if (rtl) {
$column.css({right: '100%'});
} else {
$column.css({left: '100%'});
}
}
});
}
@@ -184,6 +210,7 @@ KanbanRenderer.include({
}
}
// Apply the scroll x on the tabs
// XXX in case of RTL, should we use scrollRight?
this.$('.o_kanban_mobile_tabs').scrollLeft(scrollToLeft);
}
},
@@ -213,16 +240,17 @@ KanbanRenderer.include({
*/
_enableSwipe: function () {
var self = this;
var step = _t.database.parameters.direction === 'rtl' ? -1 : 1;
this.$el.swipe({
excludedElements: ".o_kanban_mobile_tabs",
swipeLeft: function () {
var moveToIndex = self.activeColumnIndex + 1;
var moveToIndex = self.activeColumnIndex + step;
if (moveToIndex < self.widgets.length) {
self._moveToGroup(moveToIndex, self.ANIMATE);
}
},
swipeRight: function () {
var moveToIndex = self.activeColumnIndex - 1;
var moveToIndex = self.activeColumnIndex - step;
if (moveToIndex > -1) {
self._moveToGroup(moveToIndex, self.ANIMATE);
}
@@ -502,16 +502,16 @@ ListRenderer.include({
switch (field.type) {
case 'binary': return 1;
case 'boolean': return '40px';
case 'char': return 1;
case 'date': return '100px';
case 'datetime': return '150px';
case 'float': return '100px';
case 'char': return 2;
case 'date': return '92px';
case 'datetime': return '146px';
case 'float': return '92px';
case 'html': return 3;
case 'integer': return '80px';
case 'integer': return '74px';
case 'many2many': return 2;
case 'many2one': return 2;
case 'monetary': return 1.2;
case 'one2many': return 2.5;
case 'monetary': return '104px';
case 'one2many': return 2;
case 'reference': return 1.5;
case 'selection': return 1.5;
case 'text': return 2.5;
@@ -38,6 +38,9 @@
&.o_list_many2one {
white-space: pre-wrap;
}
&.o_list_text {
word-break: break-word;
}
}
tfoot {
@@ -1,10 +1,12 @@
odoo.define('web.kanban_mobile_tests', function (require) {
"use strict";
var core = require('web.core');
var KanbanView = require('web.KanbanView');
var testUtils = require('web.test_utils');
var createView = testUtils.createView;
var _t = core._t;
QUnit.module('Views', {
beforeEach: function () {
@@ -119,6 +121,35 @@ QUnit.module('Views', {
kanban.destroy();
});
QUnit.test('mobile grouped rendering in rtl direction', async function (assert) {
assert.expect(2);
var direction = _t.database.parameters.direction;
_t.database.parameters.direction = 'rtl';
var kanban = await createView({
View: KanbanView,
model: 'partner',
data: this.data,
arch: '<kanban class="o_kanban_test o_kanban_small_column" on_create="quick_create">' +
'<templates><t t-name="kanban-box">' +
'<div><field name="foo"/></div>' +
'</t></templates>' +
'</kanban>',
domain: [['product_id', '!=', false]],
groupBy: ['product_id'],
});
assert.strictEqual(kanban.$('.o_kanban_group:first')[0].style.right, '0%',
"first tab should have 50% right");
assert.strictEqual(kanban.$('.o_kanban_group:nth(1)')[0].style.right, '100%',
"second tab should have 100% right");
kanban.destroy();
_t.database.parameters.direction = direction;
});
QUnit.test('mobile grouped with undefined column', async function (assert) {
assert.expect(5);
+6 -7
View File
@@ -1432,10 +1432,10 @@ QUnit.module('Views', {
QUnit.test('width of some of the fields should be hardcoded', async function (assert) {
const assertions = [
{ field: 'bar', expected: 40, type: 'Boolean' },
{ field: 'int_field', expected: 80, type: 'Integer' },
{ field: 'qux', expected: 100, type: 'Float' },
{ field: 'date', expected: 100, type: 'Date' },
{ field: 'datetime', expected: 150, type: 'Datetime' },
{ field: 'int_field', expected: 74, type: 'Integer' },
{ field: 'qux', expected: 92, type: 'Float' },
{ field: 'date', expected: 92, type: 'Date' },
{ field: 'datetime', expected: 146, type: 'Datetime' },
];
assert.expect(assertions.length + 1);
@@ -1497,9 +1497,8 @@ QUnit.module('Views', {
});
// the width is hardcoded to make sure we have the same condition
// between debug mode and non debug mode (#qunit-fixture is limited to
// 1000px)
list.$el.width('1000px');
// between debug mode and non debug mode
list.$el.width('1200px');
var startHeight = list.$('.o_data_row:first').height();
// start edition of first row
+6 -1
View File
@@ -103,7 +103,12 @@ class WebsiteMultiMixin(models.AbstractModel):
_name = 'website.multi.mixin'
_description = 'Multi Website Mixin'
website_id = fields.Many2one('website', string='Website', help='Restrict publishing to this website.')
website_id = fields.Many2one(
"website",
string="Website",
ondelete="restrict",
help="Restrict publishing to this website.",
)
@api.multi
def can_access_from_current_website(self, website_id=False):
+13
View File
@@ -170,6 +170,19 @@ class Website(models.Model):
if 'favicon' in vals:
vals['favicon'] = tools.image_process(vals['favicon'], size=(256, 256), crop='center', output_format='ICO')
@api.multi
def unlink(self):
# Do not delete invoices, delete what's strictly necessary
attachments_to_unlink = self.env['ir.attachment'].search([
('website_id', 'in', self.ids),
'|', '|',
('key', '!=', False), # theme attachment
('url', 'ilike', '.custom.'), # customized theme attachment
('url', 'ilike', '.assets\\_'),
])
attachments_to_unlink.unlink()
return super(Website, self).unlink()
# ----------------------------------------------------------
# Page Management
# ----------------------------------------------------------
+1 -1
View File
@@ -21,7 +21,7 @@ class Menu(models.Model):
page_id = fields.Many2one('website.page', 'Related Page', ondelete='cascade')
new_window = fields.Boolean('New Window')
sequence = fields.Integer(default=_default_sequence)
website_id = fields.Many2one('website', 'Website')
website_id = fields.Many2one('website', 'Website', ondelete='cascade')
parent_id = fields.Many2one('website.menu', 'Parent Menu', index=True, ondelete="cascade")
child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus')
parent_path = fields.Char(index=True)
+51
View File
@@ -5,6 +5,57 @@ import odoo
import odoo.tests
@odoo.tests.tagged('-at_install', 'post_install')
class TestUiCustomizeTheme(odoo.tests.HttpCase):
def test_01_attachment_website_unlink(self):
''' Some ir.attachment needs to be unlinked when a website is unlink,
otherwise some flows will just crash. That's the case when 2 website
have their theme color customized. Removing a website will make its
customized attachment generic, thus having 2 attachments with the
same URL available for other websites, leading to singleton errors
(among other).
But no all attachment should be deleted, eg we don't want to delete
a SO or invoice PDF coming from an ecommerce order.
'''
Website = self.env['website']
Page = self.env['website.page']
Attachment = self.env['ir.attachment']
website_default = Website.browse(1)
website_test = Website.create({'name': 'Website Test'})
# simulate attachment state when editing 2 theme through customize
custom_url = '/TEST/website/static/src/scss/options/colors/user_theme_color_palette.custom.web.assets_common.scss'
scss_attachment = Attachment.create({
'name': custom_url,
'type': 'binary',
'mimetype': 'text/scss',
'datas': '',
'url': custom_url,
'website_id': website_default.id
})
scss_attachment.copy({'website_id': website_test.id})
# simulate PDF from ecommerce order
# Note: it will only have its website_id flag if the website has a domain
# equal to the current URL (fallback or get_current_website())
so_attachment = Attachment.create({
'name': 'SO036.pdf',
'type': 'binary',
'mimetype': 'application/pdf',
'datas': '',
'website_id': website_test.id
})
# avoid sql error on page website_id restrict
Page.search([('website_id', '=', website_test.id)]).unlink()
website_test.unlink()
self.assertEqual(Attachment.search_count([('url', '=', custom_url)]), 1, 'Should not left duplicates when deleting a website')
self.assertTrue(so_attachment.exists(), 'Most attachment should not be deleted')
self.assertFalse(so_attachment.website_id, 'Website should be removed')
@odoo.tests.tagged('-at_install', 'post_install')
class TestUiHtmlEditor(odoo.tests.HttpCase):
def test_html_editor_multiple_templates(self):
@@ -12,12 +12,22 @@ publicWidget.registry.visitor = publicWidget.Widget.extend({
start: function () {
var defs = [this._super.apply(this, arguments)];
var self = this;
var $eventList = this.$('.country_events_list');
this._originalContent = $eventList[0].outerHTML;
defs.push(this._rpc({route: '/event/get_country_event_list'}).then(function (data) {
if (data) {
self.$('.country_events_list').replaceWith(data);
self._$loadedContent = $(data);
$eventList.replaceWith(self._$loadedContent);
}
}));
return Promise.all(defs);
},
/**
* @override
*/
destroy: function () {
this._super.apply(this, arguments);
this._$loadedContent.replaceWith(this._originalContent);
},
});
});
@@ -5,6 +5,9 @@
<field name="name">res.config.settings.view.form.inherit.website.mass.mailing</field>
<field name="model">res.config.settings</field>
<field name="arch" type="xml">
<xpath expr="//h2[@id='mailing_list_settings_title']" position="attributes">
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//div[@id='mailing_list']" position="inside">
<div class="col-lg-6 o_setting_box col-12" title="This adds a new Newsletter Popup snippet to drag &amp; drop on your website pages. It triggers a popup when visitors land on the page. This popup prompts them to enter their email address to join a mailing list.">
<div class="o_setting_left_pane">
@@ -6,4 +6,4 @@ from odoo import fields, models
class PaymentAcquirer(models.Model):
_inherit = 'payment.acquirer'
website_id = fields.Many2one('website')
website_id = fields.Many2one('website', ondelete='restrict')
+1 -1
View File
@@ -29,7 +29,7 @@ class ProductPricelist(models.Model):
domain = [('company_id', '=', company_id)]
return self.env['website'].search(domain, limit=1)
website_id = fields.Many2one('website', string="Website", default=_default_website)
website_id = fields.Many2one('website', string="Website", ondelete='restrict', default=_default_website)
code = fields.Char(string='E-commerce Promotional Code', groups="base.group_user")
selectable = fields.Boolean(help="Allow the end user to choose this price list")
@@ -18,14 +18,15 @@ class WebsiteSaleProductComparison(WebsiteSale):
products = request.env['product.product'].search([('id', 'in', product_ids)])
values['products'] = products.with_context(display_default_code=False)
res = {}
res = OrderedDict()
attrs = products.mapped('product_tmpl_id.valid_product_template_attribute_line_ids.attribute_id').filtered(lambda x: x.create_variant)
for attr in attrs.sorted(lambda att: (att.category_id.sequence, att.sequence)):
cat_name = attr.category_id.name or _('Uncategorized')
res.setdefault(cat_name, OrderedDict()).setdefault(attr.name, [' - '] * len(products))
for num, product in enumerate(products):
for var in product.product_tmpl_id._get_valid_product_template_attribute_lines():
for var in product.product_tmpl_id.valid_product_template_attribute_line_ids.filtered(lambda x: x.attribute_id.create_variant):
cat_name = var.attribute_id.category_id.name or _('Uncategorized')
att_name = var.attribute_id.name
if not var.attribute_id.create_variant:
continue
res.setdefault(cat_name, OrderedDict()).setdefault(att_name, [' - '] * len(products))
val = product.attribute_value_ids.filtered(lambda x: x.attribute_id == var.attribute_id)
if val:
res[cat_name][att_name][num] = val[0].name
@@ -18,7 +18,7 @@ class ProductWishlist(models.Model):
currency_id = fields.Many2one('res.currency', related='pricelist_id.currency_id', readonly=True)
pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', help='Pricelist when added')
price = fields.Monetary(digits=0, currency_field='currency_id', string='Price', help='Price of the product when it has been added in the wishlist')
website_id = fields.Many2one('website', required=True)
website_id = fields.Many2one('website', ondelete='cascade', required=True)
active = fields.Boolean(default=True, required=True)
@api.model
@@ -8,7 +8,7 @@ class WebsiteTwitterTweet(models.Model):
_name = 'website.twitter.tweet'
_description = 'Website Twitter'
website_id = fields.Many2one('website', string='Website')
website_id = fields.Many2one('website', string='Website', ondelete='cascade')
screen_name = fields.Char(string='Screen Name')
tweet = fields.Text(string='Tweets')
+11
View File
@@ -0,0 +1,11 @@
Ukraine, 2019-08-12
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Igor Ishchenko igor@cruxlab.com https://github.com/igoris
+11
View File
@@ -0,0 +1,11 @@
Portugal, 2019-08-09
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Reinaldo Ramos 42942314+reinaldoramosarxi@users.noreply.github.com https://github.com/reinaldoramosarxi
+6 -2
View File
@@ -458,12 +458,16 @@ template inheritance
''''''''''''''''''''
Template inheritance is used to alter existing templates in-place, e.g. to
add information to templates created by an other modules.
add information to templates created by other modules.
Template inheritance is performed via the ``t-extend`` directive which takes
the name of the template to alter as parameter.
The alteration is then performed with any number of ``t-jquery``
When ``t-extend`` is combined with ``t-name`` a new template with the given name
is created. In this case the extended template is not altered, instead the
directives define how to create the new template.
In both cases the alteration is then performed with any number of ``t-jquery``
sub-directives::
<t t-extend="base.template">
+19
View File
@@ -62,6 +62,25 @@ import time
if hasattr(time, 'tzset'):
time.tzset()
#----------------------------------------------------------
# PyPDF2 hack
# ensure that zlib does not throw error -5 when decompressing
# because some pdf won't fit into allocated memory
# https://docs.python.org/3/library/zlib.html#zlib.decompressobj
# ----------------------------------------------------------
import PyPDF2
try:
import zlib
def _decompress(data):
zobj = zlib.decompressobj()
return zobj.decompress(data)
PyPDF2.filters.decompress = _decompress
except ImportError:
pass # no fix required
#----------------------------------------------------------
# Shortcuts
#----------------------------------------------------------
+1
View File
@@ -51,6 +51,7 @@ base.lang_my,"Burmese / ဗမာစာ",my_MM,my,"Left-to-Right","[3,3]",".",",
"base.lang_nb_NO","Norwegian Bokmål / Norsk bokmål","nb_NO","nb_NO","Left-to-Right","[3,0]",","," ","%d. %b %Y","%H:%M:%S","True","1"
"base.lang_fa_IR","Persian / فارس","fa_IR","fa","Right-to-Left","[3,0]",".",",","%Y/%m/%d","%H:%M:%S","True","6"
"base.lang_pl","Polish / Język polski","pl_PL","pl","Left-to-Right","[]",",",,"%d.%m.%Y","%H:%M:%S","True","1"
"base.lang_pt_AO","Portuguese (AO) / Português (AO)","pt_AO","pt_AO","Left-to-Right","[]",",",,"%d-%m-%Y","%H:%M:%S","True","1"
"base.lang_pt_BR","Portuguese (BR) / Português (BR)","pt_BR","pt_BR","Left-to-Right","[3,0]",",",".","%d/%m/%Y","%H:%M:%S","True","7"
"base.lang_pt","Portuguese / Português","pt_PT","pt","Left-to-Right","[]",",",,"%d-%m-%Y","%H:%M:%S","True","1"
"base.lang_ro","Romanian / română","ro_RO","ro","Left-to-Right","[3,0]",",",".","%d.%m.%Y","%H:%M:%S","True","1"
1 id name code iso_code direction grouping decimal_point thousands_sep date_format time_format translatable week_start
51 base.lang_nb_NO Norwegian Bokmål / Norsk bokmål nb_NO nb_NO Left-to-Right [3,0] ,   %d. %b %Y %H:%M:%S True 1
52 base.lang_fa_IR Persian / فارس fa_IR fa Right-to-Left [3,0] . , %Y/%m/%d %H:%M:%S True 6
53 base.lang_pl Polish / Język polski pl_PL pl Left-to-Right [] , %d.%m.%Y %H:%M:%S True 1
54 base.lang_pt_AO Portuguese (AO) / Português (AO) pt_AO pt_AO Left-to-Right [] , %d-%m-%Y %H:%M:%S True 1
55 base.lang_pt_BR Portuguese (BR) / Português (BR) pt_BR pt_BR Left-to-Right [3,0] , . %d/%m/%Y %H:%M:%S True 7
56 base.lang_pt Portuguese / Português pt_PT pt Left-to-Right [] , %d-%m-%Y %H:%M:%S True 1
57 base.lang_ro Romanian / română ro_RO ro Left-to-Right [3,0] , . %d.%m.%Y %H:%M:%S True 1
+3 -1
View File
@@ -48,8 +48,10 @@ class IrRule(models.Model):
by the user with the switch company menu. These companies are
filtered and trusted.
"""
# use an empty context for 'user' to make the domain evaluation
# independent from the context
return {
'user': self.env.user,
'user': self.env.user.with_context({}),
'time': time,
'company_ids': self.env.companies.ids,
}
+1 -1
View File
@@ -883,7 +883,7 @@ class Users(models.Model):
cfg = self.env['ir.config_parameter'].sudo()
min_failures = int(cfg.get_param('base.login_cooldown_after', 5))
if min_failures == 0:
return True
return False
delay = int(cfg.get_param('base.login_cooldown_duration', 60))
return failures >= min_failures and (datetime.datetime.now() - previous) < datetime.timedelta(seconds=delay)
+2
View File
@@ -27,6 +27,8 @@ class TestFloatPrecision(TransactionCase):
try_round(0.0049,'0.00') # 0.0049 is closer to 0 than to 0.01, so should round down
try_round(0.005,'0.01') # the rule is to round half away from zero
try_round(-0.005,'-0.01') # the rule is to round half away from zero
try_round(6.6 * 0.175, '1.16') # 6.6 * 0.175 is rounded to 1.15 with epsilon = 53
try_round(-6.6 * 0.175, '-1.16')
def try_zero(amount, expected):
self.assertEqual(currency.is_zero(amount), expected,
+1 -1
View File
@@ -165,7 +165,7 @@
<div class="oe_module_vignette">
<t t-set="installed" t-value="record.state.raw_value == 'installed'"/>
<div class="o_dropdown_kanban dropdown" tabindex="-1">
<a class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" role="button" aria-label="Dropdown menu" title="Dropdown menu">
<a class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" data-display="static" href="#" role="button" aria-label="Dropdown menu" title="Dropdown menu">
<span class="fa fa-ellipsis-v"/>
</a>
<div class="dropdown-menu" role="menu" aria-labelledby="dLabel">
@@ -2,3 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_test_access_right_some_obj,access_test_access_right_some_obj,model_test_access_right_some_obj,,1,1,1,1
access_test_access_right_container,access_test_access_right_container,model_test_access_right_container,,1,1,1,1
access_test_access_right_parent,access_test_access_right_parent,model_test_access_right_parent,,1,1,1,1
access_test_access_right_obj_categ,access_test_access_right_obj_categ,model_test_access_right_obj_categ,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_test_access_right_some_obj access_test_access_right_some_obj model_test_access_right_some_obj 1 1 1 1
3 access_test_access_right_container access_test_access_right_container model_test_access_right_container 1 1 1 1
4 access_test_access_right_parent access_test_access_right_parent model_test_access_right_parent 1 1 1 1
5 access_test_access_right_obj_categ access_test_access_right_obj_categ model_test_access_right_obj_categ 1 1 1 1
+12
View File
@@ -8,6 +8,7 @@ class SomeObj(models.Model):
_description = 'Object For Test Access Right'
val = fields.Integer()
categ_id = fields.Many2one('test_access_right.obj_categ')
company_id = fields.Many2one('res.company')
forbidden = fields.Integer(
groups='test_access_rights.test_group,!base.group_no_one,base.group_user,!base.group_public',
@@ -28,3 +29,14 @@ class Parent(models.Model):
_inherits = {'test_access_right.some_obj': 'obj_id'}
obj_id = fields.Many2one('test_access_right.some_obj', required=True, ondelete='restrict')
class ObjCateg(models.Model):
_name = 'test_access_right.obj_categ'
_description = "Context dependent searchable model"
name = fields.Char(required=True)
def search(self, args, **kwargs):
if self.env.context.get('only_media'):
args += [('name', '=', 'Media')]
return super(ObjCateg, self).search(args, **kwargs)
@@ -8,10 +8,11 @@ class TestRules(TransactionCase):
def setUp(self):
super(TestRules, self).setUp()
self.id1 = self.env['test_access_right.some_obj']\
.create({'val': 1}).id
self.id2 = self.env['test_access_right.some_obj']\
.create({'val': -1}).id
ObjCateg = self.env['test_access_right.obj_categ']
SomeObj = self.env['test_access_right.some_obj']
self.categ1 = ObjCateg.create({'name': 'Food'}).id
self.id1 = SomeObj.create({'val': 1, 'categ_id': self.categ1}).id
self.id2 = SomeObj.create({'val': -1, 'categ_id': self.categ1}).id
# create a global rule forbidding access to records with a negative
# (or zero) val
self.env['ir.rule'].create({
@@ -19,6 +20,13 @@ class TestRules(TransactionCase):
'model_id': self.browse_ref('test_access_rights.model_test_access_right_some_obj').id,
'domain_force': "[('val', '>', 0)]"
})
# create a global rule that forbid access to records without
# categories, the search is part of the test
self.env['ir.rule'].create({
'name': 'See all categories',
'model_id': self.browse_ref('test_access_rights.model_test_access_right_some_obj').id,
'domain_force': "[('categ_id', 'in', user.env['test_access_right.obj_categ'].search([]).ids)]"
})
def test_basic_access(self):
env = self.env(user=self.browse_ref('base.public_user'))
@@ -82,3 +90,25 @@ class TestRules(TransactionCase):
Model = env['test_access_right.some_obj']
with self.assertQueryCount(0):
Model._filter_access_rules('read')
def test_no_context_in_ir_rules(self):
""" The context should not impact the ir rules. """
env = self.env(user=self.browse_ref('base.public_user'))
ObjCateg = self.env['test_access_right.obj_categ']
SomeObj = self.env['test_access_right.some_obj']
# validate the effect of context on category search, there are
# no existing media category
self.assertTrue(ObjCateg.search([]))
self.assertFalse(ObjCateg.with_context(only_media=True).search([]))
# record1 is food and is accessible with an empy context
ObjCateg.clear_caches()
records = SomeObj.search([('id', '=', self.id1)])
self.assertTrue(records)
# it should also be accessible as the context is not used when
# searching for SomeObjs
ObjCateg.clear_caches()
records = SomeObj.with_context(only_media=True).search([('id', '=', self.id1)])
self.assertTrue(records)
+2 -1
View File
@@ -706,7 +706,8 @@ class ChromeBrowser():
def set_cookie(self, name, value, path, domain):
params = {'name': name, 'value': value, 'path': path, 'domain': domain}
self._websocket_send('Network.setCookie', params=params)
_id = self._websocket_send('Network.setCookie', params=params)
return self._websocket_wait_id(_id)
def _wait_ready(self, ready_code, timeout=60):
self._logger.info('Evaluate ready code "%s"', ready_code)
+1 -1
View File
@@ -72,7 +72,7 @@ def float_round(value, precision_digits=None, precision_rounding=None, rounding_
normalized_value = value / rounding_factor # normalize
sign = math.copysign(1.0, normalized_value)
epsilon_magnitude = math.log(abs(normalized_value), 2)
epsilon = 2**(epsilon_magnitude-53)
epsilon = 2**(epsilon_magnitude-52)
# TIE-BREAKING: UP/DOWN (for ceiling[resp. flooring] operations)
# When rounding the value up[resp. down], we instead subtract[resp. add] the epsilon value