diff options
| author | Christophe Simonis <chs@odoo.com> | 2015-03-04 15:16:10 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2015-03-04 15:16:10 +0100 |
| commit | bc2fd0515b2ee0dafe05d95a76eccb99b35a418c (patch) | |
| tree | 0cf1fdbaa92b178ce51db635242e611e7d62173e | |
| parent | 94c33caa784e3017285b55dc68c6726ed50b5f94 (diff) | |
| parent | 017f346dd36e421877b2a7dfb94b5f805c435238 (diff) | |
[MERGE] forward port of branch 8.0 up to 017f346
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 |
