[MERGE] forward port of branch 8.0 up to 017f346

This commit is contained in:
Christophe Simonis
2015-03-04 15:16:10 +01:00
29 changed files with 376 additions and 153 deletions
+1 -1
View File
@@ -44,7 +44,7 @@
<td>
<span t-field="line.name"/>
<span t-field="line.description"/><br/>
<span t-field="line.analytic_account.complete_name" t-if="line.analytic_account"/>
<span t-field="line.analytic_account.complete_name"/>
</td>
<td style="text-center">
<span t-field="line.ref"/>
+1 -1
View File
@@ -293,7 +293,7 @@ class product_pricelist(osv.osv):
if rule.base_pricelist_id:
price_tmp = self._price_get_multi(cr, uid,
rule.base_pricelist_id, [(product,
qty, False)], context=context)[product.id]
qty, partner)], context=context)[product.id]
ptype_src = rule.base_pricelist_id.currency_id.id
price_uom_id = qty_uom_id
price = currency_obj.compute(cr, uid,
+2 -2
View File
@@ -664,7 +664,7 @@ class product_template(osv.osv):
temp_variants = []
for variant in all_variants:
for value_id in variant_id.value_ids:
temp_variants.append(variant + [int(value_id)])
temp_variants.append(sorted(variant + [int(value_id)]))
if temp_variants:
all_variants = temp_variants
@@ -682,7 +682,7 @@ class product_template(osv.osv):
variants_active_ids = []
variants_inactive = []
for product_id in tmpl_id.product_variant_ids:
variants = map(int,product_id.attribute_value_ids)
variants = sorted(map(int,product_id.attribute_value_ids))
if variants in all_variants:
variants_active_ids.append(product_id.id)
all_variants.pop(all_variants.index(variants))
@@ -79,7 +79,7 @@
</td>
<td class="text-right">
<span t-field="line.price_subtotal"
t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
t-field-options='{"widget": "monetary", "display_currency": "o.currency_id"}'/>
</td>
</tr>
</tbody>
@@ -92,21 +92,21 @@
<td><strong>Total Without Taxes</strong></td>
<td class="text-right">
<span t-field="o.amount_untaxed"
t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
t-field-options='{"widget": "monetary", "display_currency": "o.currency_id"}'/>
</td>
</tr>
<tr>
<td>Taxes</td>
<td class="text-right">
<span t-field="o.amount_tax"
t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
t-field-options='{"widget": "monetary", "display_currency": "o.currency_id"}'/>
</td>
</tr>
<tr class="border-black">
<td><strong>Total</strong></td>
<td class="text-right">
<span t-field="o.amount_total"
t-field-options='{"widget": "monetary", "display_currency": "o.pricelist_id.currency_id"}'/>
t-field-options='{"widget": "monetary", "display_currency": "o.currency_id"}'/>
</td>
</tr>
</table>
@@ -173,7 +173,7 @@ class purchase_requisition(osv.osv):
'order_id': purchase_id,
'product_id': product.id,
'account_analytic_id': requisition_line.account_analytic_id.id,
'taxes_id': [(6, 0, vals.get('taxes_ids', []))],
'taxes_id': [(6, 0, vals.get('taxes_id', []))],
})
return vals
+1
View File
@@ -32,6 +32,7 @@
<template id="style">
body {
color: #000 !important;
word-wrap: break-word;
}
.border-black td {
border-top: 1px solid black !important;
+2 -1
View File
@@ -2831,7 +2831,8 @@ class stock_inventory_line(osv.osv):
'prod_lot_id': fields.many2one('stock.production.lot', 'Serial Number', domain="[('product_id','=',product_id)]"),
'state': fields.related('inventory_id', 'state', type='char', string='Status', readonly=True),
'theoretical_qty': fields.function(_get_theoretical_qty, type='float', digits_compute=dp.get_precision('Product Unit of Measure'),
store=True, readonly=True, string="Theoretical Quantity"),
store={'stock.inventory.line': (lambda self, cr, uid, ids, c={}: ids, ['location_id', 'product_id', 'package_id', 'product_uom_id', 'company_id', 'prod_lot_id', 'partner_id'], 20),},
readonly=True, string="Theoretical Quantity"),
'partner_id': fields.many2one('res.partner', 'Owner'),
'product_name': fields.related('product_id', 'name', type='char', string='Product Name', store={
'product.product': (_get_product_name_change, ['name', 'default_code'], 20),
+5 -3
View File
@@ -547,11 +547,13 @@ var py = {};
};
py.PY_call = function (callable, args, kwargs) {
if (!args) {
args = []; kwargs = {};
} else if (typeof args === 'object' && !(args instanceof Array)) {
args = [];
}
if (typeof args === 'object' && !(args instanceof Array)) {
kwargs = args;
args = [];
} else if (!kwargs) {
}
if (!kwargs) {
kwargs = {};
}
if (callable.__is_type) {
+154 -75
View File
@@ -222,7 +222,6 @@
['milliseconds', zero], ['minutes', zero], ['hours', zero],
['weeks', zero]
]);
var d = 0, s = 0, m = 0;
var days = args.days.toJSON() + args.weeks.toJSON() * 7;
var seconds = args.seconds.toJSON()
@@ -496,13 +495,14 @@
if (!py.PY_isInstance(other, datetime.timedelta)) {
return py.NotImplemented;
}
var s = tmxxx(this.year, this.month, this.day + other.days);
return datetime.date.fromJSON(s.year, s.month, s.day);
},
__radd__: function (other) { return this.__add__(other); },
__sub__: function (other) {
if (py.PY_isInstance(other, datetime.timedelta)) {
return this.__add__(other.__neg__());
return py.PY_add(this, py.PY_negative(other));
}
if (py.PY_isInstance(other, datetime.date)) {
// FIXME: getattr and sub API methods
@@ -562,103 +562,182 @@
return py.PY_call(py.PY_getAttr(d, 'strftime'), [args.format]);
});
var args = _.map(('year month day hour minute second microsecond '
+ 'years months weeks days hours minutes secondes microseconds '
+ 'weekday leakdays yearday nlyearday').split(' '), function (arg) {
return [arg, null];
var args = _.map(('year month day '
+ 'years months weeks days '
+ 'weekday leapdays yearday nlyearday').split(' '), function (arg) {
switch (arg) {
case 'years':case 'months':case 'days':case 'leapdays':case 'weeks':
return [arg, zero];
case 'year':case 'month':case 'day':case 'weekday':
case 'yearday':case 'nlyearday':
return [arg, null];
default:
throw new Error("Unknown relativedelta argument " + arg);
}
});
args.unshift('*');
var utils = {
divmod: function (x, y) {
var rem = x % y;
return {
div: (x - rem) / y,
mod: rem
};
},
monthrange: function (year, month) {
if (month < 1 || month > 12) {
throw new Error("Illegal month " + month);
}
var day1 = this.weekday(year, month, 1);
var ndays = this.mdays[month] + (month == this.February && this.isleap(year));
return [day1, ndays];
},
weekday: function (year, month, day) {
var date = py.PY_call(datetime.date, [year, month, day]);
return py.PY_call(py.PY_getAttr(date, 'weekday'));
},
isleap: function (year) {
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
},
mdays: [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
January: 1,
February: 2
};
var relativedelta = py.type('relativedelta', null, {
__init__: function () {
this.ops = py.PY_parseArgs(arguments, args);
this.ops.days = py.float.fromJSON(
asJS(this.ops.days) + asJS(this.ops.weeks) * 7
);
var yday = zero;
if (this.ops.nlyearday) {
yday = this.ops.nlyearday;
} else if (this.ops.yearday) {
yday = this.ops.yearday;
if (asJS(this.ops.yearday) > 59) {
this.ops.leapdays = py.float.fromJS(-1);
}
}
if (py.PY_isTrue(yday)) {
var ydayidx = [31, 59, 90, 120, 151, 181, 212,
243, 273, 304, 334, 366];
for(var idx=0; idx<ydayidx.length; ++idx) {
var ydays = ydayidx[idx];
if (asJS(yday) <= ydays) {
this.ops.month = py.float.fromJSON(idx+1);
if (!idx) {
this.ops.day = yday;
} else {
this.ops.day = py.PY_subtract(
yday,
py.float.fromJSON(ydayidx[idx-1])
);
}
break;
}
}
if (idx === ydayidx.length) {
throw new Error("Invalid year day (" + asJS(yday) + ")");
}
}
this._fix();
},
_fix: function () {
var months = asJS(this.ops.months);
if (Math.abs(months) > 11) {
var s = months > 0 ? 1 : -1;
var r = utils.divmod(months * s, 12);
this.ops.months = py.float.fromJSON(r.mod*s);
this.ops.years = py.float.fromJSON(
asJS(this.ops.years) + r.div*s);
}
this._has_time = 0;
},
__add__: function (other) {
if (!py.PY_isInstance(other, datetime.date)) {
return py.NotImplemented;
}
// TODO: test this whole mess
var year = asJS(this.ops.year) || asJS(other.year);
if (asJS(this.ops.years)) {
year += asJS(this.ops.years);
}
var year = (asJS(this.ops.year) || asJS(other.year)) + asJS(this.ops.years);
var month = asJS(this.ops.month) || asJS(other.month);
if (asJS(this.ops.months)) {
month += asJS(this.ops.months);
// FIXME: no divmod in JS?
while (month < 1) {
year -= 1;
month += 12;
var months;
if (months = asJS(this.ops.months)) {
if (Math.abs(months) < 1 || Math.abs(months) > 12) {
throw new Error("Can only use relative months between -12 and +12");
}
while (month > 12) {
month += months;
if (month > 12) {
year += 1;
month -= 12;
}
if (month < 1) {
year -= 1;
month += 12;
}
}
var lastMonthDay = new Date(year, month, 0).getDate();
var day = asJS(this.ops.day) || asJS(other.day);
if (day > lastMonthDay) { day = lastMonthDay; }
var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
if (days_offset) {
day = new Date(year, month-1, day + days_offset).getDate();
var day = Math.min(utils.monthrange(year, month)[1],
asJS(this.ops.day) || asJS(other.day));
var repl = {
year: py.float.fromJSON(year),
month: py.float.fromJSON(month),
day: py.float.fromJSON(day)
};
var days = asJS(this.ops.days);
if (py.PY_isTrue(this.ops.leapdays) && month > 2 && utils.isleap(year)) {
days += asJS(this.ops.leapdays);
}
// TODO: leapdays?
// TODO: hours, minutes, seconds? Not used in XML domains
// TODO: weekday?
// FIXME: use date.replace
return py.PY_call(datetime.date, [
py.float.fromJSON(year),
py.float.fromJSON(month),
py.float.fromJSON(day)
]);
var ret = py.PY_add(
py.PY_call(py.PY_getAttr(other, 'replace'), repl),
py.PY_call(datetime.timedelta, {
days: py.float.fromJSON(days)
})
);
if (this.ops.weekday) {
// FIXME: only handles numeric weekdays, not decorated
var weekday = asJS(this.ops.weekday), nth = 1;
var jumpdays = (Math.abs(nth) - 1) * 7;
var ret_weekday = asJS(py.PY_call(py.PY_getAttr(ret, 'weekday')));
if (nth > 0) {
jumpdays += (7-ret_weekday+weekday) % 7;
} else {
jumpdays += (ret_weekday - weekday) % 7;
jumpdays *= -1;
}
ret = py.PY_add(
ret,
py.PY_call(datetime.timedelta, {
days: py.float.fromJSON(jumpdays)
})
);
}
return ret;
},
__radd__: function (other) {
return this.__add__(other);
},
__sub__: function (other) {
if (!py.PY_isInstance(other, datetime.date)) {
return py.NotImplemented;
}
// TODO: test this whole mess
var year = asJS(this.ops.year) || asJS(other.year);
if (asJS(this.ops.years)) {
year -= asJS(this.ops.years);
}
var month = asJS(this.ops.month) || asJS(other.month);
if (asJS(this.ops.months)) {
month -= asJS(this.ops.months);
// FIXME: no divmod in JS?
while (month < 1) {
year -= 1;
month += 12;
}
while (month > 12) {
year += 1;
month -= 12;
}
}
var lastMonthDay = new Date(year, month, 0).getDate();
var day = asJS(this.ops.day) || asJS(other.day);
if (day > lastMonthDay) { day = lastMonthDay; }
var days_offset = ((asJS(this.ops.weeks) || 0) * 7) + (asJS(this.ops.days) || 0);
if (days_offset) {
day = new Date(year, month-1, day - days_offset).getDate();
}
// TODO: leapdays?
// TODO: hours, minutes, seconds? Not used in XML domains
// TODO: weekday?
return py.PY_call(datetime.date, [
py.float.fromJSON(year),
py.float.fromJSON(month),
py.float.fromJSON(day)
]);
},
__rsub__: function (other) {
return this.__sub__(other);
return this.__neg__().__radd__(other);
},
__neg__: function () {
return py.PY_call(relativedelta, {
years: py.PY_negative(this.ops.years),
months: py.PY_negative(this.ops.months),
days: py.PY_negative(this.ops.days),
leapdays: this.ops.leapdays,
year: this.ops.year,
month: this.ops.month,
day: this.ops.day,
weekday: this.ops.weekday
});
}
});
+14 -2
View File
@@ -261,9 +261,21 @@ openerp.testing.section('eval.types', {
});
test('relastivedelta', function (instance) {
strictEqual(
py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=-1)).strftime('%Y-%m-%d 23:59:59')",
py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=1)).strftime('%Y-%m-%d')",
instance.web.pyeval.context()),
"2012-02-14 23:59:59");
"2012-02-16");
strictEqual(
py.eval("(datetime.date(2012, 2, 15) + relativedelta(days=-1)).strftime('%Y-%m-%d')",
instance.web.pyeval.context()),
"2012-02-14");
strictEqual(
py.eval("(datetime.date(2012, 2, 1) + relativedelta(days=-1)).strftime('%Y-%m-%d')",
instance.web.pyeval.context()),
'2012-01-31');
strictEqual(
py.eval("(datetime.date(2015,2,5)+relativedelta(days=-6,weekday=0)).strftime('%Y-%m-%d')",
instance.web.pyeval.context()),
'2015-02-02');
});
test('datetime.tojson', function (instance) {
var result = py.eval(
+3
View File
@@ -327,6 +327,9 @@
<t t-call="web.assets_common"/>
<t t-call="web.assets_backend"/>
<style>
body { overflow: auto !important; }
</style>
<script type="text/javascript" id="qunit_config">
localStorage.clear();
QUnit.config.testTimeout = 5 * 60 * 1000;
+8
View File
@@ -14,6 +14,7 @@ import werkzeug.wrappers
from PIL import Image
import openerp
from openerp.addons.web.controllers.main import WebClient
from openerp.addons.web import http
from openerp.http import request, STATIC_CACHE
from openerp.tools import image_save_for_web
@@ -269,6 +270,13 @@ class Website(openerp.addons.web.controllers.main.Home):
irt.create(request.cr, request.uid, new_trans)
return True
@http.route('/website/translations', type='json', auth="public", website=True)
def get_website_translations(self, lang):
module_obj = request.registry['ir.module.module']
module_ids = module_obj.search(request.cr, request.uid, [('name', 'ilike', 'website'), ('state', '=', 'installed')], context=request.context)
modules = [x['name'] for x in module_obj.read(request.cr, request.uid, module_ids, ['name'], context=request.context)]
return WebClient().translations(mods=modules, lang=lang)
@http.route('/website/attach', type='http', auth='user', methods=['POST'], website=True)
def attach(self, func, upload=None, url=None, disable_optimization=None):
# the upload argument doesn't allow us to access the files if more than
+20 -2
View File
@@ -305,14 +305,32 @@
if ($('html').data('website-id')) {
website.id = $('html').data('website-id');
website.session = new openerp.Session();
var modules = ['website'];
return openerp._t.database.load_translations(website.session, modules, website.get_context().lang);
return openerp.jsonRpc('/website/translations', 'call', {'lang': website.get_context().lang})
.then(function(trans) {
openerp._t.database.set_bundle(trans);});
}
}).then(function () {
var templates = openerp.qweb.templates;
var keys = _.keys(templates);
for (var i = 0; i < keys.length; i++){
treat_node(templates[keys[i]]);
}
}).promise();
}
return all_ready;
};
function treat_node(node){
if(node.nodeType === 3) {
if(node.nodeValue.match(/\S/)){
node.nodeValue = openerp._t($.trim(node.nodeValue));
}
}
else if(node.nodeType === 1 && node.hasChildNodes()) {
_.each(node.childNodes, function(subnode) {treat_node(subnode);});
}
};
website.inject_tour = function() {
// if a tour is active inject tour js
};
@@ -2,7 +2,8 @@
'use strict';
var _t = openerp._t;
var website = openerp.website;
website.ready().done(function() {
openerp.Tour.register({
id: 'banner',
name: _t("Build a page"),
@@ -100,5 +101,6 @@
},
]
});
});
}());
@@ -18,7 +18,6 @@
return this._super.apply(this, arguments).then(function () {
var $edit_button = $("button[data-action=edit]");
$edit_button.removeClass("hidden");
$edit_button.text("Translate");
if(website.no_editor) {
$edit_button.removeProp('disabled');
@@ -2,7 +2,8 @@
'use strict';
var _t = openerp._t;
var website = openerp.website;
website.ready().done(function() {
openerp.Tour.register({
id: 'blog',
name: _t("Create a blog post"),
@@ -89,5 +90,6 @@
},
]
});
});
}());
@@ -2,7 +2,8 @@
'use strict';
var _t = openerp._t;
var website = openerp.website;
website.ready().done(function() {
openerp.Tour.register({
id: 'event',
name: _t("Create an event"),
@@ -86,5 +87,6 @@
},
]
});
});
}());
@@ -1,6 +1,7 @@
(function () {
'use strict';
var website = openerp.website;
website.ready().done(function() {
openerp.Tour.register({
id: 'event_buy_tickets',
name: "Buy tickets for the Conference on Business Apps",
@@ -92,4 +93,5 @@
}
]
});
});
}());
@@ -1,6 +1,5 @@
(function () {
'use strict';
openerp.Tour.register({
id: 'shop_buy_product',
name: "Try to buy products",
@@ -2,7 +2,8 @@
'use strict';
var _t = openerp._t;
var website = openerp.website;
website.ready().done(function() {
openerp.Tour.register({
id: 'shop',
name: _t("Create a product"),
@@ -107,5 +108,6 @@
},
]
});
});
}());
@@ -1,11 +1,11 @@
(function () {
'use strict';
var _t = openerp._t;
var steps = openerp.Tour.tours.shop_buy_product.steps;
for (var k=0; k<steps.length; k++) {
if (steps[k].title === "click on add to cart") {
steps.splice(k+1, 0, {
title: "click in modal on 'Proceed to checkout' button",
title: _t("click in modal on 'Proceed to checkout' button"),
element: '.modal a:contains("Proceed to checkout")',
});
break;
+11
View File
@@ -0,0 +1,11 @@
Italy, 3 March 2015
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,
Leonardo Donelli learts92@gmail.com https://github.com/LeartS
+11
View File
@@ -0,0 +1,11 @@
Belgium, 02/03/2015
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,
Yenthe Van Ginneken yenthevg@gmail.com https://github.com/Yenthe666
+31 -3
View File
@@ -335,6 +335,17 @@ website's rich text edition.
``t-field-options`` can be used to customize fields, the most common option
is ``widget``, other options are field- or widget-dependent.
debugging
---------
``t-debug``
invokes a debugger using PDB's ``set_trace`` API. The parameter should
be the name of a module, on which a ``set_trace`` method is called::
<t t-debug="pdb"/>
is equivalent to ``importlib.import_module("pdb").set_trace()``
Helpers
-------
@@ -477,13 +488,30 @@ The javascript QWeb implementation provides a few debugging hooks:
``t-log``
takes an expression parameter, evaluates the expression during rendering
and logs its result with ``console.log``
and logs its result with ``console.log``::
<t t-set="foo" t-value="42"/>
<t t-log="foo"/>
will print ``42`` to the console
``t-debug``
triggers a debugger breakpoint during template rendering
triggers a debugger breakpoint during template rendering::
<t t-if="a_test">
<t t-debug="">
</t>
will stop execution if debugging is active (exact condition depend on the
browser and its development tools)
``t-js``
the node's body is javascript code executed during template rendering.
Takes a ``context`` parameter, which is the name under which the rendering
context will be available in the ``t-js``'s body
context will be available in the ``t-js``'s body::
<t t-set="foo" t-value="42"/>
<t t-js="ctx">
console.log("Foo is", ctx.foo);
</t>
Helpers
-------
+8 -4
View File
@@ -63,6 +63,12 @@ def raise_qweb_exception(etype=None, **kw):
e.qweb['cause'] = original
raise
def _build_attribute(name, value):
value = escape(value)
if isinstance(name, unicode): name = name.encode('utf-8')
if isinstance(value, unicode): value = value.encode('utf-8')
return ' %s="%s"' % (name, value)
class FileSystemLoader(object):
def __init__(self, path):
# TODO: support multiple files #add_file() + add cache
@@ -276,8 +282,6 @@ class QWeb(orm.AbstractModel):
self, element, attribute_name, attribute_value, qwebcontext)
for att, val in attrs:
if not val: continue
if not isinstance(val, str):
val = unicode(val).encode('utf-8')
generated_attributes += self.render_attribute(element, att, val, qwebcontext)
break
else:
@@ -340,7 +344,7 @@ class QWeb(orm.AbstractModel):
return "<%s%s/>" % (name, generated_attributes)
def render_attribute(self, element, name, value, qwebcontext):
return ' %s="%s"' % (name, escape(value))
return _build_attribute(name, value)
def render_text(self, text, element, qwebcontext):
return text.encode('utf-8')
@@ -634,7 +638,7 @@ class FieldConverter(osv.AbstractModel):
if inherit_branding:
# add branding attributes
g_att += ''.join(
' %s="%s"' % (name, escape(value))
_build_attribute(name, value)
for name, value in self.attributes(
cr, uid, field_name, record, options,
source_element, g_att, t_att, qweb_context)
+7 -5
View File
@@ -391,6 +391,8 @@ class view(osv.osv):
:rtype: list of tuples
:return: [(view_arch,view_id), ...]
"""
if not context:
context = {}
user = self.pool['res.users'].browse(cr, 1, uid, context=context)
user_groups = frozenset(user.groups_id or ())
@@ -401,13 +403,13 @@ class view(osv.osv):
['mode', '=', 'extension'],
['active', '=', True],
]
if self.pool._init:
if self.pool._init and not context.get('load_all_views'):
# Module init currently in progress, only consider views from
# modules whose code is already loaded
conditions.extend([
'|',
['model_ids.module', 'in', tuple(self.pool._init_modules)],
['id', 'in', context and context.get('check_view_ids') or (0,)],
['id', 'in', context.get('check_view_ids') or (0,)],
])
view_ids = self.search(cr, uid, conditions, context=context)
@@ -603,10 +605,9 @@ class view(osv.osv):
parent_view = self.read_combined(
cr, uid, v.inherit_id.id, fields=fields, context=context)
arch_tree = etree.fromstring(parent_view['arch'])
self.apply_inheritance_specs(
arch_tree = self.apply_inheritance_specs(
cr, uid, arch_tree, view_arch, parent_view['id'], context=context)
if context.get('inherit_branding'):
arch_tree.attrib.update({
'data-oe-model': 'ir.ui.view',
@@ -1193,7 +1194,8 @@ class view(osv.osv):
""", (model,))
ids = map(itemgetter(0), cr.fetchall())
return self._check_xml(cr, uid, ids)
context = dict(load_all_views=True)
return self._check_xml(cr, uid, ids, context=context)
def _validate_module_views(self, cr, uid, module):
"""Validate architecture of all the views of a given module"""
+14
View File
@@ -685,6 +685,20 @@ class test_views(ViewCase):
)
self.assertTrue(validate()) # inherited view
# validation of a second inherited view (depending on 1st)
self._insert_view(
name='inherited view 2',
model=model,
priority=5,
inherit_id=vid,
arch_db="""<?xml version="1.0"?>
<xpath expr="//field[@name='name']" position="after">
<field name="target"/>
</xpath>
""",
)
self.assertTrue(validate()) # inherited view
def test_view_inheritance(self):
Views = self.registry('ir.ui.view')
+6 -2
View File
@@ -528,7 +528,7 @@ class Field(object):
@property
def base_field(self):
""" Return the base field of an inherited field, or `self`. """
return self.related_field if self.inherited else self
return self.related_field.base_field if self.inherited else self
#
# Setup of field triggers
@@ -1482,7 +1482,11 @@ class Many2one(_Relational):
# many2one field value (id and name) depends on the current record's
# access rights, and not the value's access rights.
try:
return value.sudo().name_get()[0]
value_sudo = value.sudo()
# performance trick: make sure that all records of the same
# model as value in value.env will be prefetched in value_sudo.env
value_sudo.env.prefetch[value._name].update(value.env.prefetch[value._name])
return value_sudo.name_get()[0]
except MissingError:
# Should not happen, unless the foreign key is missing.
return False
+55 -38
View File
@@ -3155,17 +3155,20 @@ class BaseModel(object):
fields = self.check_field_access_rights('read', fields)
# split fields into stored and computed fields
stored, computed = [], []
stored, inherited, computed = [], [], []
for name in fields:
if name in self._columns:
stored.append(name)
elif name in self._fields:
computed.append(name)
field = self._fields[name]
if field.inherited and field.base_field.column:
inherited.append(name)
else:
_logger.warning("%s.read() with unknown field '%s'", self._name, name)
# fetch stored fields from the database to the cache
self._read_from_database(stored)
self._read_from_database(stored, inherited)
# retrieve results from records; this takes values from the cache and
# computes remaining fields
@@ -3234,50 +3237,61 @@ class BaseModel(object):
self._cache[field] = FailedValue(e)
@api.multi
def _read_from_database(self, field_names):
def _read_from_database(self, field_names, inherited_field_names=[]):
""" Read the given fields of the records in `self` from the database,
and store them in cache. Access errors are also stored in cache.
:param field_names: list of column names of model `self`; all those
fields are guaranteed to be read
:param inherited_field_names: list of column names from parent
models; some of those fields may not be read
"""
env = self.env
cr, user, context = env.args
# FIXME: The query construction needs to be rewritten using the internal Query
# object, as in search(), to avoid ambiguous column references when
# reading/sorting on a table that is auto_joined to another table with
# common columns (e.g. the magical columns)
# make a query object for selecting ids, and apply security rules to it
query = Query(['"%s"' % self._table], ['"%s".id IN %%s' % self._table], [])
self._apply_ir_rules(query, 'read')
order_str = self._generate_order_by(None, query)
# Construct a clause for the security rules.
# 'tables' holds the list of tables necessary for the SELECT, including
# the ir.rule clauses, and contains at least self._table.
rule_clause, rule_params, tables = env['ir.rule'].domain_get(self._name, 'read')
# determine the fields that are stored as columns in tables;
# for the sake of simplicity, discard inherited translated fields
fields = map(self._fields.get, field_names + inherited_field_names)
fields_pre = [
field
for field in fields
if field.base_field.column._classic_write
if not (field.inherited and field.base_field.column.translate)
]
# determine the fields that are stored as columns in self._table
fields_pre = [f for f in field_names if self._columns[f]._classic_write]
# we need fully-qualified column names in case len(tables) > 1
def qualify(f):
if isinstance(self._columns.get(f), fields.binary) and \
context.get('bin_size_%s' % f, context.get('bin_size')):
# PG 9.2 introduces conflicting pg_size_pretty(numeric) -> need ::cast
return 'pg_size_pretty(length(%s."%s")::bigint) as "%s"' % (self._table, f, f)
# the query may involve several tables: we need fully-qualified names
def qualify(field):
col = field.name
if field.inherited:
res = self._inherits_join_calc(field.name, query)
else:
return '%s."%s"' % (self._table, f)
qual_names = map(qualify, set(fields_pre + ['id']))
res = '"%s"."%s"' % (self._table, col)
if field.type == 'binary' and (context.get('bin_size') or context.get('bin_size_' + col)):
# PG 9.2 introduces conflicting pg_size_pretty(numeric) -> need ::cast
res = 'pg_size_pretty(length(%s)::bigint) as "%s"' % (res, col)
return res
query = """ SELECT %(qual_names)s FROM %(tables)s
WHERE %(table)s.id IN %%s AND (%(extra)s)
ORDER BY %(order)s
""" % {
'qual_names': ",".join(qual_names),
'tables': ",".join(tables),
'table': self._table,
'extra': " OR ".join(rule_clause) if rule_clause else "TRUE",
'order': self._parent_order or self._order,
}
qual_names = map(qualify, set(fields_pre + [self._fields['id']]))
# determine the actual query to execute
from_clause, where_clause, where_params = query.get_sql()
query_str = """ SELECT %(qual_names)s FROM %(from_clause)s
WHERE %(where_clause)s %(order_str)s
""" % {
'qual_names': ",".join(qual_names),
'from_clause': from_clause,
'where_clause': where_clause,
'order_str': order_str,
}
result = []
for sub_ids in cr.split_for_in_conditions(self.ids):
cr.execute(query, [tuple(sub_ids)] + rule_params)
cr.execute(query_str, [tuple(sub_ids)] + where_params)
result.extend(cr.dictfetchall())
ids = [vals['id'] for vals in result]
@@ -3286,8 +3300,9 @@ class BaseModel(object):
# translate the fields if necessary
if context.get('lang'):
ir_translation = env['ir.translation']
for f in fields_pre:
if self._columns[f].translate:
for field in fields_pre:
if not field.inherited and field.column.translate:
f = field.name
#TODO: optimize out of this loop
res_trans = ir_translation._get_ids(
'%s,%s' % (self._name, f), 'model', context['lang'], ids)
@@ -3295,9 +3310,10 @@ class BaseModel(object):
vals[f] = res_trans.get(vals['id'], False) or vals[f]
# apply the symbol_get functions of the fields we just read
for f in fields_pre:
symbol_get = self._columns[f]._symbol_get
for field in fields_pre:
symbol_get = field.base_field.column._symbol_get
if symbol_get:
f = field.name
for vals in result:
vals[f] = symbol_get(vals[f])
@@ -3306,7 +3322,8 @@ class BaseModel(object):
record = self.browse(vals['id'])
record._cache.update(record._convert_to_cache(vals, validate=False))
# determine the fields that must be processed now
# determine the fields that must be processed now;
# for the sake of simplicity, we ignore inherited fields
fields_post = [f for f in field_names if not self._columns[f]._classic_write]
# Compute POST fields, grouped by multi