[MERGE] forward port branch 10.0 up to 799e7f7740
This commit is contained in:
@@ -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
|
||||
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)"""
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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', [])])
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user