summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2015-03-04 15:16:10 +0100
committerChristophe Simonis <chs@odoo.com>2015-03-04 15:16:10 +0100
commitbc2fd0515b2ee0dafe05d95a76eccb99b35a418c (patch)
tree0cf1fdbaa92b178ce51db635242e611e7d62173e
parent94c33caa784e3017285b55dc68c6726ed50b5f94 (diff)
parent017f346dd36e421877b2a7dfb94b5f805c435238 (diff)
[MERGE] forward port of branch 8.0 up to 017f346
-rw-r--r--addons/hr_expense/views/report_expense.xml2
-rw-r--r--addons/product/pricelist.py2
-rw-r--r--addons/product/product.py4
-rw-r--r--addons/purchase/views/report_purchaseorder.xml8
-rw-r--r--addons/purchase_requisition/purchase_requisition.py2
-rw-r--r--addons/report/views/layouts.xml1
-rw-r--r--addons/stock/stock.py3
-rw-r--r--addons/web/static/lib/py.js/lib/py.js8
-rw-r--r--addons/web/static/src/js/pyeval.js225
-rw-r--r--addons/web/static/test/evals.js16
-rw-r--r--addons/web/views/webclient_templates.xml3
-rw-r--r--addons/website/controllers/main.py8
-rw-r--r--addons/website/static/src/js/website.js22
-rw-r--r--addons/website/static/src/js/website.tour.banner.js4
-rw-r--r--addons/website/static/src/js/website.translator.js1
-rw-r--r--addons/website_blog/static/src/js/website.tour.blog.js4
-rw-r--r--addons/website_event/static/src/js/website.tour.event.js4
-rw-r--r--addons/website_event_sale/static/src/js/website.tour.event_sale.js4
-rw-r--r--addons/website_sale/static/src/js/website_sale_tour_buy.js1
-rw-r--r--addons/website_sale/static/src/js/website_sale_tour_shop.js4
-rw-r--r--addons/website_sale_options/static/src/js/website_sale.test.js4
-rw-r--r--doc/cla/individual/LeartS.md11
-rw-r--r--doc/cla/individual/Yenthe666.md11
-rw-r--r--doc/reference/qweb.rst34
-rw-r--r--openerp/addons/base/ir/ir_qweb.py12
-rw-r--r--openerp/addons/base/ir/ir_ui_view.py12
-rw-r--r--openerp/addons/base/tests/test_views.py14
-rw-r--r--openerp/fields.py8
-rw-r--r--openerp/models.py99
29 files changed, 377 insertions, 154 deletions
diff --git a/addons/hr_expense/views/report_expense.xml b/addons/hr_expense/views/report_expense.xml
index 42be66c3492..5bb2e1dd8e2 100644
--- a/addons/hr_expense/views/report_expense.xml
+++ b/addons/hr_expense/views/report_expense.xml
@@ -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"/>
diff --git a/addons/product/pricelist.py b/addons/product/pricelist.py
index c1406a3743a..320d5438a7d 100644
--- a/addons/product/pricelist.py
+++ b/addons/product/pricelist.py
@@ -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,
diff --git a/addons/product/product.py b/addons/product/product.py
index 72a7848fd8c..5e1e0290390 100644
--- a/addons/product/product.py
+++ b/addons/product/product.py
@@ -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))
diff --git a/addons/purchase/views/report_purchaseorder.xml b/addons/purchase/views/report_purchaseorder.xml
index d1285c1d71b..87c6834d869 100644
--- a/addons/purchase/views/report_purchaseorder.xml
+++ b/addons/purchase/views/report_purchaseorder.xml
@@ -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>
diff --git a/addons/purchase_requisition/purchase_requisition.py b/addons/purchase_requisition/purchase_requisition.py
index 5a8630c9a90..d6560da4278 100644
--- a/addons/purchase_requisition/purchase_requisition.py
+++ b/addons/purchase_requisition/purchase_requisition.py
@@ -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
diff --git a/addons/report/views/layouts.xml b/addons/report/views/layouts.xml
index d68c3ef0211..917495e7e72 100644
--- a/addons/report/views/layouts.xml
+++ b/addons/report/views/layouts.xml
@@ -32,6 +32,7 @@
<template id="style">
body {
color: #000 !important;
+ word-wrap: break-word;
}
.border-black td {
border-top: 1px solid black !important;
diff --git a/addons/stock/stock.py b/addons/stock/stock.py
index 82cdf67d32b..325729cf992 100644
--- a/addons/stock/stock.py
+++ b/addons/stock/stock.py
@@ -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),
diff --git a/addons/web/static/lib/py.js/lib/py.js b/addons/web/static/lib/py.js/lib/py.js
index 2c6c832f9fe..35bb0b5223e 100644
--- a/addons/web/static/lib/py.js/lib/py.js
+++ b/addons/web/static/lib/py.js/lib/py.js
@@ -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) {
diff --git a/addons/web/static/src/js/pyeval.js b/addons/web/static/src/js/pyeval.js
index 4cb17e70f29..c132b8365fb 100644
--- a/addons/web/static/src/js/pyeval.js
+++ b/addons/web/static/src/js/pyeval.js
@@ -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();
- }
- // 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)
- ]);
- },
- __radd__: function (other) {
- return this.__add__(other);
- },
+ var day = Math.min(utils.monthrange(year, month)[1],
+ asJS(this.ops.day) || asJS(other.day));
- __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 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);
}
- 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 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)
+ })
+ );
}
- 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)
- ]);
+ return ret;
+ },
+ __radd__: function (other) {
+ return this.__add__(other);
},
__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
+ });
}
});
diff --git a/addons/web/static/test/evals.js b/addons/web/static/test/evals.js
index 13ac0f3f3b7..a85f973e874 100644
--- a/addons/web/static/test/evals.js
+++ b/addons/web/static/test/evals.js
@@ -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(
diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml
index 0e4bd883aee..ff9d25f06e7 100644
--- a/addons/web/views/webclient_templates.xml
+++ b/addons/web/views/webclient_templates.xml
@@ -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;
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index 401dd4c50df..d7262d54c6d 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -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
diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js
index 667dd139372..8c8dfcc50c7 100644
--- a/addons/website/static/src/js/website.js
+++ b/addons/website/static/src/js/website.js
@@ -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
};
diff --git a/addons/website/static/src/js/website.tour.banner.js b/addons/website/static/src/js/website.tour.banner.js
index 2dbb7342552..359bc978e30 100644
--- a/addons/website/static/src/js/website.tour.banner.js
+++ b/addons/website/static/src/js/website.tour.banner.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 @@
},
]
});
+ });
}());
diff --git a/addons/website/static/src/js/website.translator.js b/addons/website/static/src/js/website.translator.js
index 89ef3db9a15..1b300f5fd74 100644
--- a/addons/website/static/src/js/website.translator.js
+++ b/addons/website/static/src/js/website.translator.js
@@ -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');
diff --git a/addons/website_blog/static/src/js/website.tour.blog.js b/addons/website_blog/static/src/js/website.tour.blog.js
index 5e9ed46a7b4..f6ebfd7f21c 100644
--- a/addons/website_blog/static/src/js/website.tour.blog.js
+++ b/addons/website_blog/static/src/js/website.tour.blog.js
@@ -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 @@
},
]
});
+ });
}());
diff --git a/addons/website_event/static/src/js/website.tour.event.js b/addons/website_event/static/src/js/website.tour.event.js
index 94f920e8d90..7c3c6cd5370 100644
--- a/addons/website_event/static/src/js/website.tour.event.js
+++ b/addons/website_event/static/src/js/website.tour.event.js
@@ -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 @@
},
]
});
+ });
}());
diff --git a/addons/website_event_sale/static/src/js/website.tour.event_sale.js b/addons/website_event_sale/static/src/js/website.tour.event_sale.js
index 7390d38cd0d..0b7a67eb29c 100644
--- a/addons/website_event_sale/static/src/js/website.tour.event_sale.js
+++ b/addons/website_event_sale/static/src/js/website.tour.event_sale.js
@@ -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 @@
}
]
});
+ });
}());
diff --git a/addons/website_sale/static/src/js/website_sale_tour_buy.js b/addons/website_sale/static/src/js/website_sale_tour_buy.js
index eccfd936565..1235861ec15 100644
--- a/addons/website_sale/static/src/js/website_sale_tour_buy.js
+++ b/addons/website_sale/static/src/js/website_sale_tour_buy.js
@@ -1,6 +1,5 @@
(function () {
'use strict';
-
openerp.Tour.register({
id: 'shop_buy_product',
name: "Try to buy products",
diff --git a/addons/website_sale/static/src/js/website_sale_tour_shop.js b/addons/website_sale/static/src/js/website_sale_tour_shop.js
index 4b4855d8bf6..7c613ed25d3 100644
--- a/addons/website_sale/static/src/js/website_sale_tour_shop.js
+++ b/addons/website_sale/static/src/js/website_sale_tour_shop.js
@@ -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 @@
},
]
});
+ });
}());
diff --git a/addons/website_sale_options/static/src/js/website_sale.test.js b/addons/website_sale_options/static/src/js/website_sale.test.js
index eda399947d0..976af2fa304 100644
--- a/addons/website_sale_options/static/src/js/website_sale.test.js
+++ b/addons/website_sale_options/static/src/js/website_sale.test.js
@@ -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;
diff --git a/doc/cla/individual/LeartS.md b/doc/cla/individual/LeartS.md
new file mode 100644
index 00000000000..e5b17efc9ac
--- /dev/null
+++ b/doc/cla/individual/LeartS.md
@@ -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
diff --git a/doc/cla/individual/Yenthe666.md b/doc/cla/individual/Yenthe666.md
new file mode 100644
index 00000000000..a28acee7006
--- /dev/null
+++ b/doc/cla/individual/Yenthe666.md
@@ -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
diff --git a/doc/reference/qweb.rst b/doc/reference/qweb.rst
index 30ef4c25196..81b3966a0fd 100644
--- a/doc/reference/qweb.rst
+++ b/doc/reference/qweb.rst
@@ -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
-------
diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py
index 2b7af1bd721..700275d7f96 100644
--- a/openerp/addons/base/ir/ir_qweb.py
+++ b/openerp/addons/base/ir/ir_qweb.py
@@ -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)
diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py
index a194bccbdf0..11472a568bc 100644
--- a/openerp/addons/base/ir/ir_ui_view.py
+++ b/openerp/addons/base/ir/ir_ui_view.py
@@ -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"""
diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py
index 6c4a1ad67f4..74bc5dd63d6 100644
--- a/openerp/addons/base/tests/test_views.py
+++ b/openerp/addons/base/tests/test_views.py
@@ -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')
diff --git a/openerp/fields.py b/openerp/fields.py
index 94eb790ef73..769e0524caa 100644
--- a/openerp/fields.py
+++ b/openerp/fields.py
@@ -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
diff --git a/openerp/models.py b/openerp/models.py
index 62ecc41db11..b67e5ca4f55 100644
--- a/openerp/models.py
+++ b/openerp/models.py
@@ -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)
-
- # 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 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)
+ # 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)
+
+ # 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)
+ ]
+
+ # 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']))
-
- 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,
- }
+ 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
+
+ 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