diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 0915e4c9f90..8f685a80b66 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -787,7 +787,7 @@ class AccountBankStatementLine(models.Model):
or self.statement_id.journal_id.default_debit_account_id.id,
'credit': amount < 0 and -amount or 0.0,
'debit': amount > 0 and amount or 0.0,
- 'statement_id': self.statement_id.id,
+ 'statement_line_id': self.id,
'currency_id': statement_currency != company_currency and statement_currency.id or (st_line_currency != company_currency and st_line_currency.id or False),
'amount_currency': amount_currency,
}
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 50a6102b0c8..dd3f32f25f2 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -498,9 +498,9 @@ class AccountInvoice(models.Model):
else:
account_id = pay_account.id
payment_term_id = p.property_supplier_payment_term_id.id
- addr = self.partner_id.address_get(['delivery'])
- fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, delivery_id=addr['delivery'])
+ delivery_partner_id = self.get_delivery_partner_id()
+ fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, delivery_id=delivery_partner_id)
# If partner has no warning, check its company
if p.invoice_warn == 'no-message' and p.parent_id:
@@ -528,6 +528,10 @@ class AccountInvoice(models.Model):
return {'domain': {'partner_bank_id': [('id', 'in', bank_ids.ids)]}}
return {}
+ @api.multi
+ def get_delivery_partner_id(self):
+ self.ensure_one()
+ return self.partner_id.address_get(['delivery'])['delivery']
@api.onchange('journal_id')
def _onchange_journal_id(self):
@@ -1039,7 +1043,7 @@ class AccountInvoice(models.Model):
values['invoice_line_ids'] = self._refund_cleanup_lines(invoice.invoice_line_ids)
- tax_lines = filter(lambda l: l.manual, invoice.tax_line_ids)
+ tax_lines = invoice.tax_line_ids
values['tax_line_ids'] = self._refund_cleanup_lines(tax_lines)
if journal_id:
diff --git a/addons/account/models/account_journal_dashboard.py b/addons/account/models/account_journal_dashboard.py
index d45f61fe50d..220aebf4321 100644
--- a/addons/account/models/account_journal_dashboard.py
+++ b/addons/account/models/account_journal_dashboard.py
@@ -304,11 +304,13 @@ class account_journal(models.Model):
'default_type': invoice_type,
'type': invoice_type
})
+
[action] = self.env.ref('account.%s' % action_name).read()
action['context'] = ctx
action['domain'] = self._context.get('use_domain', [])
- action['views'] = False
- action['view_id'] = False
+ if action_name in ['action_bank_statement_tree', 'action_view_bank_statement_tree']:
+ action['views'] = False
+ action['view_id'] = False
return action
@api.multi
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 827d9de7073..edd0a547b29 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -8,6 +8,7 @@ from odoo.exceptions import RedirectWarning, UserError, ValidationError
from odoo.tools.misc import formatLang
from odoo.tools import float_is_zero, float_compare
from odoo.tools.safe_eval import safe_eval
+import odoo.addons.decimal_precision as dp
from lxml import etree
#----------------------------------------------------------
@@ -244,9 +245,9 @@ class AccountMove(models.Model):
journal_id=journal_id)
reversed_moves |= reversed_move
#unreconcile all lines reversed
- aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile)
+ aml = ac_move.line_ids.filtered(lambda x: x.account_id.reconcile or x.account_id.internal_type == 'liquidity')
aml.remove_move_reconcile()
- #reconcile together the reconciliable aml and their newly created counterpart
+ #reconcile together the reconciliable and the liquidity aml and their newly created counterpart
for account in [x.account_id for x in aml]:
to_rec = aml.filtered(lambda y: y.account_id == account)
to_rec |= reversed_move.line_ids.filtered(lambda y: y.account_id == account)
@@ -370,7 +371,7 @@ class AccountMoveLine(models.Model):
self.counterpart = ",".join(counterpart)
name = fields.Char(string="Label")
- quantity = fields.Float(digits=(16, 2),
+ quantity = fields.Float(digits=dp.get_precision('Product Unit of Measure'),
help="The optional quantity expressed by this line, eg: number of product sold. The quantity is not a legal requirement but is very useful for some reports.")
product_uom_id = fields.Many2one('product.uom', string='Unit of Measure')
product_id = fields.Many2one('product.product', string='Product')
@@ -780,7 +781,7 @@ class AccountMoveLine(models.Model):
"""
for datum in data:
if len(datum['mv_line_ids']) >= 1 or len(datum['mv_line_ids']) + len(datum['new_mv_line_dicts']) >= 2:
- self.process_reconciliation(datum['mv_line_ids'], datum['new_mv_line_dicts'])
+ self.env['account.move.line'].browse(datum['mv_line_ids']).process_reconciliation(datum['new_mv_line_dicts'])
if datum['type'] == 'partner':
partners = self.env['res.partner'].browse(datum['id'])
@@ -820,6 +821,11 @@ class AccountMoveLine(models.Model):
def _get_pair_to_reconcile(self):
#field is either 'amount_residual' or 'amount_residual_currency' (if the reconciled account has a secondary currency set)
field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
+ #reconciliation on bank accounts are special cases as we don't want to set them as reconciliable
+ #but we still want to reconcile entries that are reversed together in order to clear those lines
+ #in the bank reconciliation report.
+ if not self[0].account_id.reconcile and self[0].account_id.internal_type == 'liquidity':
+ field = 'balance'
rounding = self[0].company_id.currency_id.rounding
if self[0].currency_id and all([x.amount_currency and x.currency_id == self[0].currency_id for x in self]):
#or if all lines share the same currency
@@ -915,7 +921,7 @@ class AccountMoveLine(models.Model):
raise UserError(_('To reconcile the entries company should be the same for all entries!'))
if len(set(all_accounts)) > 1:
raise UserError(_('Entries are not of the same account!'))
- if not all_accounts[0].reconcile:
+ if not (all_accounts[0].reconcile or all_accounts[0].internal_type == 'liquidity'):
raise UserError(_('The account %s (%s) is not marked as reconciliable !') % (all_accounts[0].name, all_accounts[0].code))
if len(partners) > 1:
raise UserError(_('The partner has to be the same on all lines for receivable and payable accounts!'))
@@ -1438,9 +1444,9 @@ class AccountPartialReconcile(models.Model):
for rec in self:
if not rec.company_id.currency_exchange_journal_id:
raise UserError(_("You should configure the 'Exchange Rate Journal' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
- if not self.company_id.income_currency_exchange_account_id.id:
+ if not rec.company_id.income_currency_exchange_account_id.id:
raise UserError(_("You should configure the 'Gain Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
- if not self.company_id.expense_currency_exchange_account_id.id:
+ if not rec.company_id.expense_currency_exchange_account_id.id:
raise UserError(_("You should configure the 'Loss Exchange Rate Account' in the accounting settings, to manage automatically the booking of accounting entries related to differences between exchange rates."))
move_vals = {'journal_id': rec.company_id.currency_exchange_journal_id.id}
@@ -1500,6 +1506,8 @@ class AccountPartialReconcile(models.Model):
#amount is the current cash_basis amount minus the one before the reconciliation
amount = line.balance * percentage_after - line.balance * percentage_before
rounded_amt = line.company_id.currency_id.round(amount)
+ if float_is_zero(rounded_amt, precision_rounding=line.company_id.currency_id.rounding):
+ continue
if line.tax_line_id and line.tax_line_id.use_cash_basis:
if not newly_created_move:
newly_created_move = self._create_tax_basis_move()
diff --git a/addons/account/report/account_partner_ledger.py b/addons/account/report/account_partner_ledger.py
index c01d22cac04..ba2503d41a9 100644
--- a/addons/account/report/account_partner_ledger.py
+++ b/addons/account/report/account_partner_ledger.py
@@ -11,6 +11,7 @@ class ReportPartnerLedger(models.AbstractModel):
def _lines(self, data, partner):
full_account = []
+ currency = self.env['res.currency']
query_get_data = self.env['account.move.line'].with_context(data['form'].get('used_context', {}))._query_get()
reconcile_clause = "" if data['form']['reconciled'] else ' AND "account_move_line".reconciled = false '
params = [partner.id, tuple(data['computed']['move_state']), tuple(data['computed']['account_ids'])] + query_get_data[2]
@@ -40,6 +41,7 @@ class ReportPartnerLedger(models.AbstractModel):
)
sum += r['debit'] - r['credit']
r['progress'] = sum
+ r['currency_id'] = currency.browse(r.get('currency_id'))
full_account.append(r)
return full_account
diff --git a/addons/account/static/src/xml/account_reconciliation.xml b/addons/account/static/src/xml/account_reconciliation.xml
index 6796c7fb2f6..bad933225a2 100644
--- a/addons/account/static/src/xml/account_reconciliation.xml
+++ b/addons/account/static/src/xml/account_reconciliation.xml
@@ -304,10 +304,10 @@
- -u web --stop-after-init
+ $ python /usr/bin/odoo.py -d -i web_enterprise --stop-after-init
* You should be able to connect to your Odoo Enterprise instance using your usual mean of identification.
You can then link your database with your Odoo Enterprise Subscription by entering the code you received
@@ -61,11 +61,11 @@ have your own favourite. You may need to adapt sections to your usual workflow.
* Shutdown your server
* Backup your community database
* Update the ``--addons-path`` parameter of your launch command (see :ref:`setup/install/source`)
-* Update the web module by using
+* Install the web_enterprise module by using
.. code-block:: console
- $ -d -u web --stop-after-init
+ $ -d -i web_enterprise --stop-after-init
Depending on the size of your database, this may take some time.
@@ -97,7 +97,7 @@ On Windows
.. code-block:: console
- $ odoo.exe -d -u web --stop-after-init
+ $ odoo.exe -d -i web_enterprise --stop-after-init
* No need to manually launch the server, the service is running.
You should be able to connect to your Odoo Enterprise instance using your usual
diff --git a/odoo/addons/base/ir/ir_filters.xml b/odoo/addons/base/ir/ir_filters.xml
index d21c6ad25d5..f46ec82c7f6 100644
--- a/odoo/addons/base/ir/ir_filters.xml
+++ b/odoo/addons/base/ir/ir_filters.xml
@@ -24,7 +24,7 @@
ir.filters.own.rule.delete
- [('create_uid', '=', user.id)]
+ [('user_id', '=', user.id)]
diff --git a/odoo/addons/base/ir/ir_model.py b/odoo/addons/base/ir/ir_model.py
index 8d87d73716e..3ea67a10296 100644
--- a/odoo/addons/base/ir/ir_model.py
+++ b/odoo/addons/base/ir/ir_model.py
@@ -9,6 +9,7 @@ from collections import defaultdict
from odoo import api, fields, models, SUPERUSER_ID, tools, _
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.modules.registry import Registry
+from odoo.osv import expression
from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__)
@@ -351,6 +352,25 @@ class IrModelFields(models.Model):
raise UserError(_("The Selection Options expression is not a valid Pythonic expression."
"Please provide an expression in the [('key','Label'), ...] format."))
+ @api.constrains('name', 'state')
+ def _check_name(self):
+ for field in self:
+ if field.state == 'manual' and not field.name.startswith('x_'):
+ raise ValidationError(_("Custom fields must have a name that starts with 'x_' !"))
+ try:
+ models.check_pg_name(field.name)
+ except ValidationError:
+ msg = _("Field names can only contain characters, digits and underscores (up to 63).")
+ raise ValidationError(msg)
+
+ @api.constrains('model', 'name')
+ def _unique_name(self):
+ # fix on stable branch (to be converted into an SQL constraint)
+ for field in self:
+ count = self.search_count([('model', '=', field.model), ('name', '=', field.name)])
+ if count > 1:
+ raise ValidationError(_("Field names must be unique per model."))
+
_sql_constraints = [
('size_gt_zero', 'CHECK (size>=0)', 'Size of the field cannot be negative.'),
]
@@ -511,6 +531,10 @@ class IrModelFields(models.Model):
This method prevents the modification/deletion of many2one fields
that have an inverse one2many, for instance.
"""
+ self = self.filtered(lambda record: record.state == 'manual')
+ if not self:
+ return
+
for record in self:
model = self.env[record.model]
field = model._fields[record.name]
@@ -524,6 +548,23 @@ class IrModelFields(models.Model):
msg = _("The field '%s' cannot be removed because the field '%s' depends on it.")
raise UserError(msg % (field, model._field_inverses[field][0]))
+ # remove fields from registry, and check that views are not broken
+ fields = [self.env[record.model]._pop_field(record.name) for record in self]
+ domain = expression.OR([('arch_db', 'like', record.name)] for record in self)
+ views = self.env['ir.ui.view'].search(domain)
+ try:
+ for view in views:
+ view._check_xml()
+ except Exception:
+ raise UserError("\n".join([
+ _("Cannot rename/delete fields that are still present in views:"),
+ _("Fields:") + " " + ", ".join(map(str, fields)),
+ _("View:") + " " + view.name,
+ ]))
+ finally:
+ # the registry has been modified, restore it
+ self.pool.setup_models(self._cr)
+
@api.multi
def unlink(self):
if not self:
@@ -566,9 +607,6 @@ class IrModelFields(models.Model):
res = super(IrModelFields, self).create(vals)
if vals.get('state', 'manual') == 'manual':
- if not vals['name'].startswith('x_'):
- raise UserError(_("Custom fields must have a name that starts with 'x_' !"))
-
if vals.get('relation') and not self.env['ir.model'].search([('model', '=', vals['relation'])]):
raise UserError(_("Model %s does not exist!") % vals['relation'])
@@ -622,12 +660,6 @@ class IrModelFields(models.Model):
item._prepare_update()
if column_rename:
raise UserError(_('Can only rename one field at a time!'))
- if vals['name'] in obj._fields:
- raise UserError(_('Cannot rename field to %s, because that field already exists!') % vals['name'])
- if vals.get('state', 'manual') == 'manual' and not vals['name'].startswith('x_'):
- raise UserError(_('New field name must still start with x_ , because it is a custom field!'))
- if '\'' in vals['name'] or '"' in vals['name'] or ';' in vals['name']:
- raise ValueError('Invalid character in column name')
column_rename = (obj._table, item.name, vals['name'], item.index)
# We don't check the 'state', because it might come from the context
diff --git a/odoo/addons/base/ir/ir_qweb/fields.py b/odoo/addons/base/ir/ir_qweb/fields.py
index c1d525c83b3..e40f64c3937 100644
--- a/odoo/addons/base/ir/ir_qweb/fields.py
+++ b/odoo/addons/base/ir/ir_qweb/fields.py
@@ -330,7 +330,7 @@ class MonetaryConverter(models.AbstractModel):
if not options.get('display_currency') and field.type == 'monetary' and field.currency_field:
options['display_currency'] = record[field.currency_field]
- return self.value_to_html(record[field_name], options)
+ return super(MonetaryConverter, self).record_to_html(record, field_name, options)
TIMEDELTA_UNITS = (
('year', 3600 * 24 * 365),
diff --git a/odoo/addons/base/res/res_company_view.xml b/odoo/addons/base/res/res_company_view.xml
index 95bafe4f334..4746deeb1b1 100644
--- a/odoo/addons/base/res/res_company_view.xml
+++ b/odoo/addons/base/res/res_company_view.xml
@@ -1,7 +1,7 @@
-
+ res.company.form
diff --git a/odoo/addons/base/res/res_users.py b/odoo/addons/base/res/res_users.py
index e92aaf5315e..da025eeca78 100644
--- a/odoo/addons/base/res/res_users.py
+++ b/odoo/addons/base/res/res_users.py
@@ -744,7 +744,7 @@ class GroupsView(models.Model):
xml = E.field(E.group(*(xml1), col="2"), E.group(*(xml2), col="4"), name="groups_id", position="replace")
xml.addprevious(etree.Comment("GENERATED AUTOMATICALLY BY GROUPS"))
xml_content = etree.tostring(xml, pretty_print=True, xml_declaration=True, encoding="utf-8")
- view.with_context(lang=None).write({'arch': xml_content})
+ view.with_context(lang=None).write({'arch': xml_content, 'arch_fs': False})
def get_application_groups(self, domain):
""" Return the non-share groups that satisfy ``domain``. """
diff --git a/odoo/addons/base/tests/test_expression.py b/odoo/addons/base/tests/test_expression.py
index 932268d717c..104c4a6dfe1 100644
--- a/odoo/addons/base/tests/test_expression.py
+++ b/odoo/addons/base/tests/test_expression.py
@@ -683,8 +683,8 @@ class TestAutoJoin(TransactionCase):
self.assertEqual(partners, p_aa,
"_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..'): incorrect result")
# Test produced queries
- self.assertEqual(len(self.query_list), 3,
- "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') should produce 3 queries (1 in res_partner_bank, 2 on res_partner)")
+ self.assertEqual(len(self.query_list), 2,
+ "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') should produce 2 queries (1 in res_partner_bank, 1 on res_partner)")
sql_query = self.query_list[0].get_sql()
self.assertIn('res_partner_bank', sql_query[0],
"_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') first query incorrect main table")
@@ -692,16 +692,16 @@ class TestAutoJoin(TransactionCase):
expected = "%s::text like %s" % (unaccent('"res_partner_bank"."sanitized_acc_number"'), unaccent('%s'))
self.assertIn(expected, sql_query[1],
"_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') first query incorrect where condition")
-
+
self.assertEqual(['%' + name_test + '%'], sql_query[2],
"_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') first query incorrect parameter")
- sql_query = self.query_list[2].get_sql()
+ sql_query = self.query_list[1].get_sql()
self.assertIn('res_partner', sql_query[0],
- "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') third query incorrect main table")
+ "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') second query incorrect main table")
self.assertIn('"res_partner"."id" in (%s)', sql_query[1],
- "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') third query incorrect where condition")
+ "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') second query incorrect where condition")
self.assertIn(p_aa.id, sql_query[2],
- "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') third query incorrect parameter")
+ "_auto_join off: ('bank_ids.sanitized_acc_number', 'like', '..') second query incorrect parameter")
# Do: cascaded one2many without _auto_join
self._reinit_mock()
@@ -710,8 +710,8 @@ class TestAutoJoin(TransactionCase):
self.assertEqual(partners, p_a + p_b,
"_auto_join off: ('child_ids.bank_ids.id', 'in', [..]): incorrect result")
# Test produced queries
- self.assertEqual(len(self.query_list), 5,
- "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]) should produce 5 queries (1 in res_partner_bank, 4 on res_partner)")
+ self.assertEqual(len(self.query_list), 3,
+ "_auto_join off: ('child_ids.bank_ids.id', 'in', [..]) should produce 3 queries (1 in res_partner_bank, 2 on res_partner)")
# Do: one2many with _auto_join
patch_auto_join(partner_obj, 'bank_ids', True)
@@ -943,7 +943,7 @@ class TestAutoJoin(TransactionCase):
self.assertLessEqual(p_a + p_b, partners,
"_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') incorrect result")
# Test produced queries
- self.assertEqual(len(self.query_list), 5,
+ self.assertEqual(len(self.query_list), 4,
"_auto_join off: ('child_ids.state_id.country_id.code', 'like', '..') number of queries incorrect")
# Do: ('child_ids.state_id.country_id.code', 'like', '..') with _auto_join
diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py
index 74d220b05bb..9e14f000676 100644
--- a/odoo/addons/base/tests/test_ir_actions.py
+++ b/odoo/addons/base/tests/test_ir_actions.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo.exceptions import ValidationError
+import odoo
+from odoo.exceptions import UserError, ValidationError
from odoo.tools import mute_logger
import odoo.tests.common as common
@@ -181,3 +182,112 @@ class TestServerActions(TestServerActionsBase):
self.action.write({
'child_ids': [(6, 0, [self.action.id])]
})
+
+
+class TestCustomFields(common.TransactionCase):
+ MODEL = 'res.partner'
+
+ def setUp(self):
+ # use a test cursor instead of a real cursor
+ registry = odoo.registry()
+ registry.enter_test_mode()
+ fnames = set(registry[self.MODEL]._fields)
+
+ @self.addCleanup
+ def callback():
+ registry.leave_test_mode()
+ # the tests may have modified the registry, reset it
+ with registry.cursor() as cr:
+ registry.clear_manual_fields()
+ registry.setup_models(cr)
+ assert set(registry[self.MODEL]._fields) == fnames
+
+ super(TestCustomFields, self).setUp()
+
+ # do not reload the registry after removing a field
+ self.env = self.env(context={'_force_unlink': True})
+
+ def create_field(self, name):
+ """ create a custom field and return it """
+ model = self.env['ir.model'].search([('model', '=', self.MODEL)])
+ field = self.env['ir.model.fields'].create({
+ 'model_id': model.id,
+ 'name': name,
+ 'field_description': name,
+ 'ttype': 'char',
+ })
+ self.assertIn(name, self.env[self.MODEL]._fields)
+ return field
+
+ def create_view(self, name):
+ """ create a view with the given field name """
+ return self.env['ir.ui.view'].create({
+ 'name': 'yet another view',
+ 'model': self.MODEL,
+ 'arch': '' % name,
+ })
+
+ def test_create_custom(self):
+ """ custom field names must be start with 'x_' """
+ with self.assertRaises(ValidationError):
+ self.create_field('foo')
+
+ def test_rename_custom(self):
+ """ custom field names must be start with 'x_' """
+ field = self.create_field('x_foo')
+ with self.assertRaises(ValidationError):
+ field.name = 'foo'
+
+ def test_create_valid(self):
+ """ field names must be valid pg identifiers """
+ with self.assertRaises(ValidationError):
+ self.create_field('x_foo bar')
+
+ def test_rename_valid(self):
+ """ field names must be valid pg identifiers """
+ field = self.create_field('x_foo')
+ with self.assertRaises(ValidationError):
+ field.name = 'x_foo bar'
+
+ def test_create_unique(self):
+ """ one cannot create two fields with the same name on a given model """
+ self.create_field('x_foo')
+ with self.assertRaises(ValidationError):
+ self.create_field('x_foo')
+
+ def test_rename_unique(self):
+ """ one cannot create two fields with the same name on a given model """
+ field1 = self.create_field('x_foo')
+ field2 = self.create_field('x_bar')
+ with self.assertRaises(ValidationError):
+ field2.name = field1.name
+
+ def test_remove_without_view(self):
+ """ try removing a custom field that does not occur in views """
+ field = self.create_field('x_foo')
+ field.unlink()
+
+ def test_rename_without_view(self):
+ """ try renaming a custom field that does not occur in views """
+ field = self.create_field('x_foo')
+ field.name = 'x_bar'
+
+ def test_remove_with_view(self):
+ """ try removing a custom field that occurs in a view """
+ field = self.create_field('x_foo')
+ self.create_view('x_foo')
+
+ # try to delete the field, this should fail but not modify the registry
+ with self.assertRaises(UserError):
+ field.unlink()
+ self.assertIn('x_foo', self.env[self.MODEL]._fields)
+
+ def test_rename_with_view(self):
+ """ try renaming a custom field that occurs in a view """
+ field = self.create_field('x_foo')
+ self.create_view('x_foo')
+
+ # try to delete the field, this should fail but not modify the registry
+ with self.assertRaises(UserError):
+ field.name = 'x_bar'
+ self.assertIn('x_foo', self.env[self.MODEL]._fields)
diff --git a/odoo/addons/test_new_api/ir.model.access.csv b/odoo/addons/test_new_api/ir.model.access.csv
index 4f05bc6dae3..a368416e743 100644
--- a/odoo/addons/test_new_api/ir.model.access.csv
+++ b/odoo/addons/test_new_api/ir.model.access.csv
@@ -16,3 +16,4 @@ access_test_new_api_related,access_test_new_api_related,model_test_new_api_relat
access_test_new_api_company,access_test_new_api_company,model_test_new_api_company,,1,1,1,1
access_test_new_api_company_attr,access_test_new_api_company_attr,model_test_new_api_company_attr,,1,1,1,1
access_test_new_api_compute_inverse,access_test_new_api_compute_inverse,model_test_new_api_compute_inverse,,1,1,1,1
+access_test_new_api_recursive,access_test_new_api_recursive,model_test_new_api_recursive,,1,1,1,1
diff --git a/odoo/addons/test_new_api/models.py b/odoo/addons/test_new_api/models.py
index d0871a51dcf..c84827d47ce 100644
--- a/odoo/addons/test_new_api/models.py
+++ b/odoo/addons/test_new_api/models.py
@@ -348,4 +348,20 @@ class CompanyDependentAttribute(models.Model):
@api.depends('quantity', 'company.foo')
def _compute_bar(self):
for record in self:
- record.bar = (record.company.foo or '') * record.quantity
\ No newline at end of file
+ record.bar = (record.company.foo or '') * record.quantity
+
+
+class ComputeRecursive(models.Model):
+ _name = 'test_new_api.recursive'
+
+ name = fields.Char(required=True)
+ parent = fields.Many2one('test_new_api.recursive')
+ display_name = fields.Char(compute='_compute_display_name', store=True)
+
+ @api.depends('name', 'parent.display_name')
+ def _compute_display_name(self):
+ for rec in self:
+ if rec.parent:
+ rec.display_name = rec.parent.display_name + " / " + rec.name
+ else:
+ rec.display_name = rec.name
\ No newline at end of file
diff --git a/odoo/addons/test_new_api/tests/test_new_fields.py b/odoo/addons/test_new_api/tests/test_new_fields.py
index ca61afa413a..378ace4af9b 100644
--- a/odoo/addons/test_new_api/tests/test_new_fields.py
+++ b/odoo/addons/test_new_api/tests/test_new_fields.py
@@ -179,6 +179,29 @@ class TestFields(common.TransactionCase):
cath.parent = finn
self.assertEqual(ewan.display_name, "Gabriel / Finnley / Catherine / Ewan")
+ def test_12_recursive_recompute(self):
+ """ test recomputation on recursively dependent field """
+ a = self.env['test_new_api.recursive'].create({'name': 'A'})
+ b = self.env['test_new_api.recursive'].create({'name': 'B', 'parent': a.id})
+ c = self.env['test_new_api.recursive'].create({'name': 'C', 'parent': b.id})
+ d = self.env['test_new_api.recursive'].create({'name': 'D', 'parent': c.id})
+ self.assertEqual(a.display_name, 'A')
+ self.assertEqual(b.display_name, 'A / B')
+ self.assertEqual(c.display_name, 'A / B / C')
+ self.assertEqual(d.display_name, 'A / B / C / D')
+
+ b.parent = False
+ self.assertEqual(a.display_name, 'A')
+ self.assertEqual(b.display_name, 'B')
+ self.assertEqual(c.display_name, 'B / C')
+ self.assertEqual(d.display_name, 'B / C / D')
+
+ b.name = 'X'
+ self.assertEqual(a.display_name, 'A')
+ self.assertEqual(b.display_name, 'X')
+ self.assertEqual(c.display_name, 'X / C')
+ self.assertEqual(d.display_name, 'X / C / D')
+
def test_12_cascade(self):
""" test computed field depending on computed field """
message = self.env.ref('test_new_api.message_0_0')
diff --git a/odoo/fields.py b/odoo/fields.py
index d1d25edf05e..2b289227f9f 100644
--- a/odoo/fields.py
+++ b/odoo/fields.py
@@ -670,6 +670,7 @@ class Field(object):
model._field_triggers.add(field, (self, path_str))
elif path:
self.recursive = True
+ model._field_triggers.add(field, (self, '.'.join(path)))
############################################################################
#
diff --git a/odoo/models.py b/odoo/models.py
index f0a5cb16786..b91f5a81386 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -569,7 +569,7 @@ class BaseModel(object):
field = cls._fields.get(name)
if not field:
_logger.warning("method %s.%s: @constrains parameter %r is not a field name", cls._name, attr, name)
- if not (field.store or field.inverse):
+ elif not (field.store or field.inverse):
_logger.warning("method %s.%s: @constrains parameter %r is not writeable", cls._name, attr, name)
methods.append(func)
@@ -1938,7 +1938,7 @@ class BaseModel(object):
def _read_group_resolve_many2one_fields(self, data, fields):
many2onefields = {field['field'] for field in fields if field['type'] == 'many2one'}
for field in many2onefields:
- ids_set = {d[field] for d in data}
+ ids_set = {d[field] for d in data if d[field]}
m2o_records = self.env[self._fields[field].comodel_name].browse(ids_set)
data_dict = dict(m2o_records.name_get())
for d in data:
@@ -3839,7 +3839,8 @@ class BaseModel(object):
"""
self.ensure_one()
vals = self.copy_data(default)[0]
- new = self.create(vals)
+ # To avoid to create a translation in the lang of the user, copy_translation will do it
+ new = self.with_context(lang=None).create(vals)
self.copy_translations(new)
return new
diff --git a/odoo/modules/graph.py b/odoo/modules/graph.py
index aaf90ae8443..ff2f99188fe 100644
--- a/odoo/modules/graph.py
+++ b/odoo/modules/graph.py
@@ -80,7 +80,7 @@ class Graph(dict):
info = odoo.modules.module.load_information_from_description_file(module)
if info and info['installable']:
packages.append((module, info)) # TODO directly a dict, like in get_modules_with_version
- else:
+ elif module != 'studio_customization':
_logger.warning('module %s: not installable, skipped', module)
dependencies = dict([(p, info['depends']) for p, info in packages])
@@ -112,10 +112,7 @@ class Graph(dict):
unmet_deps = filter(lambda p: p not in self, dependencies[package])
_logger.error('module %s: Unmet dependencies: %s', package, ', '.join(unmet_deps))
- result = len(self) - len_graph
- if result != len(module_list):
- _logger.warning('Some modules were not loaded.')
- return result
+ return len(self) - len_graph
def __iter__(self):
diff --git a/odoo/modules/module.py b/odoo/modules/module.py
index ecf95be088b..d15df6dba59 100644
--- a/odoo/modules/module.py
+++ b/odoo/modules/module.py
@@ -302,7 +302,7 @@ def load_information_from_description_file(module, mod_path=None):
:param mod_path: Physical path of module, if not providedThe name of the module (sale, purchase, ...)
"""
if not mod_path:
- mod_path = get_module_path(module)
+ mod_path = get_module_path(module, downloaded=True)
manifest_file = module_manifest(mod_path)
if manifest_file:
# default values for descriptor
diff --git a/odoo/osv/expression.py b/odoo/osv/expression.py
index eea14988eb7..bef1cb1c523 100644
--- a/odoo/osv/expression.py
+++ b/odoo/osv/expression.py
@@ -876,8 +876,7 @@ class expression(object):
# Making search easier when there is a left operand as one2many or many2many
elif len(path) > 1 and field.store and field.type in ('many2many', 'one2many'):
right_ids = comodel.search([('.'.join(path[1:]), operator, right)]).ids
- table_ids = model.with_context(active_test=False).search([(path[0], 'in', right_ids)]).ids
- leaf.leaf = ('id', 'in', table_ids)
+ leaf.leaf = (path[0], 'in', right_ids)
push(leaf)
elif not field.store:
@@ -980,15 +979,16 @@ class expression(object):
rel_table, rel_id1, rel_id2 = field.relation, field.column1, field.column2
if operator in HIERARCHY_FUNCS:
- def _rec_convert(ids):
- if comodel == model:
- return ids
- return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator)
-
ids2 = to_ids(right, comodel)
dom = HIERARCHY_FUNCS[operator]('id', ids2, comodel)
ids2 = comodel.search(dom).ids
- push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), model))
+ if comodel == model:
+ push(create_substitution_leaf(leaf, ('id', 'in', ids2), model))
+ else:
+ subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2)
+ # avoid flattening of argument in to_sql()
+ subquery = cr.mogrify(subquery, [tuple(ids2)])
+ push(create_substitution_leaf(leaf, ('id', 'inselect', (subquery, [])), internal=True))
else:
call_null_m2m = True
if right is not False:
@@ -1014,8 +1014,11 @@ class expression(object):
operator = 'in' # operator changed because ids are directly related to main object
else:
call_null_m2m = False
- m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
- push(create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), model))
+ subop = 'not inselect' if operator in NEGATIVE_TERM_OPERATORS else 'inselect'
+ subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2)
+ # avoid flattening of argument in to_sql()
+ subquery = cr.mogrify(subquery, [tuple(filter(None, res_ids))])
+ push(create_substitution_leaf(leaf, ('id', subop, (subquery, [])), internal=True))
if call_null_m2m:
m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'