[MERGE] forward port branch 10.0 up to 799e7f7740

This commit is contained in:
Christophe Simonis
2017-01-04 19:43:24 +01:00
19 changed files with 121 additions and 63 deletions
@@ -13,7 +13,7 @@
</div>
</div>
<button class="btn btn-default js_automatic_reconciliation pull-right" title="Let odoo try to reconcile entries for the user">Automatic reconciliation</button>
<div class="oe_clear o_clear"></div>
<div class="oe_clear clearer"></div>
<div class="notification_area"></div>
<div class="interface_options oe_form o_form"></div>
<div class="reconciliation_lines_container"></div>
@@ -1,3 +1,5 @@
# -*- coding: utf-8 -*-
# flake8: noqa
import base_import_module
import ir_module
import ir_ui_view
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from operator import itemgetter
from openerp import api, models
class IrUiView(models.Model):
_inherit = 'ir.ui.view'
@api.model
def _validate_custom_views(self, model):
# views from imported modules should be considered as custom views
result = super(IrUiView, self)._validate_custom_views(model)
self._cr.execute("""
SELECT max(v.id)
FROM ir_ui_view v
LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
LEFT JOIN ir_module_module m ON (m.name = md.module)
WHERE m.imported = true
AND v.model = %s
AND v.active = true
GROUP BY coalesce(v.inherit_id, v.id)
""", [model])
ids = map(itemgetter(0), self._cr.fetchall())
views = self.with_context(load_all_views=True).browse(ids)
return views._check_xml() and result
+1 -1
View File
@@ -207,7 +207,7 @@ class Employee(models.Model):
today_date = datetime.datetime.utcnow().date()
today_start = fields.Datetime.to_string(today_date) # get the midnight of the current utc day
today_end = fields.Datetime.to_string(today_date + relativedelta(hours=23, minutes=59, seconds=59))
holidays = self.env['hr.holidays'].search([
holidays = self.env['hr.holidays'].sudo().search([
('employee_id', '!=', False),
('state', 'not in', ['cancel', 'refuse']),
('date_from', '<=', today_end),
+1 -1
View File
@@ -80,7 +80,7 @@ class PadCommon(models.AbstractModel):
if url:
try:
page = urllib2.urlopen('%s/export/html' % url).read()
mo = re.search('<body>(.*)</body>', page)
mo = re.search('<body>(.*)</body>', page, re.DOTALL)
if mo:
content = mo.group(1)
except:
@@ -43,7 +43,7 @@
<span t-field="l.discount"/>
</td>
<td class="text-right">
<span t-esc="', '.join(map(lambda x: x.name, l.invoice_line_tax_ids))"/>
<span t-esc="', '.join(map(lambda x: x.description or x.name, l.invoice_line_tax_ids))"/>
</td>
<td class="text-right" groups="sale.group_show_price_subtotal">
<span t-field="l.price_subtotal"
+6
View File
@@ -17,12 +17,18 @@ class AccountAnalyticLine(models.Model):
@api.multi
def write(self, values):
so_lines = self.mapped('so_line')
if values.get('task_id'):
task = self.env['project.task'].browse(values['task_id'])
values['so_line'] = task.sale_line_id.id or values.get('so_line', False)
for line in self:
values.update(line._get_timesheet_cost(values))
super(AccountAnalyticLine, line).write(values)
# Update delivered quantity on SO lines which are not linked to the analytic lines anymore
so_lines -= self.mapped('so_line')
if so_lines:
so_lines.with_context(force_so_lines=so_lines).sudo()._compute_analytic()
return True
def _get_timesheet_cost(self, values):
+1 -1
View File
@@ -389,7 +389,7 @@ class Picking(models.Model):
if self.partner_id:
if self.partner_id.picking_warn == 'no-message' and self.partner_id.parent_id:
partner = self.partner_id.parent_id
elif self.partner_id.picking_warn not in ('no-message', 'block') and partner.parent_id.picking_warn == 'block':
elif self.partner_id.picking_warn not in ('no-message', 'block') and self.partner_id.parent_id.picking_warn == 'block':
partner = self.partner_id.parent_id
else:
partner = self.partner_id
@@ -205,18 +205,20 @@ return Widget.extend({
all_zero = all_zero && (datapt.value === 0);
});
if (some_negative && !all_negative) {
return this.$el.append(QWeb.render('GraphView.error', {
this.$el.append(QWeb.render('GraphView.error', {
title: _t("Invalid data"),
description: _t("Pie chart cannot mix positive and negative numbers. " +
"Try to change your domain to only display positive results"),
}));
return;
}
if (all_zero) {
return this.$el.append(QWeb.render('GraphView.error', {
this.$el.append(QWeb.render('GraphView.error', {
title: _t("Invalid data"),
description: _t("Pie chart cannot display all zero numbers.. " +
"Try to change your domain to display positive results"),
}));
return;
}
if (this.groupbys.length) {
data = this.data.map(function (datapt) {
+1 -2
View File
@@ -165,8 +165,7 @@ class SaleOrder(models.Model):
""" Payment callback: validate the order and write transaction details in chatter """
# create draft invoice if transaction is ok
if transaction and transaction.state == 'done':
if self.state in ['draft', 'sent']:
self.sudo().action_confirm()
transaction._confirm_so()
message = _('Order paid by %s. Transaction: %s. Amount: %s.') % (transaction.partner_id.name, transaction.acquirer_reference, transaction.amount)
self.message_post(body=message)
return True
+11
View File
@@ -0,0 +1,11 @@
Spain, 2017-01-01
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,
Pin Zhang <zhangpin04@gmail.com> https://github.com/PinZhang
+1 -1
View File
@@ -649,7 +649,7 @@ class QWeb(object):
if hasattr(self, interpret_handler):
_logger.warning(
"Directive '%s' must be AST-compiled. Dynamic interpreter %s will ignored",
name, interpret_handler
mname, interpret_handler
)
return compile_handler(el, options)
+1 -1
View File
@@ -1111,7 +1111,7 @@ actual arch.
query = """SELECT max(v.id)
FROM ir_ui_view v
LEFT JOIN ir_model_data md ON (md.model = 'ir.ui.view' AND md.res_id = v.id)
WHERE md.module IS NULL
WHERE md.module NOT IN (SELECT name FROM ir_module_module)
AND v.model = %s
AND v.active = true
GROUP BY coalesce(v.inherit_id, v.id)"""
+1 -1
View File
@@ -71,7 +71,7 @@
<record id="HUF" model="res.currency">
<field name="name">HUF</field>
<field name="symbol">Ft</field>
<field name="rounding">0.01</field>
<field name="rounding">1.00</field>
<field name="active" eval="False"/>
</record>
+6 -4
View File
@@ -351,10 +351,12 @@ class Partner(models.Model):
result['value'] = {key: convert(self.parent_id[key]) for key in address_fields}
return result
@api.onchange('state_id')
def onchange_state(self):
if self.state_id:
self.country_id = self.state_id.country_id
@api.onchange('country_id')
def _onchange_country_id(self):
if self.country_id:
return {'domain': {'state_id': [('country_id', '=', self.country_id.id)]}}
else:
return {'domain': {'state_id': []}}
@api.onchange('email')
def onchange_email(self):
@@ -241,6 +241,9 @@ class TestExpression(TransactionCase):
partners = Partner.search([('company_id.partner_id', 'in', [])])
self.assertFalse(partners)
# check multi-level expressions with magic columns
partners = Partner.search([('create_uid.active', '=', True)])
# check that multi-level expressions with negative op work
all_partners = Partner.search([('company_id', '!=', False)])
res_partners = Partner.search([('company_id.partner_id', 'not in', [])])
+8 -4
View File
@@ -191,6 +191,8 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=
# Set new modules and dependencies
module.write({'state': 'installed', 'latest_version': ver})
package.load_state = package.state
package.load_version = package.installed_version
package.state = 'installed'
for kind in ('init', 'demo', 'update'):
if hasattr(package, kind):
@@ -338,6 +340,11 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
registry.setup_models(cr)
# STEP 3.5: execute migration end-scripts
migrations = odoo.modules.migration.MigrationManager(cr, graph)
for package in graph:
migrations.migrate_module(package, 'end')
# STEP 4: Finish and cleanup installations
if processed_modules:
cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
@@ -394,12 +401,9 @@ def load_modules(db, force_demo=False, status=None, update_module=False):
# STEP 6: verify custom views on every model
if update_module:
View = env['ir.ui.view']
custom_view_test = True
for model in registry:
if not View._validate_custom_views(model):
custom_view_test = False
_logger.error('invalid custom view(s) for model %s', model)
report.record_result(custom_view_test)
_logger.warning('invalid custom view(s) for model %s', model)
if report.failures:
_logger.error('At least one test failed when loading the modules.')
+42 -38
View File
@@ -3,12 +3,14 @@
""" Modules migration handling. """
from collections import defaultdict
import glob
import imp
import logging
import os
from os.path import join as opj
import odoo
from odoo.modules.module import get_resource_path
import odoo.release as release
import odoo.tools as tools
from odoo.tools.parse_version import parse_version
@@ -19,59 +21,64 @@ _logger = logging.getLogger(__name__)
class MigrationManager(object):
"""
This class manage the migration of modules
Migrations files must be python files containing a "migrate(cr, installed_version)" function.
Theses files must respect a directory tree structure: A 'migrations' folder which containt a
folder by version. Version can be 'module' version or 'server.module' version (in this case,
the files will only be processed by this version of the server). Python file names must start
by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation
Migrations files must be python files containing a `migrate(cr, installed_version)`
function. Theses files must respect a directory tree structure: A 'migrations' folder
which containt a folder by version. Version can be 'module' version or 'server.module'
version (in this case, the files will only be processed by this version of the server).
Python file names must start by `pre` or `post` and will be executed, respectively,
before and after the module initialisation. `end` scripts are run after all modules have
been updated.
Example:
<moduledir>
`-- migrations
|-- 1.0
| |-- pre-update_table_x.py
| |-- pre-update_table_y.py
| |-- post-clean-data.py
| `-- README.txt # not processed
|-- 5.0.1.1 # files in this folder will be executed only on a 5.0 server
| |-- post-create_plop_records.py
| |-- end-cleanup.py
| `-- README.txt # not processed
|-- 9.0.1.1 # processed only on a 9.0 server
| |-- pre-delete_table_z.py
| `-- post-clean-data.py
`-- foo.py # not processed
This similar structure is generated by the maintenance module with the migrations files get by
the maintenance contract
`-- foo.py # not processed
"""
def __init__(self, cr, graph):
self.cr = cr
self.graph = graph
self.migrations = {}
self.migrations = defaultdict(dict)
self._get_files()
def _get_files(self):
"""
import addons.base.maintenance.utils as maintenance_utils
maintenance_utils.update_migrations_files(self.cr)
#"""
def get_scripts(path):
if not path:
return {}
return {
version: glob.glob1(opj(path, version), '*.py')
for version in os.listdir(path)
if os.path.isdir(opj(path, version))
}
for pkg in self.graph:
self.migrations[pkg.name] = {}
if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade' or
getattr(pkg, 'load_state', None) == 'to upgrade'):
continue
get_module_filetree = odoo.modules.module.get_module_filetree
self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {}
self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {}
self.migrations[pkg.name] = {
'module': get_scripts(get_resource_path(pkg.name, 'migrations')),
'maintenance': get_scripts(get_resource_path('base', 'maintenance', 'migrations', pkg.name)),
}
def migrate_module(self, pkg, stage):
assert stage in ('pre', 'post')
assert stage in ('pre', 'post', 'end')
stageformat = {
'pre': '[>%s]',
'post': '[%s>]',
'end': '[$%s]',
}
state = pkg.state if stage in ('pre', 'post') else getattr(pkg, 'load_state', None)
if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade') or pkg.state == 'to install':
if not (hasattr(pkg, 'update') or state == 'to upgrade') or state == 'to install':
return
def convert_version(version):
@@ -80,18 +87,17 @@ class MigrationManager(object):
return "%s.%s" % (release.major_version, version)
def _get_migration_versions(pkg):
def __get_dir(tree):
return [d for d in tree if tree[d] is not None]
versions = list(set(
__get_dir(self.migrations[pkg.name]['module']) +
__get_dir(self.migrations[pkg.name]['maintenance'])
ver
for lv in self.migrations[pkg.name].values()
for ver, lf in lv.items()
if lf
))
versions.sort(key=lambda k: parse_version(convert_version(k)))
return versions
def _get_migration_files(pkg, version, stage):
""" return a list of tuple (module, file)
""" return a list of migration script files
"""
m = self.migrations[pkg.name]
lst = []
@@ -102,17 +108,15 @@ class MigrationManager(object):
}
for x in mapping.keys():
if version in m[x]:
if version in m.get(x):
for f in m[x][version]:
if m[x][version][f] is not None:
continue
if not f.startswith(stage + '-'):
continue
lst.append(opj(mapping[x], version, f))
lst.sort()
return lst
parsed_installed_version = parse_version(pkg.installed_version or '')
parsed_installed_version = parse_version(getattr(pkg, 'load_version', pkg.installed_version) or '')
current_version = parse_version(convert_version(pkg.data['version']))
versions = _get_migration_versions(pkg)
+2 -5
View File
@@ -836,9 +836,6 @@ class expression(object):
new_leaf = create_substitution_leaf(leaf, dom_leaf, model)
push(new_leaf)
elif path[0] in MAGIC_COLUMNS:
push_result(leaf)
# ----------------------------------------
# PATH SPOTTED
# -> many2one or one2many with _auto_join:
@@ -1134,8 +1131,8 @@ class expression(object):
# final sanity checks - should never fail
assert operator in (TERM_OPERATORS + ('inselect', 'not inselect')), \
"Invalid operator %r in domain term %r" % (operator, leaf)
assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in model._fields \
or left in MAGIC_COLUMNS, "Invalid field %r in domain term %r" % (left, leaf)
assert leaf in (TRUE_LEAF, FALSE_LEAF) or left in model._fields, \
"Invalid field %r in domain term %r" % (left, leaf)
assert not isinstance(right, BaseModel), \
"Invalid value %r in domain term %r" % (right, leaf)