diff --git a/addons/account/models/account_bank_statement.py b/addons/account/models/account_bank_statement.py
index 3b9267deb9c..1ee32068a00 100644
--- a/addons/account/models/account_bank_statement.py
+++ b/addons/account/models/account_bank_statement.py
@@ -795,7 +795,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 29557781875..8a39428c162 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -504,9 +504,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:
@@ -534,6 +534,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):
@@ -1050,7 +1054,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 d14b95be8e5..b945a8e3584 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 4be1ac19bd7..8bac68560cc 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')
@@ -811,6 +812,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
@@ -906,7 +912,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!'))
@@ -1500,9 +1506,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 = rec.env['account.move'].create(
@@ -1552,6 +1558,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/views/account_view.xml b/addons/account/views/account_view.xml
index 25a153a54d1..e92b0b73443 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -621,7 +621,7 @@
-
+ form
diff --git a/addons/account/views/report_invoice.xml b/addons/account/views/report_invoice.xml
index 07973558ab0..ade9b2738d7 100644
--- a/addons/account/views/report_invoice.xml
+++ b/addons/account/views/report_invoice.xml
@@ -125,18 +125,23 @@
-
diff --git a/doc/cla/corporate/metalsartigan.md b/doc/cla/corporate/metalsartigan.md
new file mode 100644
index 00000000000..027d794fcaa
--- /dev/null
+++ b/doc/cla/corporate/metalsartigan.md
@@ -0,0 +1,15 @@
+Canada, 2017-3-24
+
+Métal Sartigan 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,
+
+Jérôme Thériault jerther@gmail.com https://github.com/jerther
+
+List of contributors:
+
+Jérôme Thériault jerther@gmail.com https://github.com/jerther
diff --git a/doc/cla/corporate/ontruck.md b/doc/cla/corporate/ontruck.md
new file mode 100644
index 00000000000..baf0b0a2df4
--- /dev/null
+++ b/doc/cla/corporate/ontruck.md
@@ -0,0 +1,15 @@
+Spain, 2017-03-30
+
+OnTruck 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,
+
+Iñigo Juantegui
+
+List of contributors:
+
+Antonio Espinosa https://github.com/antespi
diff --git a/doc/cla/corporate/tecnativa.md b/doc/cla/corporate/tecnativa.md
new file mode 100644
index 00000000000..72a73cbbe28
--- /dev/null
+++ b/doc/cla/corporate/tecnativa.md
@@ -0,0 +1,22 @@
+Spain, 2017-04-04
+
+Tecnativa S.L. 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,
+
+Pedro M. Baeza pedro.baeza@tecnativa.com https://github.com/pedrobaeza
+
+List of contributors:
+
+Pedro M. Baeza pedro.baeza@tecnativa.com https://github.com/pedrobaeza
+Rafael Blasco rafael.blasco@tecnativa.com https://github.com/rafaelbn
+Sergio Teruel sergio.teruel@tecnativa.com https://github.com/sergio-teruel
+Carlos Dauden carlos.dauden@tecnativa.com https://github.com/carlosdauden
+Jairo Llopis jairo.llopis@tecnativa.com https://github.com/yajo
+Vicent Cubells vicent.cubells@tecnativa.com https://github.com/cubells
+Luis Montalba luis.montalba@tecnativa.com https://github.com/luismontalba
+David Vidal david.vidal@tecnativa.com https://github.com/chienandalu
diff --git a/doc/cla/corporate/tvtmarine-automation.md b/doc/cla/corporate/tvtmarine-automation.md
index a9a365d33ee..8b5b59d501b 100644
--- a/doc/cla/corporate/tvtmarine-automation.md
+++ b/doc/cla/corporate/tvtmarine-automation.md
@@ -1,4 +1,4 @@
-Vietnam, 2016-11-30
+Vietnam, 2017-04-03
T.V.T Marine Automation agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
@@ -12,4 +12,6 @@ David Tran david.tran@tvtmarine.com https://github.com/tvtma/
List of contributors:
-David Tran david.tran@tvtmarine.com https://github.com/tvtma/
\ No newline at end of file
+David Tran david.tran@tvtmarine.com https://github.com/davidtranhp
+Long Do dhlong.1209@gmail.com https://github.com/hoanglong87
+Hao Hoang hao.hoang@ma.tvtmarine.com https://github.com/hoanghao2001
\ No newline at end of file
diff --git a/doc/cla/individual/monami7001.md b/doc/cla/individual/monami7001.md
new file mode 100644
index 00000000000..4549f0e8026
--- /dev/null
+++ b/doc/cla/individual/monami7001.md
@@ -0,0 +1,9 @@
+Azer, 2017-03-26
+
+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,
+
+GHADHOUN AZER ezer1989@gmail.com https://github.com/monami7001
diff --git a/doc/cla/individual/ttpphuctran.md b/doc/cla/individual/ttpphuctran.md
new file mode 100644
index 00000000000..2cbbdd58851
--- /dev/null
+++ b/doc/cla/individual/ttpphuctran.md
@@ -0,0 +1,11 @@
+Vietnam, 2017-04-10
+
+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,
+
+Tran Thanh Phuc ttp.phuctran@gmail.com https://github.com/ttpphuctran
diff --git a/doc/cla/sign-cla.md b/doc/cla/sign-cla.md
index 62de420d0e8..247961f63bc 100644
--- a/doc/cla/sign-cla.md
+++ b/doc/cla/sign-cla.md
@@ -14,7 +14,7 @@ requirement is explained in the [FAQ](#faq).
`odony`, the file would be `doc/cla/individual/odony.md`. The file should
contain:
- ```
+```
,
I hereby agree to the terms of the Odoo Individual Contributor License
@@ -50,7 +50,7 @@ signature is merged.
If the name of the company is Odoo, the file would be
`doc/cla/corporate/odoo.md`. The file should contain:
- ```
+```
, agrees to the terms of the Odoo Corporate Contributor License
diff --git a/doc/reference/data.rst b/doc/reference/data.rst
index 20172fa063b..c8ebf864a88 100644
--- a/doc/reference/data.rst
+++ b/doc/reference/data.rst
@@ -58,7 +58,7 @@ following attributes:
Requires an :term:`external id`, defaults to ``True``.
``field``
-'''''''''
+----------
Each record can be composed of ``field`` tags, defining values to set when
creating the record. A ``record`` with no ``field`` will use all default
diff --git a/doc/setup/enterprise.rst b/doc/setup/enterprise.rst
index c853fa7c697..a613e0c8e39 100644
--- a/doc/setup/enterprise.rst
+++ b/doc/setup/enterprise.rst
@@ -15,7 +15,7 @@ In any case the basic guidelines are:
* Shutdown your server
-* Upgrade the web module
+* Install the web_enterprise module
* Restart your server
@@ -45,7 +45,7 @@ On Linux, using an installer
.. code-block:: console
- $ python /usr/bin/odoo.py -d -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 62749754be3..4c64a172057 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__)
@@ -348,6 +349,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.'),
]
@@ -508,6 +528,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]
@@ -521,6 +545,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:
@@ -563,9 +604,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'])
@@ -619,12 +657,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
@@ -800,6 +832,9 @@ class IrModelFields(models.Model):
def _instanciate(self, field_data, partial):
""" Return a field instance corresponding to parameters ``field_data``. """
attrs = self._instanciate_attrs(field_data, partial)
+ if partial and not attrs:
+ # field not instanciable yet, ignore.
+ return None
return fields.Field.by_type[field_data['ttype']](**attrs)
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 db1e17863d2..ae8863eee4d 100644
--- a/odoo/addons/base/res/res_users.py
+++ b/odoo/addons/base/res/res_users.py
@@ -745,7 +745,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 257fb6f998f..9edf4a16114 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 c3ba71ab301..03b5d7a1a1c 100644
--- a/odoo/models.py
+++ b/odoo/models.py
@@ -570,7 +570,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 48cc0e8a6e1..ca4998fd254 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 be2977c131e..398585ba239 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'