summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-03-06 10:59:32 +0100
committerChristophe Simonis <chs@odoo.com>2019-03-06 10:59:32 +0100
commitafe013330295b5f069a0df8529d1f1f0d12f2b3f (patch)
treee9e39304f0e797c0cd8b4314c173948e90181a90
parentb5885f0cb02ba6853e17eb93009b1ebad2ff8d35 (diff)
parent3e54704e669441e655b1fe19ee0fa0646cdbf8d5 (diff)
[MERGE] forward port branch saas-11.3 up to 3e54704e669
-rw-r--r--addons/account/models/account_bank_statement.py4
-rw-r--r--addons/account/tests/test_account_invoice_rounding.py4
-rw-r--r--addons/auth_signup/models/res_partner.py9
-rw-r--r--addons/event_sale/models/sale_order.py5
-rw-r--r--addons/hr_gamification/wizard/gamification_badge_user_wizard.py2
-rw-r--r--addons/hr_holidays/models/hr_leave.py2
-rw-r--r--addons/l10n_fr_fec/wizard/account_fr_fec.py79
-rw-r--r--addons/mail/models/mail_activity.py10
-rw-r--r--addons/point_of_sale/static/src/css/pos.css2
-rw-r--r--addons/portal/wizard/portal_wizard.py2
-rw-r--r--addons/product/models/product_pricelist.py6
-rw-r--r--addons/product/models/product_template.py6
-rw-r--r--addons/product/views/product_views.xml3
-rw-r--r--addons/sale/models/res_partner.py22
-rw-r--r--addons/sale_management/models/sale_order_template.py5
-rw-r--r--addons/stock/models/stock_move.py3
-rw-r--r--addons/stock/models/stock_picking.py2
-rw-r--r--addons/stock/models/stock_scrap.py3
-rw-r--r--addons/stock/tests/test_move.py9
-rw-r--r--addons/stock_account/views/product_views.xml2
-rw-r--r--addons/stock_account/views/stock_account_views.xml4
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js2
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_model.js8
-rw-r--r--addons/web/static/tests/views/pivot_tests.js42
-rw-r--r--addons/web_editor/models/ir_ui_view.py3
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.editor.js16
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js8
-rw-r--r--addons/website/__init__.py16
-rw-r--r--addons/website/__manifest__.py1
-rw-r--r--addons/website/static/src/js/tours/rte.js20
-rw-r--r--addons/website_event_sale/models/__init__.py1
-rw-r--r--addons/website_event_sale/models/product_pricelist.py23
-rw-r--r--doc/cla/corporate/adhoc.md1
-rw-r--r--doc/cla/corporate/projet-grup.md18
-rw-r--r--doc/cla/corporate/savoirfairelinux.md2
-rw-r--r--doc/cla/individual/cantecim.md11
-rw-r--r--doc/cla/individual/mjabuz.md11
-rw-r--r--odoo/addons/base/models/ir_model.py3
-rw-r--r--odoo/addons/base/models/ir_rule.py2
-rw-r--r--odoo/addons/base/models/qweb.py57
-rw-r--r--odoo/addons/base/tests/test_expression.py30
-rw-r--r--odoo/addons/base/tests/test_qweb.py41
-rw-r--r--odoo/addons/base/tests/test_views.py36
-rw-r--r--odoo/fields.py1
-rw-r--r--odoo/models.py1
-rw-r--r--odoo/osv/expression.py2
46 files changed, 437 insertions, 103 deletions
diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index c76a1f74499..7aca6df336e 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -588,8 +588,8 @@ class AccountBankStatementLine(models.Model):
# Create The payment
payment = self.env['account.payment']
+ partner_id = self.partner_id or (aml_dict.get('move_line') and aml_dict['move_line'].partner_id) or self.env['res.partner']
if abs(total)>0.00001:
- partner_id = self.partner_id and self.partner_id.id or False
partner_type = False
if partner_id and len(account_types) == 1:
partner_type = 'customer' if account_types == receivable_account_type else 'supplier'
@@ -604,7 +604,7 @@ class AccountBankStatementLine(models.Model):
payment = self.env['account.payment'].create({
'payment_method_id': payment_methods and payment_methods[0].id or False,
'payment_type': total >0 and 'inbound' or 'outbound',
- 'partner_id': self.partner_id and self.partner_id.id or False,
+ 'partner_id': partner_id.id,
'partner_type': partner_type,
'journal_id': self.statement_id.journal_id.id,
'payment_date': self.date,
diff --git a/addons/account/tests/test_account_invoice_rounding.py b/addons/account/tests/test_account_invoice_rounding.py
index dcb3752f6bc..4838eb3dc44 100644
--- a/addons/account/tests/test_account_invoice_rounding.py
+++ b/addons/account/tests/test_account_invoice_rounding.py
@@ -3,6 +3,7 @@
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
from odoo.exceptions import ValidationError
from odoo.tests import tagged
+from odoo.tools import mute_logger
import time
@@ -59,7 +60,8 @@ class TestAccountInvoiceRounding(AccountingTestCase):
# to avoid a ValidationError from _check_cash_rounding because the onchange
# are not well triggered in the tests.
try:
- invoice_id.cash_rounding_id = cash_rounding_id
+ with mute_logger('odoo.models'):
+ invoice_id.cash_rounding_id = cash_rounding_id
except ValidationError:
pass
diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py
index 5703e018548..9e4bc9ac518 100644
--- a/addons/auth_signup/models/res_partner.py
+++ b/addons/auth_signup/models/res_partner.py
@@ -8,6 +8,7 @@ from collections import defaultdict
from datetime import datetime, timedelta
from odoo import api, exceptions, fields, models, _
+from odoo.tools import pycompat
class SignupError(Exception):
pass
@@ -27,16 +28,16 @@ class ResPartner(models.Model):
signup_token = fields.Char(copy=False, groups="base.group_erp_manager")
signup_type = fields.Char(string='Signup Token Type', copy=False, groups="base.group_erp_manager")
signup_expiration = fields.Datetime(copy=False, groups="base.group_erp_manager")
- signup_valid = fields.Boolean(compute='_compute_signup_valid', compute_sudo=True, string='Signup Token is Valid')
+ signup_valid = fields.Boolean(compute='_compute_signup_valid', string='Signup Token is Valid')
signup_url = fields.Char(compute='_compute_signup_url', string='Signup URL')
@api.multi
@api.depends('signup_token', 'signup_expiration')
def _compute_signup_valid(self):
dt = now()
- for partner in self:
- partner.signup_valid = bool(partner.signup_token) and \
- (not partner.signup_expiration or dt <= partner.signup_expiration)
+ for partner, partner_sudo in pycompat.izip(self, self.sudo()):
+ partner.signup_valid = bool(partner_sudo.signup_token) and \
+ (not partner_sudo.signup_expiration or dt <= partner_sudo.signup_expiration)
@api.multi
def _compute_signup_url(self):
diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py
index 1b15093de03..fac1730bead 100644
--- a/addons/event_sale/models/sale_order.py
+++ b/addons/event_sale/models/sale_order.py
@@ -73,6 +73,11 @@ class SaleOrderLine(models.Model):
if self.event_ticket_id and (not self.event_id or self.event_id != self.event_ticket_id.event_id):
self.event_ticket_id = None
+ @api.onchange('product_uom', 'product_uom_qty')
+ def product_uom_change(self):
+ if not self.event_ticket_id:
+ super(SaleOrderLine, self).product_uom_change()
+
@api.onchange('event_ticket_id')
def _onchange_event_ticket_id(self):
company = self.event_id.company_id or self.env.user.company_id
diff --git a/addons/hr_gamification/wizard/gamification_badge_user_wizard.py b/addons/hr_gamification/wizard/gamification_badge_user_wizard.py
index 4e486a0e048..7c4c174129f 100644
--- a/addons/hr_gamification/wizard/gamification_badge_user_wizard.py
+++ b/addons/hr_gamification/wizard/gamification_badge_user_wizard.py
@@ -10,7 +10,7 @@ class GamificationBadgeUserWizard(models.TransientModel):
employee_id = fields.Many2one('hr.employee', string='Employee', required=True)
user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id',
- store=False, readonly=True)
+ store=False, readonly=True, compute_sudo=True)
@api.multi
def action_grant_badge(self):
diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py
index f93dfd44c78..c39239ab082 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -108,7 +108,7 @@ class HolidaysRequest(models.Model):
"\nThe status is 'Approved', when leave request is approved by manager.")
payslip_status = fields.Boolean('Reported in last payslips', help='Green this button when the leave has been taken into account in the payslip.')
report_note = fields.Text('HR Comments')
- user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', related_sudo=True, store=True, default=lambda self: self.env.uid, readonly=True)
+ user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', related_sudo=True, compute_sudo=True, store=True, default=lambda self: self.env.uid, readonly=True)
# leave type configuration
holiday_status_id = fields.Many2one(
"hr.leave.type", string="Leave Type", required=True, readonly=True,
diff --git a/addons/l10n_fr_fec/wizard/account_fr_fec.py b/addons/l10n_fr_fec/wizard/account_fr_fec.py
index c606ce0972c..2215764fdf4 100644
--- a/addons/l10n_fr_fec/wizard/account_fr_fec.py
+++ b/addons/l10n_fr_fec/wizard/account_fr_fec.py
@@ -76,6 +76,27 @@ class AccountFrFec(models.TransientModel):
listrow = list(row)
return listrow
+ def _get_company_legal_data(self, company):
+ """
+ Dom-Tom are excluded from the EU's fiscal territory
+ Those regions do not have SIREN
+ sources:
+ https://www.service-public.fr/professionnels-entreprises/vosdroits/F23570
+ http://www.douane.gouv.fr/articles/a11024-tva-dans-les-dom
+ """
+ dom_tom_group = self.env.ref('l10n_fr.dom-tom')
+ is_dom_tom = company.country_id.code in dom_tom_group.country_ids.mapped('code')
+ if not is_dom_tom and not company.vat:
+ raise Warning(
+ _("Missing VAT number for company %s") % company.name)
+ if not is_dom_tom and company.vat[0:2] != 'FR':
+ raise Warning(
+ _("FEC is for French companies only !"))
+
+ return {
+ 'siren': company.vat[4:13] if not is_dom_tom else '',
+ }
+
@api.multi
def generate_fec(self):
self.ensure_one()
@@ -87,6 +108,9 @@ class AccountFrFec(models.TransientModel):
# 2) CSV files are easier to read/use for a regular accountant.
# So it will be easier for the accountant to check the file before
# sending it to the fiscal administration
+ company = self.env.user.company_id
+ company_legal_data = self._get_company_legal_data(company)
+
header = [
u'JournalCode', # 0
u'JournalLib', # 1
@@ -108,18 +132,7 @@ class AccountFrFec(models.TransientModel):
u'Idevise', # 17
]
- company = self.env.user.company_id
- if not company.vat:
- raise Warning(
- _("Missing VAT number for company %s") % company.name)
- if company.vat[0:2] != 'FR':
- raise Warning(
- _("FEC is for French companies only !"))
-
- fecfile = io.BytesIO()
- w = pycompat.csv_writer(fecfile, delimiter='|')
- w.writerow(header)
-
+ rows_to_write = [header]
# INITIAL BALANCE
unaffected_earnings_xml_ref = self.env.ref('account.data_unaffected_earnings')
unaffected_earnings_line = True # used to make sure that we add the unaffected earning initial balance only once
@@ -195,7 +208,8 @@ class AccountFrFec(models.TransientModel):
else:
listrow[11] = '0,00'
listrow[12] = str(-listrow_amount).replace('.', ',')
- w.writerow(listrow)
+ rows_to_write.append(listrow)
+
#if the unaffected earnings account wasn't in the selection yet: add it manually
if (not unaffected_earnings_line
and unaffected_earnings_results
@@ -206,7 +220,7 @@ class AccountFrFec(models.TransientModel):
if unaffected_earnings_account:
unaffected_earnings_results[4] = unaffected_earnings_account.code
unaffected_earnings_results[5] = unaffected_earnings_account.name
- w.writerow(unaffected_earnings_results)
+ rows_to_write.append(unaffected_earnings_results)
# INITIAL BALANCE - receivable/payable
sql_query = '''
@@ -264,7 +278,7 @@ class AccountFrFec(models.TransientModel):
for row in self._cr.fetchall():
listrow = list(row)
account_id = listrow.pop()
- w.writerow(listrow)
+ rows_to_write.append(listrow)
# LINES
sql_query = '''
@@ -329,20 +343,19 @@ class AccountFrFec(models.TransientModel):
sql_query, (self.date_from, self.date_to, company.id))
for row in self._cr.fetchall():
- w.writerow(list(row))
+ rows_to_write.append(list(row))
- siren = company.vat[4:13]
+ fecvalue = self._csv_write_rows(rows_to_write)
end_date = fields.Date.to_string(self.date_to).replace('-', '')
suffix = ''
if self.export_type == "nonofficial":
suffix = '-NONOFFICIAL'
- fecvalue = fecfile.getvalue()
+
self.write({
'fec_data': base64.encodestring(fecvalue),
# Filename = <siren>FECYYYYMMDD where YYYMMDD is the closing date
- 'filename': '%sFEC%s%s.csv' % (siren, end_date, suffix),
+ 'filename': '%sFEC%s%s.csv' % (company_legal_data['siren'], end_date, suffix),
})
- fecfile.close()
action = {
'name': 'FEC',
@@ -351,3 +364,29 @@ class AccountFrFec(models.TransientModel):
'target': 'self',
}
return action
+
+ def _csv_write_rows(self, rows, lineterminator=u'\r\n'):
+ """
+ Write FEC rows into a file
+ It seems that Bercy's bureaucracy is not too happy about the
+ empty new line at the End Of File.
+
+ @param {list(list)} rows: the list of rows. Each row is a list of strings
+ @param {unicode string} [optional] lineterminator: effective line terminator
+ Has nothing to do with the csv writer parameter
+ The last line written won't be terminated with it
+
+ @return the value of the file
+ """
+ fecfile = io.BytesIO()
+ writer = pycompat.csv_writer(fecfile, delimiter='|', lineterminator='')
+
+ rows_length = len(rows)
+ for i, row in enumerate(rows):
+ if not i == rows_length - 1:
+ row.append(lineterminator)
+ writer.writerow(row)
+
+ fecvalue = fecfile.getvalue()
+ fecfile.close()
+ return fecvalue
diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py
index 4fc05a95e05..0b327d59338 100644
--- a/addons/mail/models/mail_activity.py
+++ b/addons/mail/models/mail_activity.py
@@ -122,9 +122,9 @@ class MailActivity(models.Model):
activity_type_id = fields.Many2one(
'mail.activity.type', 'Activity',
domain="['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]", ondelete='restrict')
- activity_category = fields.Selection(related='activity_type_id.category', readonly=False)
- activity_decoration = fields.Selection(related='activity_type_id.decoration_type', readonly=False)
- icon = fields.Char('Icon', related='activity_type_id.icon', readonly=False)
+ activity_category = fields.Selection(related='activity_type_id.category', readonly=True)
+ activity_decoration = fields.Selection(related='activity_type_id.decoration_type', readonly=True)
+ icon = fields.Char('Icon', related='activity_type_id.icon', readonly=True)
summary = fields.Char('Summary')
note = fields.Html('Note')
feedback = fields.Html('Feedback')
@@ -146,8 +146,8 @@ class MailActivity(models.Model):
('today', 'Today'),
('planned', 'Planned')], 'State',
compute='_compute_state')
- recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type")
- previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type')
+ recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type", readonly=True)
+ previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type', readonly=True)
has_recommended_activities = fields.Boolean(
'Next activities available',
compute='_compute_has_recommended_activities',
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index 59ec2f0b801..8ae3d0d7687 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -1479,6 +1479,8 @@ td {
body {
margin: 0;
color: white !important;
+ /* avoid black background if backgrounds are printed */
+ background: initial;
}
.oe_leftbar,
.oe_loading,
diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py
index ee5b82eecda..2f17441f451 100644
--- a/addons/portal/wizard/portal_wizard.py
+++ b/addons/portal/wizard/portal_wizard.py
@@ -35,7 +35,7 @@ class PortalWizard(models.TransientModel):
contact_ids = set()
user_changes = []
for partner in self.env['res.partner'].sudo().browse(partner_ids):
- contact_partners = partner.child_ids or [partner]
+ contact_partners = partner.child_ids | partner
for contact in contact_partners:
# make sure that each contact appears at most once in the list
if contact.id not in contact_ids:
diff --git a/addons/product/models/product_pricelist.py b/addons/product/models/product_pricelist.py
index 44461a789d1..4139c5c93d3 100644
--- a/addons/product/models/product_pricelist.py
+++ b/addons/product/models/product_pricelist.py
@@ -243,7 +243,11 @@ class Pricelist(models.Model):
break
# Final price conversion into pricelist currency
if suitable_rule and suitable_rule.compute_price != 'fixed' and suitable_rule.base != 'pricelist':
- price = product.currency_id._convert(price, self.currency_id, self.env.user.company_id, date, round=False)
+ if suitable_rule.base == 'standard_price':
+ cur = product.cost_currency_id
+ else:
+ cur = product.currency_id
+ price = cur._convert(price, self.currency_id, self.env.user.company_id, date, round=False)
results[product.id] = (price, suitable_rule and suitable_rule.id or False)
diff --git a/addons/product/models/product_template.py b/addons/product/models/product_template.py
index 1fcc1afacc7..8b0061e1848 100644
--- a/addons/product/models/product_template.py
+++ b/addons/product/models/product_template.py
@@ -57,6 +57,8 @@ class ProductTemplate(models.Model):
currency_id = fields.Many2one(
'res.currency', 'Currency', compute='_compute_currency_id')
+ cost_currency_id = fields.Many2one(
+ 'res.currency', 'Cost Currency', compute='_compute_cost_currency_id')
# price fields
# price: total template price, context dependent (partner, pricelist, quantity)
@@ -157,6 +159,10 @@ class ProductTemplate(models.Model):
for template in self:
template.currency_id = template.company_id.sudo().currency_id.id or main_company.currency_id.id
+ def _compute_cost_currency_id(self):
+ for template in self:
+ template.cost_currency_id = self.env.user.company_id.currency_id.id
+
@api.multi
def _compute_template_price(self):
prices = self._compute_template_price_no_inverse()
diff --git a/addons/product/views/product_views.xml b/addons/product/views/product_views.xml
index f14f35c8cff..73321703058 100644
--- a/addons/product/views/product_views.xml
+++ b/addons/product/views/product_views.xml
@@ -52,13 +52,14 @@
<field name="list_price" string="Sales Price" widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}"/>
<label for="standard_price" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '&gt;', 1), ('is_product_variant', '=', False)]}"/>
<div name="standard_price_uom" groups="base.group_user" attrs="{'invisible': [('product_variant_count', '&gt;', 1), ('is_product_variant', '=', False)]}">
- <field name="standard_price" widget='monetary' options="{'currency_field': 'currency_id'}"/>
+ <field name="standard_price" widget='monetary' options="{'currency_field': 'cost_currency_id'}"/>
</div>
<field name="company_id" groups="base.group_multi_company"
options="{'no_create': True}"/>
<field name="uom_id" groups="uom.group_uom" options="{'no_create': True}"/>
<field name="uom_po_id" groups="uom.group_uom" options="{'no_create': True}"/>
<field name="currency_id" invisible="1"/>
+ <field name="cost_currency_id" invisible="1"/>
<field name="product_variant_id" invisible="1"/>
</group>
</group>
diff --git a/addons/sale/models/res_partner.py b/addons/sale/models/res_partner.py
index d03c877b938..a1f60defb2d 100644
--- a/addons/sale/models/res_partner.py
+++ b/addons/sale/models/res_partner.py
@@ -14,14 +14,20 @@ class ResPartner(models.Model):
sale_warn_msg = fields.Text('Message for Sales Order')
def _compute_sale_order_count(self):
- sale_data = self.env['sale.order'].read_group(domain=[('partner_id', 'child_of', self.ids)],
- fields=['partner_id'], groupby=['partner_id'])
- mapped_data = dict([(m['partner_id'][0], m['partner_id_count']) for m in sale_data])
- for partner in self:
- # let's obtain the partner id and all its child ids
- partner_ids = [partner.id] + partner.child_ids.ids
- # then we can sum for all the partner's child
- partner.sale_order_count = sum(mapped_data.get(child, 0) for child in partner_ids)
+ # retrieve all children partners and prefetch 'parent_id' on them
+ all_partners = self.search([('id', 'child_of', self.ids)])
+ all_partners.read(['parent_id'])
+
+ sale_order_groups = self.env['sale.order'].read_group(
+ domain=[('partner_id', 'in', all_partners.ids)],
+ fields=['partner_id'], groupby=['partner_id']
+ )
+ for group in sale_order_groups:
+ partner = self.browse(group['partner_id'][0])
+ while partner:
+ if partner in self:
+ partner.sale_order_count += group['partner_id_count']
+ partner = partner.parent_id
def can_edit_vat(self):
can_edit_vat = super(ResPartner, self).can_edit_vat()
diff --git a/addons/sale_management/models/sale_order_template.py b/addons/sale_management/models/sale_order_template.py
index a81c103415b..abadd6a71fb 100644
--- a/addons/sale_management/models/sale_order_template.py
+++ b/addons/sale_management/models/sale_order_template.py
@@ -120,7 +120,10 @@ class SaleOrderTemplateOption(models.Model):
return
product = self.product_id
self.price_unit = product.list_price
- self.name = product.name
+ name = product.name
+ if self.product_id.description_sale:
+ name += '\n' + self.product_id.description_sale
+ self.name = name
self.uom_id = product.uom_id
domain = {'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
return {'domain': domain}
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 6f76933fad9..64f9e170e9d 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -1134,7 +1134,8 @@ class StockMove(models.Model):
moves_todo.mapped('move_dest_ids')._action_assign()
# We don't want to create back order for scrap moves
- if all(move_todo.scrapped for move_todo in moves_todo):
+ # Replace by a kwarg in master
+ if self.env.context.get('is_scrap'):
return moves_todo
if picking:
diff --git a/addons/stock/models/stock_picking.py b/addons/stock/models/stock_picking.py
index 3929bb15718..b4d120ce53b 100644
--- a/addons/stock/models/stock_picking.py
+++ b/addons/stock/models/stock_picking.py
@@ -699,7 +699,7 @@ class Picking(models.Model):
# If no lots when needed, raise error
picking_type = self.picking_type_id
precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure')
- no_quantities_done = all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in self.move_line_ids)
+ no_quantities_done = all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in self.move_line_ids.filtered(lambda m: m.state not in ('done', 'cancel')))
no_reserved_quantities = all(float_is_zero(move_line.product_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids)
if no_reserved_quantities and no_quantities_done:
raise UserError(_('You cannot validate a transfer if no quantites are reserved nor done. To force the transfer, switch in edit more and encode the done quantities.'))
diff --git a/addons/stock/models/stock_scrap.py b/addons/stock/models/stock_scrap.py
index 7c8eeefb57a..9a9ccd51943 100644
--- a/addons/stock/models/stock_scrap.py
+++ b/addons/stock/models/stock_scrap.py
@@ -106,7 +106,8 @@ class StockScrap(models.Model):
def do_scrap(self):
for scrap in self:
move = self.env['stock.move'].create(scrap._prepare_move_values())
- move._action_done()
+ # master: replace context by cancel_backorder
+ move.with_context(is_scrap=True)._action_done()
scrap.write({'move_id': move.id, 'state': 'done'})
return True
diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py
index 0939389dda9..8a6cb87da67 100644
--- a/addons/stock/tests/test_move.py
+++ b/addons/stock/tests/test_move.py
@@ -3379,6 +3379,15 @@ class StockMove(TransactionCase):
})
picking.action_confirm()
picking.action_assign()
+
+ scrap = self.env['stock.scrap'].create({
+ 'picking_id': picking.id,
+ 'product_id': self.product1.id,
+ 'product_uom_id': self.uom_unit.id,
+ 'scrap_qty': 5.0,
+ })
+ scrap.do_scrap()
+
# No products are reserved on the move of 10, click on `button_validate`.
with self.assertRaises(UserError):
picking.button_validate()
diff --git a/addons/stock_account/views/product_views.xml b/addons/stock_account/views/product_views.xml
index 237a3862444..2c8fdcdd4b6 100644
--- a/addons/stock_account/views/product_views.xml
+++ b/addons/stock_account/views/product_views.xml
@@ -40,7 +40,7 @@
<field name="standard_price" position="replace">
<field name="standard_price"
attrs="{'readonly':[('valuation','=','real_time')]}"
- widget='monetary' options="{'currency_field': 'currency_id', 'field_digits': True}"
+ widget='monetary' options="{'currency_field': 'cost_currency_id', 'field_digits': True}"
class="float-left"/>
<div name="update_cost_price" class="col-lg-2">
<button string="Update Cost" type="action"
diff --git a/addons/stock_account/views/stock_account_views.xml b/addons/stock_account/views/stock_account_views.xml
index 4b0614577ad..db4154cb031 100644
--- a/addons/stock_account/views/stock_account_views.xml
+++ b/addons/stock_account/views/stock_account_views.xml
@@ -77,8 +77,8 @@
<field name="qty_at_date"/>
<field name="uom_id" groups="uom.group_uom"/>
<field name="currency_id" invisible="1"/>
- <field name="stock_value_currency_id" invisible="1"/>
- <field name="stock_value" sum="Stock Valuation" widget="monetary" options="{'currency_field': 'stock_value_currency_id'}"/>
+ <field name="cost_currency_id" invisible="1"/>
+ <field name="stock_value" sum="Stock Valuation" widget="monetary" options="{'currency_field': 'cost_currency_id'}"/>
<field name="cost_method" invisible="1"/>
<button name="action_valuation_at_date_details" type="object" icon="fa-info-circle" aria-label="Valuation" title="Valuation" attrs="{'invisible': [('cost_method', '!=', 'fifo')]}" />
</tree>
diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js
index 260266ac9a9..f1086f2a2d2 100644
--- a/addons/web/static/src/js/fields/relational_fields.js
+++ b/addons/web/static/src/js/fields/relational_fields.js
@@ -241,7 +241,7 @@ var FieldMany2One = AbstractField.extend({
_bindAutoComplete: function () {
var self = this;
// avoid ignoring autocomplete="off" by obfuscating placeholder, see #30439
- if (this.$input.attr('placeholder')) {
+ if ($.browser.chrome && this.$input.attr('placeholder')) {
this.$input.attr('placeholder', function (index, val) {
return val.split('').join('\ufeff');
});
diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js
index 3f195c1ea24..4bd648e5a0a 100644
--- a/addons/web/static/src/js/views/pivot/pivot_model.js
+++ b/addons/web/static/src/js/views/pivot/pivot_model.js
@@ -986,14 +986,14 @@ var PivotModel = AbstractModel.extend({
});
var index = 0;
- var rowGroupBys = !_.isEmpty(this.data.groupedBy) ? this.data.groupedBy : this.initialRowGroupBys;
+ var rowGroupBys = !_.isEmpty(this.data.groupedBy) ? this.data.groupedBy : this.initialRowGroupBys.slice();
+ this.data.groupedBy = rowGroupBys;
var colGroupBys = this.data.colGroupBys;
var dataPoint, row, col, attrs, cell_value;
var main_row_header, main_col_header;
var groupBys;
var m;
-
for (var i = 0; i < rowGroupBys.length + 1; i++) {
for (var j = 0; j < colGroupBys.length + 1; j++) {
for (var k = 0; k < data[index].length; k++) {
@@ -1129,8 +1129,8 @@ var PivotModel = AbstractModel.extend({
*/
_updateMainGroupBys: function (old, main) {
var new_groupby_length = this._getHeaderDepth(main.root) - 1;
- var new_groupby_list = old.root.groupbys.slice(0, new_groupby_length);
- main.root.groupbys = new_groupby_list;
+ main.root.groupbys = old.root.groupbys;
+ main.root.groupbys.splice(new_groupby_length);
},
/**
* @param {Object} old_tree
diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js
index b09f4d5b1bf..8ea5987bbfa 100644
--- a/addons/web/static/tests/views/pivot_tests.js
+++ b/addons/web/static/tests/views/pivot_tests.js
@@ -1076,6 +1076,48 @@ QUnit.module('Views', {
pivot.destroy();
});
+ QUnit.test('Reload, group by columns, reload', function (assert) {
+ assert.expect(2);
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot/>',
+ });
+
+ // Set a column groupby
+ pivot.$('thead .o_pivot_header_cell_closed').click();
+ pivot.$('.o_field_selection li[data-field=customer] a').click();
+
+ // Set a domain
+ pivot.update({domain: [['product_id', '=', 41]]});
+
+ var expectedContext = {pivot_column_groupby: ['customer'],
+ pivot_measures: ['__count'],
+ pivot_row_groupby: []};
+
+ // Check that column groupbys were not lost
+ assert.deepEqual(pivot.getContext(), expectedContext,
+ 'Column groupby not lost after first reload');
+
+ // Set a column groupby
+ pivot.$('thead .o_pivot_header_cell_closed').click();
+ pivot.$('.o_field_selection li[data-field=product_id] a').click();
+
+ // Set a domain
+ pivot.update({domain: [['product_id', '=', 37]]});
+
+ var expectedContext = {pivot_column_groupby: ['customer', 'product_id'],
+ pivot_measures: ['__count'],
+ pivot_row_groupby: []};
+
+ assert.deepEqual(pivot.getContext(), expectedContext,
+ 'Column groupby not lost after second reload');
+
+ pivot.destroy();
+ });
+
QUnit.test('correctly uses pivot_ keys from the context', function (assert) {
assert.expect(7);
diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py
index b870a57edf9..a592e505602 100644
--- a/addons/web_editor/models/ir_ui_view.py
+++ b/addons/web_editor/models/ir_ui_view.py
@@ -272,5 +272,6 @@ class IrUiView(models.Model):
``bundles=True`` returns also the asset bundles
"""
user_groups = set(self.env.user.groups_id)
- views = self.with_context(active_test=False)._views_get(key, bundles=bundles)
+ View = self.with_context(active_test=False, lang=None)
+ views = View._views_get(key, bundles=bundles)
return views.filtered(lambda v: not v.groups_id or len(user_groups.intersection(v.groups_id)))
diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js
index 4a9e6cb91d6..7fe47777697 100644
--- a/addons/web_editor/static/src/js/editor/snippets.editor.js
+++ b/addons/web_editor/static/src/js/editor/snippets.editor.js
@@ -84,6 +84,16 @@ var SnippetEditor = Widget.extend({
});
}
+ this.$target.on('transitionstart.snippet_editor, animationstart.snippet_editor', function () {
+ self._targetIsAnimated = true;
+ });
+ this.$target.on('transitionend.snippet_editor, animationend.snippet_editor', function () {
+ self._targetIsAnimated = false;
+ if (self.$el.is('.oe_active')) {
+ self.cover();
+ }
+ });
+
return $.when.apply($, defs);
},
/**
@@ -93,6 +103,7 @@ var SnippetEditor = Widget.extend({
this.cleanForSave();
this._super.apply(this, arguments);
this.$target.removeData('snippet-editor');
+ this.$target.off('.snippet_editor');
},
//--------------------------------------------------------------------------
@@ -124,6 +135,11 @@ var SnippetEditor = Widget.extend({
* Makes the editor overlay cover the associated snippet.
*/
cover: function () {
+ if (this._targetIsAnimated) {
+ // Do not cover a target being animated, it will be covered once the
+ // animation is completed.
+ return;
+ }
var offset = this.$target.offset();
var manipulatorOffset = this.$el.parent().offset();
offset.top -= manipulatorOffset.top;
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js
index 97d3777b95f..b254ea932d2 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -290,13 +290,11 @@ var SnippetOption = Widget.extend({
.addClass('active');
_processSelectClassElements(this.$el);
- _.each(this.$el.find('.dropdown-menu'), function (group) {
- _processSelectClassElements($(group).children());
- });
- function _processSelectClassElements($elements) {
+ function _processSelectClassElements($el) {
var maxNbClasses = -1;
- $elements.filter('[data-select-class]')
+ $el.find('[data-select-class]')
+ .addBack('[data-select-class]')
.removeClass('active')
.filter(function () {
var className = $(this).data('selectClass');
diff --git a/addons/website/__init__.py b/addons/website/__init__.py
index c158f87e14f..dcd5ec789d0 100644
--- a/addons/website/__init__.py
+++ b/addons/website/__init__.py
@@ -4,3 +4,19 @@
from . import controllers
from . import models
from . import wizard
+
+import odoo
+from odoo import api, SUPERUSER_ID
+from functools import partial
+
+
+def uninstall_hook(cr, registry):
+ def rem_website_id_null(dbname):
+ db_registry = odoo.modules.registry.Registry.new(dbname)
+ with api.Environment.manage(), db_registry.cursor() as cr:
+ env = api.Environment(cr, SUPERUSER_ID, {})
+ env['ir.model.fields'].search([
+ ('name', '=', 'website_id'),
+ ('model', '=', 'res.config.settings'),
+ ]).unlink()
+ cr.after('commit', partial(rem_website_id_null, cr.dbname))
diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py
index fe7c479d23e..80a52659470 100644
--- a/addons/website/__manifest__.py
+++ b/addons/website/__manifest__.py
@@ -36,4 +36,5 @@
],
'qweb': ['static/src/xml/website.backend.xml'],
'application': True,
+ 'uninstall_hook': 'uninstall_hook',
}
diff --git a/addons/website/static/src/js/tours/rte.js b/addons/website/static/src/js/tours/rte.js
index 46fbbff3fce..67426a64303 100644
--- a/addons/website/static/src/js/tours/rte.js
+++ b/addons/website/static/src/js/tours/rte.js
@@ -167,5 +167,25 @@ tour.register('rte_translator', {
}, {
content: "check bis: placeholder translation",
trigger: 'input[placeholder="test french placeholder"]',
+}, {
+ content: "Open customize menu",
+ trigger: "#customize-menu > .dropdown-toggle",
+}, {
+ content: "Open HTML editor",
+ trigger: "[data-action='ace']",
+}, {
+ content: "Check that the editor is not showing translated content (1)",
+ trigger: '.ace_text-layer .ace_line:contains("an HTML")',
+ run: function (actions) {
+ var lineEscapedText = $(this.$anchor.text()).text();
+ if (lineEscapedText !== "&lt;b&gt;&lt;/b&gt; is an HTML&nbsp;tag &amp; is empty") {
+ console.error('The HTML editor should display the correct untranslated content');
+ $('body').addClass('rte_translator_error');
+ }
+ },
+}, {
+ content: "Check that the editor is not showing translated content (2)",
+ trigger: 'body:not(.rte_translator_error)',
+ run: function () {},
}]);
});
diff --git a/addons/website_event_sale/models/__init__.py b/addons/website_event_sale/models/__init__.py
index 1bd7829ab86..a9c94cbe9ae 100644
--- a/addons/website_event_sale/models/__init__.py
+++ b/addons/website_event_sale/models/__init__.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from . import product
+from . import product_pricelist
from . import sale_order
from . import website
diff --git a/addons/website_event_sale/models/product_pricelist.py b/addons/website_event_sale/models/product_pricelist.py
new file mode 100644
index 00000000000..5b0d4465759
--- /dev/null
+++ b/addons/website_event_sale/models/product_pricelist.py
@@ -0,0 +1,23 @@
+# -*- coding: utf-8 -*-
+
+from odoo import _, api, models
+
+class PricelistItem(models.Model):
+ _inherit = "product.pricelist.item"
+
+ @api.onchange('applied_on', 'product_id', 'product_tmpl_id', 'min_quantity')
+ def _onchange_event_sale_warning(self):
+ if self.min_quantity > 0:
+ msg = ''
+ if self.applied_on == '3_global' or self.applied_on == '2_product_category':
+ msg = _("A pricelist item with a positive min. quantity will not be applied to the event tickets products.")
+ elif ((self.applied_on == '1_product' and self.product_tmpl_id.event_ok) or
+ (self.applied_on == '0_product_variant' and self.product_id.event_ok)):
+ msg = _("A pricelist item with a positive min. quantity cannot be applied to this event tickets product.")
+ if msg:
+ return {'warning':
+ {
+ 'title': _("Warning"),
+ 'message': msg
+ }
+ }
diff --git a/doc/cla/corporate/adhoc.md b/doc/cla/corporate/adhoc.md
index 428f46b0056..ca781cae7a5 100644
--- a/doc/cla/corporate/adhoc.md
+++ b/doc/cla/corporate/adhoc.md
@@ -14,3 +14,4 @@ Damián Soriano ds@adhoc.com.ar https://github.com/damiansoriano
Juan José Scarafía jjs@adhoc.com.ar https://github.com/jjscarafia
Nicolás Mac Rouillon nmr@adhoc.com.ar https://github.com/nicomacr
Katherine Zaoral kz@adhoc.com.ar https://github.com/zaoral
+Valentino Defelice vd@adhoc.com.ar https://github.com/ValentinoDefelice
diff --git a/doc/cla/corporate/projet-grup.md b/doc/cla/corporate/projet-grup.md
deleted file mode 100644
index 635f8d5148c..00000000000
--- a/doc/cla/corporate/projet-grup.md
+++ /dev/null
@@ -1,18 +0,0 @@
-Turkey, 2017-12-05
-
-Projet Yazilim Ve Danismanlik AS agrees to the terms of the Odoo Corporate Contributor License
-Agreement v1.0.
-
-I declare that I am authorized and able to make this agreement and sign this
-declaration.
-
-Signed,
-
-Can Tecim can.tecim@gmail.com https://github.com/cantecim
-
-List of contributors:
-
-Can Tecim can.tecim@gmail.com https://github.com/cantecim
-Can Tecim dev@projetgrup.com https://github.com/devprojetgrup
-Selahattin Ünlü selahattin.unlu@yandex.com https://github.com/selahattinunlu
-Abdurrahman Işık isikabdurrahman@yahoo.com https://github.com/abdurrahman
diff --git a/doc/cla/corporate/savoirfairelinux.md b/doc/cla/corporate/savoirfairelinux.md
index 6dca085c4ce..913e317cb07 100644
--- a/doc/cla/corporate/savoirfairelinux.md
+++ b/doc/cla/corporate/savoirfairelinux.md
@@ -21,6 +21,8 @@ Julien Jezequel-Breard julien.jezequel-breard@savoirfairelinux.com https://githu
Quentin Lavallée-Bourdeau quentin.lavallee@savoirfairelinux.com https://github.com/qtiplb
Rim Ben Dhaou rim.bendhaou@savoirfairelinux.com https://github.com/rimbendhaou
Yasmine El Mrini yasmine.elmrini@savoirfairelinux.com https://github.com/yasmineelmrini
+Luis Garcia Ruiz luis.garcia@savoirfairelinux.com https://github.com/eilst
+Dimitar Apostolov dimitar.apostolov@savoirfairelinux.com https://github.com/dapostolov-sfl
Jérome Boisvert-Chouinard jerome.boisvertchouinard@savoirfairelinux.com https://github.com/jbchouinard (up to 2017-10-17)
Julie Moussu julie.mousse@savoirfairelinux.com https://github.com/JulieSFL (up to 2017-10-17)
Pierre Lamarche pierre.lamarche@savoirfairelinux.com https://github.com/plamarche (up to 2017-10-17)
diff --git a/doc/cla/individual/cantecim.md b/doc/cla/individual/cantecim.md
new file mode 100644
index 00000000000..1aa35478772
--- /dev/null
+++ b/doc/cla/individual/cantecim.md
@@ -0,0 +1,11 @@
+Turkey, 2019-02-18
+
+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,
+
+Can Tecim can.tecim@gmail.com https://github.com/cantecim
diff --git a/doc/cla/individual/mjabuz.md b/doc/cla/individual/mjabuz.md
new file mode 100644
index 00000000000..55e9589f992
--- /dev/null
+++ b/doc/cla/individual/mjabuz.md
@@ -0,0 +1,11 @@
+Bangladesh, 16 February, 2019
+
+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,
+
+Abu Zafar azmikbal@gmail.com https://github.com/MjAbuz
diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py
index 1bbf53794a6..4a59c5d4249 100644
--- a/odoo/addons/base/models/ir_model.py
+++ b/odoo/addons/base/models/ir_model.py
@@ -199,6 +199,9 @@ class IrModel(models.Model):
# prevent screwing up fields that depend on these models' fields
model.field_id._prepare_update()
+ # delete fields whose comodel is being removed
+ self.env['ir.model.fields'].search([('relation', 'in', self.mapped('model'))]).unlink()
+
self._drop_table()
res = super(IrModel, self).unlink()
diff --git a/odoo/addons/base/models/ir_rule.py b/odoo/addons/base/models/ir_rule.py
index 90869ef45ac..9f63a415892 100644
--- a/odoo/addons/base/models/ir_rule.py
+++ b/odoo/addons/base/models/ir_rule.py
@@ -106,6 +106,8 @@ class IrRule(models.Model):
group_domains.append(dom)
# combine global domains and group domains
+ if not group_domains:
+ return expression.AND(global_domains)
return expression.AND(global_domains + [expression.OR(group_domains)])
@api.model
diff --git a/odoo/addons/base/models/qweb.py b/odoo/addons/base/models/qweb.py
index 51693c55cb1..de5d3cf9e2b 100644
--- a/odoo/addons/base/models/qweb.py
+++ b/odoo/addons/base/models/qweb.py
@@ -31,6 +31,8 @@ try:
except ImportError:
astor = None
+from odoo.tools.parse_version import parse_version
+
unsafe_eval = eval
_logger = logging.getLogger(__name__)
@@ -176,7 +178,7 @@ class QWebException(Exception):
return str(self)
# Avoid DeprecationWarning while still remaining compatible with werkzeug pre-0.9
-escape = (lambda text: _escape(text, quote=True)) if getattr(werkzeug, '__version__', '0.0') < '0.9.0' else _escape
+escape = (lambda text: _escape(text, quote=True)) if parse_version(getattr(werkzeug, '__version__', '0.0')) < parse_version('0.9.0') else _escape
def foreach_iterator(base_ctx, enum, name):
ctx = base_ctx.copy()
@@ -1486,7 +1488,7 @@ class QWeb(object):
)
if call_options:
- # update this dict with the content of `t-call-options`
+ # update this dict with the content of `t-call-options`
content.extend([
# options_.update(template options)
ast.Expr(ast.Call(
@@ -1497,7 +1499,56 @@ class QWeb(object):
),
args=[self._compile_expr(call_options)],
keywords=[], starargs=None, kwargs=None
- ))
+ )),
+
+ # if options.get('lang') != options_.get('lang'):
+ # self = self.with_context(lang=options.get('lang'))
+ ast.If(
+ test=ast.Compare(
+ left=ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='options', ctx=ast.Load()),
+ attr='get',
+ ctx=ast.Load()
+ ),
+ args=[ast.Str("lang")], keywords=[],
+ starargs=None, kwargs=None
+ ),
+ ops=[ast.NotEq()],
+ comparators=[ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id=name_options, ctx=ast.Load()),
+ attr='get',
+ ctx=ast.Load()
+ ),
+ args=[ast.Str("lang")], keywords=[],
+ starargs=None, kwargs=None
+ )]
+ ),
+ body=[
+ ast.Assign(
+ targets=[ast.Name(id='self', ctx=ast.Store())],
+ value=ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='self', ctx=ast.Load()),
+ attr='with_context',
+ ctx=ast.Load()
+ ),
+ args=[],
+ keywords=[ast.keyword('lang', ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id=name_options, ctx=ast.Load()),
+ attr='get',
+ ctx=ast.Load()
+ ),
+ args=[ast.Str("lang")], keywords=[],
+ starargs=None, kwargs=None
+ ))],
+ starargs=None, kwargs=None
+ )
+ )],
+ orelse=[],
+ )
])
if nsmap:
diff --git a/odoo/addons/base/tests/test_expression.py b/odoo/addons/base/tests/test_expression.py
index 9f628054f02..b40035dadd6 100644
--- a/odoo/addons/base/tests/test_expression.py
+++ b/odoo/addons/base/tests/test_expression.py
@@ -640,6 +640,36 @@ class TestExpression(TransactionCase):
'&', ('foo', '=', 2), ('bar', '=', 2)]
self.assertEqual(expression.OR([d1, d2]), expected)
+ def test_proper_combine_unit_leaves(self):
+ # test that unit leaves (TRUE_LEAF, FALSE_LEAF) are properly handled in specific cases
+ false = expression.FALSE_DOMAIN
+ true = expression.TRUE_DOMAIN
+ normal = [('foo', '=', 'bar')]
+ # OR with single FALSE_LEAF
+ expr = expression.OR([false])
+ self.assertEqual(expr, false)
+ # OR with multiple FALSE_LEAF
+ expr = expression.OR([false, false])
+ self.assertEqual(expr, false)
+ # OR with FALSE_LEAF and a normal leaf
+ expr = expression.OR([false, normal])
+ self.assertEqual(expr, normal)
+ # OR with AND of single TRUE_LEAF and normal leaf
+ expr = expression.OR([expression.AND([true]), normal])
+ self.assertEqual(expr, true)
+ # AND with single TRUE_LEAF
+ expr = expression.AND([true])
+ self.assertEqual(expr, true)
+ # AND with multiple TRUE_LEAF
+ expr = expression.AND([true, true])
+ self.assertEqual(expr, true)
+ # AND with TRUE_LEAF and normal leaves
+ expr = expression.AND([true, normal])
+ self.assertEqual(expr, normal)
+ # AND with OR with single FALSE_LEAF and normal leaf
+ expr = expression.AND([expression.OR([false]), normal])
+ self.assertEqual(expr, false)
+
class TestAutoJoin(TransactionCase):
diff --git a/odoo/addons/base/tests/test_qweb.py b/odoo/addons/base/tests/test_qweb.py
index 2a01fa1ca44..d8ce5e48f68 100644
--- a/odoo/addons/base/tests/test_qweb.py
+++ b/odoo/addons/base/tests/test_qweb.py
@@ -501,6 +501,47 @@ class TestQWebNS(TransactionCase):
with self.assertRaises(QWebException, msg=error_msg):
view1.render()
+ def test_render_t_call_propagates_t_lang(self):
+ current_lang = 'en_US'
+ other_lang = 'fr_FR'
+
+ self.env['res.lang'].load_lang(lang=other_lang)
+
+ self.env['res.lang'].search([('code', '=', other_lang)], limit=1).write({
+ 'active': True,
+ 'decimal_point': '*',
+ 'thousands_sep': '/'
+ })
+
+ view1 = self.env['ir.ui.view'].create({
+ 'name': "callee",
+ 'type': 'qweb',
+ 'arch': u"""
+ <t t-name="base.callee">
+ <t t-esc="9000000.00" t-options="{'widget': 'float', 'precision': 2}" />
+ </t>
+ """
+ })
+ self.env['ir.model.data'].create({
+ 'name': 'callee',
+ 'model': 'ir.ui.view',
+ 'module': 'base',
+ 'res_id': view1.id,
+ })
+
+ view2 = self.env['ir.ui.view'].create({
+ 'name': "calling",
+ 'type': 'qweb',
+ 'arch': u"""
+ <t t-name="base.calling">
+ <t t-call="base.callee" t-lang="'%s'" />
+ </t>
+ """ % other_lang
+ })
+
+ rendered = view2.with_context(lang=current_lang).render().strip()
+ self.assertEqual(rendered, b'9/000/000*00')
+
from copy import deepcopy
class FileSystemLoader(object):
diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py
index 7b8deb8ea33..33ac7035488 100644
--- a/odoo/addons/base/tests/test_views.py
+++ b/odoo/addons/base/tests/test_views.py
@@ -1107,7 +1107,7 @@ class TestViews(ViewCase):
# implemeted elsewhere...
modifiers_tests()
- @mute_logger('odoo.addons.base.models.ir_ui_view')
+ @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models')
def test_invalid_field(self):
with self.assertRaises(ValidationError):
self.View.create({
@@ -1121,7 +1121,7 @@ class TestViews(ViewCase):
""",
})
- @mute_logger('odoo.addons.base.models.ir_ui_view')
+ @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models')
def test_invalid_subfield(self):
with self.assertRaises(ValidationError):
self.View.create({
@@ -1140,7 +1140,7 @@ class TestViews(ViewCase):
""",
})
- @mute_logger('odoo.addons.base.models.ir_ui_view')
+ @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models')
def test_context_in_view(self):
arch = """
<form string="View">
@@ -1160,7 +1160,7 @@ class TestViews(ViewCase):
'arch': arch % '',
})
- @mute_logger('odoo.addons.base.models.ir_ui_view')
+ @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models')
def test_context_in_subview(self):
arch = """
<form string="View">
@@ -1192,7 +1192,7 @@ class TestViews(ViewCase):
'arch': arch % ('<field name="model"/>', ''),
})
- @mute_logger('odoo.addons.base.models.ir_ui_view')
+ @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models')
def test_context_in_subview_with_parent(self):
arch = """
<form string="View">
@@ -1223,7 +1223,7 @@ class TestViews(ViewCase):
'arch': arch % ('', '<field name="model"/>'),
})
- @mute_logger('odoo.addons.base.models.ir_ui_view')
+ @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models')
def test_context_in_subsubview_with_parent(self):
arch = """
<form string="View">
@@ -1265,7 +1265,7 @@ class TestViews(ViewCase):
'arch': arch % ('', '', '<field name="model"/>'),
})
- @mute_logger('odoo.addons.base.models.ir_ui_view')
+ @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models')
def test_domain_in_view(self):
arch = """
<form string="View">
@@ -1285,7 +1285,7 @@ class TestViews(ViewCase):
'arch': arch % '',
})
- @mute_logger('odoo.addons.base.models.ir_ui_view')
+ @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models')
def test_domain_in_subview(self):
arch = """
<form string="View">
@@ -1316,7 +1316,7 @@ class TestViews(ViewCase):
'arch': arch % ('<field name="model"/>', ''),
})
- @mute_logger('odoo.addons.base.models.ir_ui_view')
+ @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models')
def test_domain_in_subview_with_parent(self):
arch = """
<form string="View">
@@ -1347,7 +1347,7 @@ class TestViews(ViewCase):
'arch': arch % ('', '<field name="model"/>'),
})
- @mute_logger('odoo.addons.base.models.ir_ui_view')
+ @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models')
def test_domain_on_field_in_view(self):
field = self.env['ir.ui.view']._fields['inherit_id']
self.patch(field, 'domain', "[('model', '=', model)]")
@@ -1370,7 +1370,7 @@ class TestViews(ViewCase):
'arch': arch % '',
})
- @mute_logger('odoo.addons.base.ir.ir_ui_view')
+ @mute_logger('odoo.addons.base.ir.ir_ui_view', 'odoo.models')
def test_domain_on_field_in_subview(self):
field = self.env['ir.ui.view']._fields['inherit_id']
self.patch(field, 'domain', "[('model', '=', model)]")
@@ -1404,7 +1404,7 @@ class TestViews(ViewCase):
'arch': arch % ('<field name="model"/>', ''),
})
- @mute_logger('odoo.addons.base.ir.ir_ui_view')
+ @mute_logger('odoo.addons.base.ir.ir_ui_view', 'odoo.models')
def test_domain_on_field_in_subview_with_parent(self):
field = self.env['ir.ui.view']._fields['inherit_id']
self.patch(field, 'domain', "[('model', '=', parent.model)]")
@@ -1438,7 +1438,7 @@ class TestViews(ViewCase):
'arch': arch % ('', '<field name="model"/>'),
})
- @mute_logger('odoo.addons.base.ir.ir_ui_view')
+ @mute_logger('odoo.addons.base.ir.ir_ui_view', 'odoo.models')
def test_domain_on_field_in_noneditable_subview(self):
field = self.env['ir.ui.view']._fields['inherit_id']
self.patch(field, 'domain', "[('model', '=', model)]")
@@ -1466,7 +1466,7 @@ class TestViews(ViewCase):
'arch': arch % ' editable="bottom"',
})
- @mute_logger('odoo.addons.base.ir.ir_ui_view')
+ @mute_logger('odoo.addons.base.ir.ir_ui_view', 'odoo.models')
def test_domain_on_readonly_field_in_view(self):
field = self.env['ir.ui.view']._fields['inherit_id']
self.patch(field, 'domain', "[('model', '=', model)]")
@@ -1496,7 +1496,7 @@ class TestViews(ViewCase):
'arch': arch,
})
- @mute_logger('odoo.addons.base.ir.ir_ui_view')
+ @mute_logger('odoo.addons.base.ir.ir_ui_view', 'odoo.models')
def test_domain_on_readonly_field_in_subview(self):
field = self.env['ir.ui.view']._fields['inherit_id']
self.patch(field, 'domain', "[('model', '=', model)]")
@@ -1524,7 +1524,7 @@ class TestViews(ViewCase):
'arch': arch % '',
})
- @mute_logger('odoo.addons.base.ir.ir_ui_view')
+ @mute_logger('odoo.addons.base.ir.ir_ui_view', 'odoo.models')
def test_attrs_field(self):
arch = """
<form string="View">
@@ -1545,7 +1545,7 @@ class TestViews(ViewCase):
'arch': arch % '',
})
- @mute_logger('odoo.addons.base.models.ir_ui_view')
+ @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models')
def test_attrs_subfield(self):
arch = """
<form string="View">
@@ -1577,7 +1577,7 @@ class TestViews(ViewCase):
'arch': arch % ('<field name="model"/>', ''),
})
- @mute_logger('odoo.addons.base.models.ir_ui_view')
+ @mute_logger('odoo.addons.base.models.ir_ui_view', 'odoo.models')
def test_attrs_subfield_with_parent(self):
arch = """
<form string="View">
diff --git a/odoo/fields.py b/odoo/fields.py
index 1efc86de888..b02564dd908 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -161,6 +161,7 @@ class Field(MetaField('DummyField', (object,), {})):
:param compute_sudo: whether the field should be recomputed as superuser
to bypass access rights (boolean, by default ``False``)
+ Note that this has no effects on non-stored computed fields
The methods given for ``compute``, ``inverse`` and ``search`` are model
methods. Their signature is shown in the following example::
diff --git a/odoo/models.py b/odoo/models.py
index e49f98ee0d8..439b8423f09 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -1109,6 +1109,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):
except ValidationError as e:
raise
except Exception as e:
+ _logger.exception('Exception while validating constraint')
raise ValidationError("%s\n\n%s" % (_("Error while validating constraint"), tools.ustr(e)))
@api.model
diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py
index f5518fead94..170c2b971e8 100644
--- a/odoo/osv/expression.py
+++ b/odoo/osv/expression.py
@@ -262,7 +262,7 @@ def combine(operator, unit, zero, domains):
result += normalize_domain(domain)
count += 1
result = [operator] * (count - 1) + result
- return result
+ return result or unit
def AND(domains):