summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Matthieu <chm@odoo.com>2016-05-02 16:03:48 +0200
committerChristophe Matthieu <chm@odoo.com>2016-06-14 09:46:25 +0200
commit7ede9bcb2de9d52994b3a6fcb84edc3f81d60284 (patch)
treebf91070143134b5b5ef31334c913bf52089c6710
parent3ebf46fcc0ca048fcc1d433a073b94e64fb7049c (diff)
[IMP] base ir.qweb: compile template (and use ast) to improve rendering speed
* The compiled templates are cached per user, lang, inherit context values * ir.ui.fields: attributes method return an dict, and record_to_html return only the content value of the field * all rendered text use build_text and all attributes use build_attribute * t-esc-options is removed and replace by format_value method * AssetsBundle receive the list files and remains
-rw-r--r--addons/account/views/report_agedpartnerbalance.xml28
-rw-r--r--addons/account/views/report_financial.xml10
-rw-r--r--addons/account/views/report_generalledger.xml12
-rw-r--r--addons/account/views/report_journal.xml14
-rw-r--r--addons/account/views/report_overdue.xml12
-rw-r--r--addons/account/views/report_partnerledger.xml14
-rw-r--r--addons/account/views/report_trialbalance.xml6
-rw-r--r--addons/l10n_fr/views/report_l10nfrbilan.xml274
-rw-r--r--addons/l10n_fr/views/report_l10nfrresultat.xml106
-rw-r--r--addons/report/models/ir_qweb.py5
-rw-r--r--addons/sale/views/report_invoice_layouted.xml2
-rw-r--r--addons/sale/views/report_saleorder.xml2
-rw-r--r--addons/web/controllers/main.py2
-rw-r--r--addons/web_editor/models/ir_qweb.py220
-rw-r--r--addons/web_editor/models/ir_ui_view.py13
-rw-r--r--addons/web_editor/models/test_models.py2
-rw-r--r--addons/web_editor/static/src/js/snippets.editor.js4
-rw-r--r--addons/website/models/ir_http.py6
-rw-r--r--addons/website/models/ir_qweb.py62
-rw-r--r--addons/website_blog/views/website_blog_templates.xml2
-rw-r--r--addons/website_form/controllers/main.py2
-rw-r--r--addons/website_payment/views/website_payment_templates.xml4
-rw-r--r--addons/website_quote/views/website_quotation.xml2
-rw-r--r--addons/website_sale/controllers/main.py1
-rw-r--r--openerp/addons/base/ir/ir_qweb.py1733
-rw-r--r--openerp/addons/base/ir/ir_qweb/__init__.py10
-rw-r--r--openerp/addons/base/ir/ir_qweb/assetsbundle.py615
-rw-r--r--openerp/addons/base/ir/ir_qweb/fields.py463
-rw-r--r--openerp/addons/base/ir/ir_qweb/ir_qweb.py266
-rw-r--r--openerp/addons/base/ir/ir_qweb/qweb.py972
-rw-r--r--openerp/addons/base/ir/ir_qweb/utils.py381
-rw-r--r--openerp/addons/base/tests/test_qweb.py2
-rw-r--r--openerp/addons/test_assetsbundle/tests/test_assetsbundle.py45
33 files changed, 3136 insertions, 2156 deletions
diff --git a/addons/account/views/report_agedpartnerbalance.xml b/addons/account/views/report_agedpartnerbalance.xml
index e4e3a8451b3..560c711be9b 100644
--- a/addons/account/views/report_agedpartnerbalance.xml
+++ b/addons/account/views/report_agedpartnerbalance.xml
@@ -50,13 +50,13 @@
</tr>
<tr t-if="get_partner_lines">
<th>Account Total</th>
- <th class="text-right"><span t-esc="get_direction[6]" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></th>
- <th class="text-right"><span t-esc="get_direction[4]" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></th>
- <th class="text-right"><span t-esc="get_direction[3]" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></th>
- <th class="text-right"><span t-esc="get_direction[2]" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></th>
- <th class="text-right"><span t-esc="get_direction[1]" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></th>
- <th class="text-right"><span t-esc="get_direction[0]" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></th>
- <th class="text-right"><span t-esc="get_direction[5]" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></th>
+ <th class="text-right"><span t-esc="format(get_direction[6], 'monetary', res_company.currency_id)"/></th>
+ <th class="text-right"><span t-esc="format(get_direction[4], 'monetary', res_company.currency_id)"/></th>
+ <th class="text-right"><span t-esc="format(get_direction[3], 'monetary', res_company.currency_id)"/></th>
+ <th class="text-right"><span t-esc="format(get_direction[2], 'monetary', res_company.currency_id)"/></th>
+ <th class="text-right"><span t-esc="format(get_direction[1], 'monetary', res_company.currency_id)"/></th>
+ <th class="text-right"><span t-esc="format(get_direction[0], 'monetary', res_company.currency_id)"/></th>
+ <th class="text-right"><span t-esc="format(get_direction[5], 'monetary', res_company.currency_id)"/></th>
</tr>
</thead>
<tbody>
@@ -65,25 +65,25 @@
<span t-esc="partner['name']"/>
</td>
<td class="text-right">
- <span t-esc="partner['direction']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(partner['direction'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="partner['4']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(partner['4'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="partner['3']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(partner['3'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="partner['2']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(partner['2'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="partner['1']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(partner['1'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="partner['0']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(partner['0'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="partner['total']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(partner['total'], 'monetary', res_company.currency_id)"/>
</td>
</tr>
</tbody>
diff --git a/addons/account/views/report_financial.xml b/addons/account/views/report_financial.xml
index 2647ed10fa7..692e0bc2676 100644
--- a/addons/account/views/report_financial.xml
+++ b/addons/account/views/report_financial.xml
@@ -43,13 +43,13 @@
<span t-att-style="style" t-esc="a.get('name')"/>
</td>
<td class="text-right" style="white-space: text-nowrap;">
- <span t-att-style="style" t-esc="a.get('debit')" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-att-style="style" t-esc="format(a.get('debit'), 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right" style="white-space: text-nowrap;">
- <span t-att-style="style" t-esc="a.get('credit')" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-att-style="style" t-esc="format(a.get('credit'), 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right" style="white-space: text-nowrap;">
- <span t-att-style="style" t-esc="a.get('balance')" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-att-style="style" t-esc="format(a.get('balance'), 'monetary', res_company.currency_id)"/>
</td>
</t>
</tr>
@@ -73,7 +73,7 @@
<span style="color: white;" t-esc="'..' * a.get('level', 0)"/>
<span t-att-style="style" t-esc="a.get('name')"/>
</td>
- <td class="text-right"><span t-att-style="style" t-esc="a.get('balance')" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td class="text-right"><span t-att-style="style" t-esc="format(a.get('balance'), 'monetary', res_company.currency_id)"/></td>
</t>
</tr>
</tbody>
@@ -96,7 +96,7 @@
<span style="color: white;" t-esc="'..'"/>
<span t-att-style="style" t-esc="a.get('name')"/>
</td>
- <td class="text-right"><span t-att-style="style" t-esc="a.get('balance')" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td class="text-right"><span t-att-style="style" t-esc="format(a.get('balance'), 'monetary', res_company.currency_id)"/></td>
<td class="text-right"><span t-att-style="style" t-esc="a.get('balance_cmp')"/></td>
</t>
</tr>
diff --git a/addons/account/views/report_generalledger.xml b/addons/account/views/report_generalledger.xml
index 9e2300098b1..7ef9fef28dc 100644
--- a/addons/account/views/report_generalledger.xml
+++ b/addons/account/views/report_generalledger.xml
@@ -66,13 +66,13 @@
<span t-esc="account['name']"/>
</td>
<td class="text-right">
- <span t-esc="account['debit']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(account['debit'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="account['credit']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(account['credit'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="account['balance']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(account['balance'], 'monetary', res_company.currency_id)"/>
</td>
<td groups="base.group_multi_currency"/>
</tr>
@@ -84,13 +84,13 @@
<td><span t-esc="line['move_name']"/></td>
<td><span t-esc="line['lname']"/></td>
<td class="text-right">
- <span t-esc="line['debit']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(line['debit'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="line['credit']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(line['credit'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="line['balance']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(line['balance'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right" groups="base.group_multi_currency">
<span t-esc="line['amount_currency'] if line['amount_currency'] > 0.00 else ''"/>
diff --git a/addons/account/views/report_journal.xml b/addons/account/views/report_journal.xml
index 73926363f09..76ff18c7799 100644
--- a/addons/account/views/report_journal.xml
+++ b/addons/account/views/report_journal.xml
@@ -51,10 +51,10 @@
<td><span t-field="aml.account_id.code"/></td>
<td><span t-esc="aml.partner_id and aml.partner_id.name[:23] or ''"/></td>
<td><span t-esc="aml.name[:35]"/></td>
- <td><span t-esc="aml.debit" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
- <td><span t-esc="aml.credit" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(aml.debit, 'monetary', res_company.currency_id)"/></td>
+ <td><span t-esc="format(aml.credit, 'monetary', res_company.currency_id)"/></td>
<td t-if="data['form']['amount_currency'] and aml.amount_currency">
- <span t-esc="aml.amount_currency" t-esc-options='{"widget": "monetary", "display_currency": "aml.currency_id"}'/>
+ <span t-esc="format(aml.amount_currency, 'monetary', aml.currency_id)"/>
</td>
</tr>
</tbody>
@@ -65,8 +65,8 @@
<table class="table table-condensed">
<tr>
<td><strong>Total</strong></td>
- <td><span t-esc="sum_debit(data, o)" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
- <td><span t-esc="sum_credit(data, o)" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(sum_debit(data, o), 'monetary', res_company.currency_id)"/></td>
+ <td><span t-esc="format(sum_credit(data, o), 'monetary', res_company.currency_id)"/></td>
</tr>
</table>
</div>
@@ -87,8 +87,8 @@
<t t-set="taxes" t-value="get_taxes(data, o)"/>
<tr t-foreach="taxes" t-as="tax">
<td><span t-esc="tax.name"/></td>
- <td><span t-esc="taxes[tax]['base_amount']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
- <td><span t-esc="taxes[tax]['tax_amount']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(taxes[tax]['base_amount'], 'monetary', res_company.currency_id)"/></td>
+ <td><span t-esc="format(taxes[tax]['tax_amount'], 'monetary', res_company.currency_id)"/></td>
</tr>
</tbody>
</table>
diff --git a/addons/account/views/report_overdue.xml b/addons/account/views/report_overdue.xml
index 668cdc790b3..8071c8d7601 100644
--- a/addons/account/views/report_overdue.xml
+++ b/addons/account/views/report_overdue.xml
@@ -44,10 +44,10 @@
<span t-esc="line['ref']"/>
</td>
<td class="text-right">
- <span t-esc="line['debit']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/>
+ <span t-esc="format(line['debit'], 'monetary', currency)"/>
</td>
<td class="text-right">
- <span t-esc="line['credit']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/>
+ <span t-esc="format(line['credit'], 'monetary', currency)"/>
</td>
<td class="text-center">
<span t-if="line['blocked']">x</span>
@@ -56,16 +56,16 @@
<tr>
<td colspan="3"></td>
<td><strong>Sub-Total :</strong></td>
- <td class="text-right"><span t-esc="Totals[o.id][currency]['due']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/></td>
- <td class="text-right"><span t-esc="Totals[o.id][currency]['paid']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/></td>
- <td class="text-right"><span t-esc="Totals[o.id][currency]['mat']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/></td>
+ <td class="text-right"><span t-esc="format(Totals[o.id][currency]['due'], 'monetary', currency)"/></td>
+ <td class="text-right"><span t-esc="format(Totals[o.id][currency]['paid'], 'monetary', currency)"/></td>
+ <td class="text-right"><span t-esc="format(Totals[o.id][currency]['mat'], 'monetary', currency)"/></td>
<td></td>
</tr>
<tr>
<td colspan="3"></td>
<td><strong>Balance :</strong></td>
<td class="text-right">
- <span t-esc="Totals[o.id][currency]['due'] - Totals[o.id][currency]['paid']" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/>
+ <span t-esc="format(Totals[o.id][currency]['due'] - Totals[o.id][currency]['paid'], 'monetary', currency)"/>
</td>
</tr>
</table>
diff --git a/addons/account/views/report_partnerledger.xml b/addons/account/views/report_partnerledger.xml
index e8cb8c3e263..94ac9b0d4b6 100644
--- a/addons/account/views/report_partnerledger.xml
+++ b/addons/account/views/report_partnerledger.xml
@@ -46,13 +46,13 @@
- <strong t-esc="o.name"/>
</td>
<td class="text-right">
- <strong t-esc="sum_partner(data, o, 'debit')" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <strong t-esc="format(sum_partner(data, o, 'debit'), 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <strong t-esc="sum_partner(data, o, 'credit')" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <strong t-esc="format(sum_partner(data, o, 'credit'), 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <strong t-esc="sum_partner(data, o, 'debit - credit')" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <strong t-esc="format(sum_partner(data, o, 'debit - credit'), 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr t-foreach="lines(data, o)" t-as="line">
@@ -69,16 +69,16 @@
<span t-esc="line['displayed_name']"/>
</td>
<td class="text-right">
- <span t-esc="line['debit']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(line['debit'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="line['credit']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(line['credit'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="line['progress']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(line['progress'], 'monetary', res_company.currency_id)"/>
</td>
<td t-if="amount_currency">
- <span t-esc="line['amount_currency']" t-esc-options='{"widget": "monetary", "display_currency": "line[\"currency_id\"]"}'/>
+ <span t-esc="format(line['amount_currency'], 'monetary', line['currency_id'])"/>
</td>
</tr>
</tbody>
diff --git a/addons/account/views/report_trialbalance.xml b/addons/account/views/report_trialbalance.xml
index 5470f6385ed..7ada5bc393b 100644
--- a/addons/account/views/report_trialbalance.xml
+++ b/addons/account/views/report_trialbalance.xml
@@ -50,13 +50,13 @@
<span t-att-style="style" t-esc="account['name']"/>
</td>
<td class="text-right">
- <span t-att-style="style" t-esc="account['debit']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-att-style="style" t-esc="format(account['debit'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-att-style="style" t-esc="account['credit']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-att-style="style" t-esc="format(account['credit'], 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-att-style="style" t-esc="account['balance']" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-att-style="style" t-esc="format(account['balance'], 'monetary', res_company.currency_id)"/>
</td>
</tr>
</tbody>
diff --git a/addons/l10n_fr/views/report_l10nfrbilan.xml b/addons/l10n_fr/views/report_l10nfrbilan.xml
index f5c697c3fad..5923e6a763f 100644
--- a/addons/l10n_fr/views/report_l10nfrbilan.xml
+++ b/addons/l10n_fr/views/report_l10nfrbilan.xml
@@ -33,11 +33,11 @@
<tr>
<td>Capital souscrit - non appel&#xE9;</td>
<td class="text-right">
- <span t-esc="bavar1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar1, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
- <span t-esc="bavar1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar1, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -59,83 +59,83 @@
<tr>
<td>Frais d'&#xE9;tablissement</td>
<td class="text-right">
- <span t-esc="bavar2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar2, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar2b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar2b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar2+bavar2b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar2+bavar2b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Frais de recherche et de d&#xE9;veloppement</td>
<td class="text-right">
- <span t-esc="bavar3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar3, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar3b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar3b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar3+bavar3b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar3+bavar3b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Concessions, brevets, licences,..., droits et valeurs similaires</td>
<td class="text-right">
- <span t-esc="bavar4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar4, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar4b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar4b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar4+bavar4b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar4+bavar4b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Fonds commercial</td>
<td class="text-right">
- <span t-esc="bavar5" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar5, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar5b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar5b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar5+bavar5b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar5+bavar5b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres</td>
<td class="text-right">
- <span t-esc="bavar6" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar6, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar6b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar6b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar6+bavar6b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar6+bavar6b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Immobilisations incorporelles en cours</td>
<td class="text-right">
- <span t-esc="bavar7" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar7, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar7b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar7b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar7+bavar7b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar7+bavar7b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Avances et acomptes</td>
<td class="text-right">
- <span t-esc="bavar8" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar8, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
- <span t-esc="bavar8" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar8, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -149,71 +149,71 @@
<tr>
<td>Terrains</td>
<td class="text-right">
- <span t-esc="bavar9" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar9, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar9b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar9b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar9+bavar9b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar9+bavar9b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Constructions</td>
<td class="text-right">
- <span t-esc="bavar10" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar10, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar10b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar10b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar10+bavar10b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar10+bavar10b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Installations techniques,mat&#xE9;riel et outillage</td>
<td class="text-right">
- <span t-esc="bavar11" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar11, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar11b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar11b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar11+bavar11b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar11+bavar11b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres</td>
<td class="text-right">
- <span t-esc="bavar12" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar12, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar12b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar12b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar12+bavar12b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar12+bavar12b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Immobilisations corporelles en cours</td>
<td class="text-right">
- <span t-esc="bavar13" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar13, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar13b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar13b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar13+bavar13b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar13+bavar13b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Avances et acomptes</td>
<td class="text-right">
- <span t-esc="bavar14" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar14, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
- <span t-esc="bavar14" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar14, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -227,73 +227,73 @@
<tr>
<td>Participations</td>
<td class="text-right">
- <span t-esc="bavar15" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar15, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar15b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar15b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar15+bavar15b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar15+bavar15b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Cr&#xE9;ances rattach&#xE9;es &#xE0; des participations</td>
<td class="text-right">
- <span t-esc="bavar16" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar16, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar16b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar16b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar16+bavar16b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar16+bavar16b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Titres immobilis&#xE9;s de l'activit&#xE9; de portefeuille</td>
<td class="text-right">
- <span t-esc="bavar17" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar17, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar17b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar17b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar17+bavar17b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar17+bavar17b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres titres immobilis&#xE9;s</td>
<td class="text-right">
- <span t-esc="bavar18" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar18, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar18b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar18b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar18+bavar18b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar18+bavar18b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Pr&#xEA;ts</td>
<td class="text-right">
- <span t-esc="bavar19" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar19, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar19b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar19b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar19+bavar19b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar19+bavar19b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres</td>
<td class="text-right">
- <span t-esc="bavar20" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar20, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar20b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar20b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar20+bavar20b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar20+bavar20b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -301,13 +301,13 @@
<strong>TOTAL I</strong>
</td>
<td class="text-right">
- <span t-esc="at1a" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(at1a, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-at1b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-at1b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="at1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(at1, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -325,59 +325,59 @@
<tr>
<td>Mati&#xE8;res premi&#xE8;res et autres approvisionnements</td>
<td class="text-right">
- <span t-esc="bavar21" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar21, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar21b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar21b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar21+bavar21b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar21+bavar21b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>En-cours de production [biens et services]</td>
<td class="text-right">
- <span t-esc="bavar22" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar22, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar22b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar22b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar22+bavar22b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar22+bavar22b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Produits interm&#xE9;diaires et finis</td>
<td class="text-right">
- <span t-esc="bavar23" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar23, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar23b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar23b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar23+bavar23b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar23+bavar23b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Marchandises</td>
<td class="text-right">
- <span t-esc="bavar24" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar24, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar24b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar24b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar24+bavar24b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar24+bavar24b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Avances et acomptes vers&#xE9;s sur commandes</td>
<td class="text-right">
- <span t-esc="bavar25" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar25, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
- <span t-esc="bavar25" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar25, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -389,35 +389,35 @@
<tr>
<td>Cr&#xE9;ances clients et comptes rattach&#xE9;s</td>
<td class="text-right">
- <span t-esc="bavar26" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar26, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar26b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar26b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar26+bavar26b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar26+bavar26b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres</td>
<td class="text-right">
- <span t-esc="bavar27" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar27, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar27b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar27b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar27+bavar27b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar27+bavar27b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Capital souscrit - appel&#xE9; , non vers&#xE9;</td>
<td class="text-right">
- <span t-esc="bavar28" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar28, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
- <span t-esc="bavar28" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar28, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -429,55 +429,55 @@
<tr>
<td>Actions propres</td>
<td class="text-right">
- <span t-esc="bavar29" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar29, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar29b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar29b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar29+bavar29b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar29+bavar29b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres titres</td>
<td class="text-right">
- <span t-esc="bavar30" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar30, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-bavar30b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-bavar30b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="bavar30+bavar30b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar30+bavar30b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Instruments de tr&#xE9;sorerie</td>
<td class="text-right">
- <span t-esc="bavar31" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar31, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
- <span t-esc="bavar31" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar31, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Disponibilit&#xE9;s</td>
<td class="text-right">
- <span t-esc="bavar32" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar32, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
- <span t-esc="bavar32" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar32, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Charges constat&#xE9;s d'avance</td>
<td class="text-right">
- <span t-esc="bavar33" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar33, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
- <span t-esc="bavar33" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar33, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -485,43 +485,43 @@
<strong>TOTAL II</strong>
</td>
<td class="text-right">
- <span t-esc="at2a" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(at2a, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-at2b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-at2b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="at2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(at2, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Charges &#xE0; r&#xE9;partir sur plusieurs exercices ( III )</td>
<td class="text-right">
- <span t-esc="bavar34" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar34, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
- <span t-esc="bavar34" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar34, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Primes de remboursement des emprunts ( IV )</td>
<td class="text-right">
- <span t-esc="bavar35" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar35, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
- <span t-esc="bavar35" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar35, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>&#xC9;carts de conversion actif ( V )</td>
<td class="text-right">
- <span t-esc="bavar36" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar36, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
- <span t-esc="bavar36" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(bavar36, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -529,13 +529,13 @@
<strong>TOTAL ACTIF ( I + II + III + IV + V )</strong>
</td>
<td class="text-right">
- <span t-esc="at1a+at2a" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(at1a+at2a, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="-at1b-at2b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(-at1b-at2b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
- <span t-esc="actif" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span t-esc="format(actif, 'monetary', res_company.currency_id)"/>
</td>
</tr>
</table>
@@ -549,19 +549,19 @@
</tr>
<tr>
<td>Capital [dont vers&#xE9;...]</td>
- <td><span t-esc="bpvar1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar1, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Primes d'&#xE9;mission, de fusion, d'apport</td>
- <td><span t-esc="bpvar2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar2, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>&#xC9;carts de r&#xE9;&#xE9;valuation</td>
- <td><span t-esc="bpvar3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar3, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>&#xC9;cart d'&#xE9;quivalence</td>
- <td><span t-esc="bpvar4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar4, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td><strong>R&#xC9;SERVES</strong></td>
@@ -569,37 +569,37 @@
</tr>
<tr>
<td>R&#xE9;serve l&#xE9;gale</td>
- <td><span t-esc="bpvar5" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar5, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>R&#xE9;serves statutaires ou contractuelles</td>
- <td><span t-esc="bpvar6" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar6, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
- <td>R&#xE9;serves r&#xE9;glement&#xE9;es</td><td><span t-esc="bpvar7" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td></tr>
+ <td>R&#xE9;serves r&#xE9;glement&#xE9;es</td><td><span t-esc="format(bpvar7, 'monetary', res_company.currency_id)"/></td></tr>
<tr>
<td>Autres r&#xE9;serves</td>
- <td><span t-esc="bpvar8" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar8, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Report &#xE0; nouveau</td>
- <td><span t-esc="bpvar9" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar9, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td><strong>R&#xC9;SULTAT DE L'EXERCICE [b&#xE9;n&#xE9;fice ou perte]</strong></td>
- <td><span t-esc="bpvar10" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar10, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Subventions d'investissement</td>
- <td><span t-esc="bpvar11" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar11, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Provisions r&#xE9;glement&#xE9;es</td>
- <td><span t-esc="bpvar12" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar12, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL I</strong></td>
- <td><span t-esc="pt1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(pt1, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td><strong>PROVISIONS</strong></td>
@@ -607,15 +607,15 @@
</tr>
<tr>
<td>Provisions pour risques</td>
- <td><span t-esc="bpvar13" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar13, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Provisions pour charges</td>
- <td><span t-esc="bpvar14" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar14, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL II</strong></td>
- <td><span t-esc="pt2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(pt2, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td><strong>DETTES</strong></td>
@@ -623,59 +623,59 @@
</tr>
<tr>
<td>Emprunts obligataires convertibles</td>
- <td><span t-esc="bpvar15" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar15, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Autres emprunts obligataires</td>
- <td><span t-esc="bpvar16" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar16, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Emprunts et dettes aupr&#xE8;s des &#xE9;tablissements de cr&#xE9;dit</td>
- <td><span t-esc="bpvar17" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar17, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Emprunts et dettes financi&#xE8;res diverses</td>
- <td><span t-esc="bpvar18" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar18, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Avances et acomptes re&#xE7;us sur commandes en cours</td>
- <td><span t-esc="bpvar19" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar19, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Dettes fournisseurs et comptes rattach&#xE9;s </td>
- <td><span t-esc="bpvar20" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar20, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Dettes fiscales et sociales</td>
- <td><span t-esc="bpvar21" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar21, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Dettes sur immobilisations et comptes rattach&#xE9;s</td>
- <td><span t-esc="bpvar22" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar22, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Autres dettes</td>
- <td><span t-esc="bpvar23" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar23, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Instruments de tr&#xE9;sorerie</td>
- <td><span t-esc="bpvar24" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar24, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Produits constat&#xE9;s d'avance</td>
- <td><span t-esc="bpvar25" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar25, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL III</strong></td>
- <td><span t-esc="pt3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(pt3, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>&#xC9;carts de conversion passif <font face="Times-Roman">( IV )</font></td>
- <td><span t-esc="bpvar26" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(bpvar26, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td><strong>TOTAL G&#xC9;N&#xC9;RAL (I + II + III + IV)</strong></td>
- <td><span t-esc="passif" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(passif, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>&amp;nbsp;</td>
@@ -683,7 +683,7 @@
</tr>
<tr>
<td><strong>ACTIF - PASSIF</strong></td>
- <td><span t-esc="round(actif-passif,2)" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
+ <td><span t-esc="format(round(actif-passif,2), 'monetary', res_company.currency_id)"/></td>
</tr>
</tbody>
</table>
diff --git a/addons/l10n_fr/views/report_l10nfrresultat.xml b/addons/l10n_fr/views/report_l10nfrresultat.xml
index 9938e3fad6f..0516dd58199 100644
--- a/addons/l10n_fr/views/report_l10nfrresultat.xml
+++ b/addons/l10n_fr/views/report_l10nfrresultat.xml
@@ -35,61 +35,61 @@
<tr>
<td>Achat de marchandises</td>
<td>
- <span class="text-right" t-esc="cdrc1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc1, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Variation des stocks</td>
<td>
- <span class="text-right" t-esc="cdrc2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc2, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Achats de mati&#xE8;res premi&#xE8;res et autres approvisionnements</td>
<td>
- <span class="text-right" t-esc="cdrc3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc3, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Variation des stocks</td>
<td>
- <span class="text-right" t-esc="cdrc4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc4, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres achats et charges externes</td>
<td>
- <span class="text-right" t-esc="cdrc5" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc5, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Redevances de cr&#xE9;dit-bail mobilier</td>
<td>
- <span class="text-right" t-esc="cdrc6" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc6, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Redevances de cr&#xE9;dit-bail immobilier</td>
<td>
- <span class="text-right" t-esc="cdrc7" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc7, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Imp&#xF4;ts, taxes et versements assimil&#xE9;s</td>
<td>
- <span class="text-right" t-esc="cdrc8" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc8, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Salaires et traitements</td>
<td>
- <span class="text-right" t-esc="cdrc9" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc9, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Charges sociales</td>
<td>
- <span class="text-right" t-esc="cdrc10" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc10, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -99,43 +99,43 @@
<tr>
<td>Sur immobilisations : dotations aux amortissements</td>
<td>
- <span class="text-right" t-esc="cdrc11" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc11, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Sur immobilisations : dotations aux d&#xE9;pr&#xE9;ciations</td>
<td>
- <span class="text-right" t-esc="cdrc12" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc12, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Sur actif circulant : dotations aux d&#xE9;pr&#xE9;ciations</td>
<td>
- <span class="text-right" t-esc="cdrc13" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc13, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Dotations aux provisions</td>
<td>
- <span class="text-right" t-esc="cdrc14" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc14, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres charges</td>
<td>
- <span class="text-right" t-esc="cdrc15" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc15, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL I</strong></td>
<td>
- <span class="text-right" t-esc="ct1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(ct1, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td><strong>Quotes-parts de r&#xE9;sultat sur op&#xE9;rations faites en commun ( II )</strong></td>
<td>
- <span class="text-right" t-esc="cdrc16" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc16, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -145,31 +145,31 @@
<tr>
<td>Dotations aux amortissements, aux d&#xE9;pr&#xE9;ciations et aux provisions</td>
<td>
- <span class="text-right" t-esc="cdrc17" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc17, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Int&#xE9;r&#xEA;ts et charges assimil&#xE9;es</td>
<td>
- <span class="text-right" t-esc="cdrc18" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc18, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Diff&#xE9;rences n&#xE9;gatives de change</td>
<td>
- <span class="text-right" t-esc="cdrc19" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc19, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Charges nettes sur cessions de valeurs mobili&#xE8;res de placement</td>
<td>
- <span class="text-right" t-esc="cdrc20" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc20, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL III</strong></td>
<td>
- <span class="text-right" t-esc="ct3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(ct3, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -179,43 +179,43 @@
<tr>
<td>Sur op&#xE9;rations de gestion</td>
<td>
- <span class="text-right" t-esc="cdrc21" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc21, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Sur op&#xE9;rations en capital</td>
<td>
- <span class="text-right" t-esc="cdrc22" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc22, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Dotations aux amortissements, aux d&#xE9;pr&#xE9;ciations et aux provisions</td>
<td>
- <span class="text-right" t-esc="cdrc23" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc23, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL IV</strong></td>
<td>
- <span class="text-right" t-esc="ct4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(ct4, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td><strong>Participation des salariés aux résultats ( V )</strong></td>
<td>
- <span class="text-right" t-esc="cdrc24" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc24, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td><strong>Impôts sur les bénéfices ( VI )</strong></td>
<td>
- <span class="text-right" t-esc="cdrc25" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrc25, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL CHARGES ( I + II + III + IV+ V+ VI )</strong></td>
<td>
- <span class="text-right" t-esc="charges" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(charges, 'monetary', res_company.currency_id)"/>
</td>
</tr>
@@ -236,67 +236,67 @@
<tr>
<td>Vente de marchandises</td>
<td>
- <span class="text-right" t-esc="cdrp1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp1, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Production vendue [biens et services]</td>
<td>
- <span class="text-right" t-esc="cdrp2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp2, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td><strong>Sous-total A - Montant net du chiffre d'affaires</strong></td>
<td>
- <span class="text-right" t-esc="pta" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(pta, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Production stock&#xE9;e</td>
<td>
- <span class="text-right" t-esc="cdrp3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp3, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Production immobilis&#xE9;e</td>
<td>
- <span class="text-right" t-esc="cdrp4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp4, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Subventions d'exploitation</td>
<td>
- <span class="text-right" t-esc="cdrp5" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp5, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Reprises sur provisions, d&#xE9;pr&#xE9;ciations (et amortissements) et transferts de charges</td>
<td>
- <span class="text-right" t-esc="cdrp6" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp6, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres produits</td>
<td>
- <span class="text-right" t-esc="cdrp7" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp7, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td><strong>Sous-total B</strong></td>
<td>
- <span class="text-right" t-esc="ptb" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(ptb, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL I ( A + B )</strong></td>
<td>
- <span class="text-right" t-esc="pt1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(pt1, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td><strong>Quotes-parts de r&#xE9;sultat sur op&#xE9;rations faites en commun (II)</strong></td>
<td>
- <span class="text-right" t-esc="cdrp8" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp8, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -306,43 +306,43 @@
<tr>
<td>De participation</td>
<td>
- <span class="text-right" t-esc="cdrp9" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp9, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>D'autres valeurs mobili&#xE8;res et cr&#xE9;ances de l'actif immobilis&#xE9;</td>
<td>
- <span class="text-right" t-esc="cdrp10" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp10, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres int&#xE9;r&#xEA;ts et produits assimil&#xE9;s</td>
<td>
- <span class="text-right" t-esc="cdrp11" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp11, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Reprises sur provisions, d&#xE9;pr&#xE9;ciations et transferts de charges</td>
<td>
- <span class="text-right" t-esc="cdrp12" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp12, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Diff&#xE9;rences positives de change</td>
<td>
- <span class="text-right" t-esc="cdrp13" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp13, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Produits nets sur cessions de valeurs mobili&#xE8;res de placement</td>
<td>
- <span class="text-right" t-esc="cdrp14" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp14, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL III</strong></td>
<td>
- <span class="text-right" t-esc="pt3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(pt3, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -352,37 +352,37 @@
<tr>
<td>Sur op&#xE9;rations de gestion</td>
<td>
- <span class="text-right" t-esc="cdrp15" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp15, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Sur op&#xE9;rations en capital</td>
<td>
- <span class="text-right" t-esc="cdrp16" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp16, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Reprises sur provisions, d&#xE9;pr&#xE9;ciations et transferts de charges</td>
<td>
- <span class="text-right" t-esc="cdrp17" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(cdrp17, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL IV</strong></td>
<td>
- <span class="text-right" t-esc="pt4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(pt4, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL DES PRODUITS ( I + II + III + IV )</strong></td>
<td>
- <span class="text-right" t-esc="produits" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(produits, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>PRODUITS - CHARGES</strong></td>
<td>
- <span class="text-right" t-esc="produits-charges" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
+ <span class="text-right" t-esc="format(produits-charges, 'monetary', res_company.currency_id)"/>
</td>
</tr>
</tbody>
diff --git a/addons/report/models/ir_qweb.py b/addons/report/models/ir_qweb.py
index 6e60f795c6b..68e9673d902 100644
--- a/addons/report/models/ir_qweb.py
+++ b/addons/report/models/ir_qweb.py
@@ -1,7 +1,7 @@
from reportlab.graphics.barcode import createBarcodeDrawing
from openerp.osv import osv
-from openerp.addons.base.ir.ir_qweb import HTMLSafe
+from openerp.addons.base.ir.ir_qweb import unicodifier
class BarcodeConverter(osv.AbstractModel):
@@ -13,12 +13,13 @@ class BarcodeConverter(osv.AbstractModel):
_inherit = 'ir.qweb.field'
def value_to_html(self, cr, uid, value, field, options=None, context=None):
+ options = options or {}
barcode_type = options.get('type', 'Code128')
barcode = self.pool['report'].barcode(
barcode_type,
value,
**dict((key, value) for key, value in options.items() if key in ['width', 'height', 'humanreadable']))
- return HTMLSafe('<img src="data:%s;base64,%s">' % ('png', barcode.encode('base64')))
+ return unicodifier('<img src="data:%s;base64,%s">' % ('png', barcode.encode('base64')))
def from_html(self, cr, uid, model, field, element, context=None):
return None
diff --git a/addons/sale/views/report_invoice_layouted.xml b/addons/sale/views/report_invoice_layouted.xml
index 037298e3aad..bcbf57de207 100644
--- a/addons/sale/views/report_invoice_layouted.xml
+++ b/addons/sale/views/report_invoice_layouted.xml
@@ -58,7 +58,7 @@
<td colspan="6">
<strong>Subtotal: </strong>
<t t-set="subtotal" t-value="sum(line.price_subtotal for line in layout_category['lines'])"/>
- <span t-esc="subtotal" t-esc-options='{"widget": "monetary", "display_currency": "o.currency_id"}'/>
+ <span t-esc="format(subtotal, 'monetary', o.currency_id)"/>
</td>
</tr>
</t>
diff --git a/addons/sale/views/report_saleorder.xml b/addons/sale/views/report_saleorder.xml
index 49ce4ac2bdf..471c7a47ac3 100644
--- a/addons/sale/views/report_saleorder.xml
+++ b/addons/sale/views/report_saleorder.xml
@@ -107,7 +107,7 @@
<td colspan="6">
<strong>Subtotal: </strong>
<t t-set="subtotal" t-value="sum(line.price_subtotal for line in layout_category['lines'])"/>
- <span t-esc="subtotal" t-esc-options='{"widget": "monetary", "display_currency": "doc.pricelist_id.currency_id"}'/>
+ <span t-esc="format(subtotal, 'monetary', doc.pricelist_id.currency_id)"/>
</td>
</tr>
</t>
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index b3c6dc067dd..fb92fc71bb1 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -32,7 +32,7 @@ except ImportError:
import openerp
import openerp.modules.registry
-from openerp.addons.base.ir.ir_qweb import AssetsBundle, QWebTemplateNotFound
+from openerp.addons.base.ir.ir_qweb import AssetsBundle
from openerp.modules import get_resource_path
from openerp.tools import topological_sort
from openerp.tools.translate import _
diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb.py
index f6758536780..b6082bc0597 100644
--- a/addons/web_editor/models/ir_qweb.py
+++ b/addons/web_editor/models/ir_qweb.py
@@ -6,6 +6,7 @@ as well as render a few fields differently.
Also, adds methods to convert values back to openerp models.
"""
+import ast
import cStringIO
import datetime
import itertools
@@ -23,6 +24,8 @@ from lxml import etree, html
from PIL import Image as I
import openerp.modules
+from odoo import api
+
import openerp
from openerp.osv import orm, fields
from openerp.tools import ustr, DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
@@ -43,20 +46,26 @@ class QWeb(orm.AbstractModel):
re_remove_spaces = re.compile('\s+')
- def render_tag_snippet(self, element, template_attributes, generated_attributes, qwebcontext):
- cr = qwebcontext['request'].cr
- uid = qwebcontext['request'].uid
- view_reg = self.pool['ir.ui.view']
- page_xmlid = self.eval_format(template_attributes["snippet"], qwebcontext)
- view = view_reg.browse(cr, uid, view_reg.search(cr, uid, [('key', '=', page_xmlid)]))
- generated_attributes += " " + "".join([
- 'data-oe-name="%s"' % view.name,
- 'data-oe-type="snippet"',
- 'data-oe-thumbnail="%s"' % template_attributes.get('thumbnail', "oe-thumbnail")])
+ def _compile_directive_snippet(self, el, ast_calls):
+ el.set('t-call', el.attrib.pop('t-snippet'))
+ qwebcontext = ir_qweb.QWebContext(self.env, {})
+ name = self.env['ir.ui.view'].search([('key', '=', el.attrib.get('t-call'))]).display_name
+ thumbnail = el.attrib.pop('t-thumbnail', "oe-thumbnail")
+ div = u'<div name="%s" data-oe-type="snippet" data-oe-thumbnail="%s">' % (escape(ir_qweb.unicodifier(name)), escape(ir_qweb.unicodifier(thumbnail)))
+ return [self._append(ast.Str(div))] + self._compile_node(el, ast_calls) + [self._append(ast.Str(u'</div>'))]
+
+ def _compile_directive_tag(self, el, ast_calls):
+ if el.get('t-placeholder'):
+ el.set('t-att-placeholder', el.attrib.pop('t-placeholder'))
+ return super(QWeb, self)._compile_directive_tag(el, ast_calls)
- template_attributes['call'] = template_attributes['snippet']
- return self.render_tag_call(element, template_attributes, generated_attributes, qwebcontext)
+ def _directives_eval_order(self):
+ directives = super(QWeb, self)._directives_eval_order()
+ directives.insert(directives.index('call'), 'snippet')
+ return directives
+ def _nondirectives_ignore(self):
+ return {'t-thumbnail'} | super(QWeb, self)._nondirectives_ignore()
#------------------------------------------------------
# QWeb fields
@@ -67,36 +76,28 @@ class Field(orm.AbstractModel):
_name = 'ir.qweb.field'
_inherit = 'ir.qweb.field'
- def attributes(self, cr, uid, field_name, record, options,
- source_element, g_att, t_att, qweb_context, context=None):
- if options is None:
- options = {}
- field = record._model._fields[field_name]
- attrs = []
+ @api.model
+ def attributes(self, record, field_name, options, qwebcontext):
+ attrs = super(Field, self).attributes(record, field_name, options, qwebcontext)
+ field = record._fields[field_name]
- placeholder = options.get('placeholder') \
- or source_element.get('placeholder') \
- or getattr(field, 'placeholder', None)
+ placeholder = options.get('placeholder') or getattr(field, 'placeholder', None)
if placeholder:
- attrs.append(('placeholder', placeholder))
+ attrs['placeholder'] = placeholder
- if context and context.get('edit_translations') and context.get('translatable') and field.type in ('char', 'text') and field.translate:
+ if options['translate'] and field.type in ('char', 'text'):
name = "%s,%s" % (record._model._name, field_name)
- domain = [('name', '=', name), ('res_id', '=', record.id), ('type', '=', 'model'), ('lang', '=', context.get('lang'))]
+ domain = [('name', '=', name), ('res_id', '=', record.id), ('type', '=', 'model'), ('lang', '=', qwebcontext.env.context.get('lang'))]
translation = record.env['ir.translation'].search(domain, limit=1)
- attrs.append(('data-oe-translation-state', translation and translation.state or 'to_translate'))
+ attrs['data-oe-translation-state'] = translation and translation.state or 'to_translate'
- return itertools.chain(
- super(Field, self).attributes(cr, uid, field_name, record, options,
- source_element, g_att, t_att,
- qweb_context, context=context),
- attrs
- )
+ return attrs
def value_from_string(self, value):
return value
- def from_html(self, cr, uid, model, field, element, context=None):
+ @api.model
+ def from_html(self, model, field, element):
return self.value_from_string(element.text_content().strip())
@@ -111,8 +112,9 @@ class Float(orm.AbstractModel):
_name = 'ir.qweb.field.float'
_inherit = 'ir.qweb.field.float'
- def from_html(self, cr, uid, model, field, element, context=None):
- lang = self.user_lang(cr, uid, context=context)
+ @api.model
+ def from_html(self, model, field, element):
+ lang = self.user_lang()
value = element.text_content().strip()
@@ -124,32 +126,26 @@ class ManyToOne(orm.AbstractModel):
_name = 'ir.qweb.field.many2one'
_inherit = 'ir.qweb.field.many2one'
- def attributes(self, cr, uid, field_name, record, options,
- source_element, g_att, t_att, qweb_context,
- context=None):
- attrs = super(ManyToOne, self).attributes(
- cr, uid, field_name, record, options, source_element, g_att, t_att,
- qweb_context, context=context)
+ @api.model
+ def attributes(self, record, field_name, options, qwebcontext):
+ attrs = super(ManyToOne, self).attributes(record, field_name, options, qwebcontext)
many2one = getattr(record, field_name)
if many2one:
- data = [('data-oe-many2one-id', many2one.id),
- ('data-oe-many2one-model', many2one._name)]
- return itertools.chain(attrs, data)
- else:
- return attrs
+ attrs['data-oe-many2one-id'] = many2one.id
+ attrs['data-oe-many2one-model'] = many2one._name
+ return attrs
- def from_html(self, cr, uid, model, field, element, context=None):
- Model = self.pool[element.get('data-oe-model')]
- M2O = self.pool[field.comodel_name]
- field_name = element.get('data-oe-field')
+ @api.model
+ def from_html(self, model, field, element):
+ Model = self.env[element.get('data-oe-model')]
id = int(element.get('data-oe-id'))
+ M2O = self.env[field.comodel_name]
+ field_name = element.get('data-oe-field')
many2one_id = int(element.get('data-oe-many2one-id'))
-
- if many2one_id and M2O.exists(cr, uid, [many2one_id]):
+ record = many2one_id and M2O.browse(many2one_id)
+ if record and record.exists():
# save the new id of the many2one
- Model.write(cr, uid, [id], {
- field_name: many2one_id
- }, context=context)
+ Model.browse(id).write({field_name: many2one_id})
# not necessary, but might as well be explicit about it
return None
@@ -159,22 +155,18 @@ class Contact(orm.AbstractModel):
_name = 'ir.qweb.field.contact'
_inherit = 'ir.qweb.field.contact'
- def attributes(self, cr, uid, field_name, record, options,
- source_element, g_att, t_att, qweb_context,
- context=None):
- attrs = super(Contact, self).attributes(
- cr, uid, field_name, record, options, source_element, g_att, t_att,
- qweb_context, context=context)
- if getattr(record, field_name):
- return itertools.chain(attrs, [('data-oe-contact-options', json.dumps(options))])
- else:
- return attrs
+ @api.model
+ def attributes(self, record, field_name, options, qwebcontext):
+ attrs = super(Contact, self).attributes(record, field_name, options, qwebcontext)
+ attrs['data-oe-contact-options'] = json.dumps(options)
+ return attrs
# helper to call the rendering of contact field
- def get_record_to_html(self, cr, uid, ids, options=None, context=None):
- node = self.record_to_html(cr, uid, 'record', {
- 'record': self.pool['res.partner'].browse(cr, uid, ids[0], context=context)},
- options=options, context=context)
+ @api.model
+ def get_record_to_html(self, ids, options=None):
+ node = self.record_to_html('record', {
+ 'record': self.env['res.partner'].browse(ids[0])},
+ options=options)
return node and node.__html__()
@@ -193,15 +185,14 @@ class Date(orm.AbstractModel):
_name = 'ir.qweb.field.date'
_inherit = 'ir.qweb.field.date'
- def attributes(self, cr, uid, field_name, record, options,
- source_element, g_att, t_att, qweb_context,
- context=None):
- attrs = super(Date, self).attributes(
- cr, uid, field_name, record, options, source_element, g_att, t_att,
- qweb_context, context=None)
- return itertools.chain(attrs, [('data-oe-original', record[field_name])])
+ @api.model
+ def attributes(self, record, field_name, options, qwebcontext):
+ attrs = super(Date, self).attributes(record, field_name, options, qwebcontext)
+ attrs['data-oe-original'] = record[field_name]
+ return attrs
- def from_html(self, cr, uid, model, field, element, context=None):
+ @api.model
+ def from_html(self, model, field, element):
value = element.text_content().strip()
if not value:
return False
@@ -214,9 +205,9 @@ class DateTime(orm.AbstractModel):
_name = 'ir.qweb.field.datetime'
_inherit = 'ir.qweb.field.datetime'
- def attributes(self, cr, uid, field_name, record, options,
- source_element, g_att, t_att, qweb_context,
- context=None):
+ @api.model
+ def attributes(self, record, field_name, options, qwebcontext):
+ attrs = super(DateTime, self).attributes(record, field_name, options, qwebcontext)
value = record[field_name]
if isinstance(value, basestring):
value = datetime.datetime.strptime(
@@ -224,19 +215,13 @@ class DateTime(orm.AbstractModel):
if value:
# convert from UTC (server timezone) to user timezone
value = fields.datetime.context_timestamp(
- cr, uid, timestamp=value, context=context)
+ self._cr, self._uid, timestamp=value, context=qwebcontext.context)
value = value.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+ attrs['data-oe-original'] = value
+ return attrs
- attrs = super(DateTime, self).attributes(
- cr, uid, field_name, record, options, source_element, g_att, t_att,
- qweb_context, context=None)
- return itertools.chain(attrs, [
- ('data-oe-original', value)
- ])
-
- def from_html(self, cr, uid, model, field, element, context=None):
- if context is None:
- context = {}
+ @api.model
+ def from_html(self, model, field, element):
value = element.text_content().strip()
if not value:
return False
@@ -245,8 +230,7 @@ class DateTime(orm.AbstractModel):
dt = parser.parse(value)
# convert back from user's timezone to UTC
- tz_name = context.get('tz') \
- or self.pool['res.users'].read(cr, openerp.SUPERUSER_ID, uid, ['tz'], context=context)['tz']
+ tz_name = self.env.context.get('tz') or self.env.user.tz
if tz_name:
try:
user_tz = pytz.timezone(tz_name)
@@ -268,7 +252,8 @@ class Text(orm.AbstractModel):
_name = 'ir.qweb.field.text'
_inherit = 'ir.qweb.field.text'
- def from_html(self, cr, uid, model, field, element, context=None):
+ @api.model
+ def from_html(self, model, field, element):
return html_to_text(element)
@@ -276,10 +261,10 @@ class Selection(orm.AbstractModel):
_name = 'ir.qweb.field.selection'
_inherit = 'ir.qweb.field.selection'
- def from_html(self, cr, uid, model, field, element, context=None):
- record = self.browse(cr, uid, [], context=context)
+ @api.model
+ def from_html(self, model, field, element):
value = element.text_content().strip()
- selection = field.get_description(record.env)['selection']
+ selection = field.get_description(self.env)['selection']
for k, v in selection:
if isinstance(v, str):
v = ustr(v)
@@ -294,7 +279,8 @@ class HTML(orm.AbstractModel):
_name = 'ir.qweb.field.html'
_inherit = 'ir.qweb.field.html'
- def from_html(self, cr, uid, model, field, element, context=None):
+ @api.model
+ def from_html(self, model, field, element):
content = []
if element.text:
content.append(element.text)
@@ -313,20 +299,13 @@ class Image(orm.AbstractModel):
_name = 'ir.qweb.field.image'
_inherit = 'ir.qweb.field.image'
- def to_html(self, cr, uid, field_name, record, options,
- source_element, t_att, g_att, qweb_context, context=None):
- assert source_element.tag != 'img',\
+ @api.model
+ def record_to_html(self, record, field_name, options, qwebcontext=None):
+ assert options['tagName'] != 'img',\
"Oddly enough, the root tag of an image field can not be img. " \
"That is because the image goes into the tag, or it gets the " \
"hose again."
- return super(Image, self).to_html(
- cr, uid, field_name, record, options,
- source_element, t_att, g_att, qweb_context, context=context)
-
- def record_to_html(self, cr, uid, field_name, record, options=None, context=None):
- if options is None:
- options = {}
aclasses = ['img', 'img-responsive'] + options.get('class', '').split()
classes = ' '.join(itertools.imap(escape, aclasses))
@@ -350,11 +329,12 @@ class Image(orm.AbstractModel):
img = '<img class="%s" src="%s" style="%s"%s/>' % \
(classes, src, options.get('style', ''), ' alt="%s"' % alt if alt else '')
- return ir_qweb.HTMLSafe(img)
+ return ir_qweb.unicodifier(img)
local_url_re = re.compile(r'^/(?P<module>[^]]+)/static/(?P<rest>.+)$')
- def from_html(self, cr, uid, model, field, element, context=None):
+ @api.model
+ def from_html(self, model, field, element):
url = element.find('img').get('src')
url_object = urlparse.urlsplit(url)
@@ -370,7 +350,7 @@ class Image(orm.AbstractModel):
model = query.get('model', fragments[3])
oid = query.get('id', fragments[4].split('_')[0])
field = query.get('field', fragments[5])
- item = self.pool[model].browse(cr, uid, int(oid), context=context)
+ item = self.env[model].browse(int(oid))
return item[field]
if self.local_url_re.match(url_object.path):
@@ -432,8 +412,9 @@ class Monetary(orm.AbstractModel):
_name = 'ir.qweb.field.monetary'
_inherit = 'ir.qweb.field.monetary'
- def from_html(self, cr, uid, model, field, element, context=None):
- lang = self.user_lang(cr, uid, context=context)
+ @api.model
+ def from_html(self, model, field, element):
+ lang = self.user_lang()
value = element.find('span').text.strip()
@@ -445,15 +426,14 @@ class Duration(orm.AbstractModel):
_name = 'ir.qweb.field.duration'
_inherit = 'ir.qweb.field.duration'
- def attributes(self, cr, uid, field_name, record, options,
- source_element, g_att, t_att, qweb_context,
- context=None):
- attrs = super(Duration, self).attributes(
- cr, uid, field_name, record, options, source_element, g_att, t_att,
- qweb_context, context=None)
- return itertools.chain(attrs, [('data-oe-original', record[field_name])])
+ @api.model
+ def attributes(self, record, field_name, options, qwebcontext):
+ attrs = super(Duration, self).attributes(record, field_name, options, qwebcontext)
+ attrs['data-oe-original'] = record[field_name]
+ return attrs
- def from_html(self, cr, uid, model, field, element, context=None):
+ @api.model
+ def from_html(self, model, field, element):
value = element.text_content().strip()
# non-localized value
diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py
index 01082efb37b..762312bc28f 100644
--- a/addons/web_editor/models/ir_ui_view.py
+++ b/addons/web_editor/models/ir_ui_view.py
@@ -37,18 +37,17 @@ class view(osv.osv):
def extract_embedded_fields(self, cr, uid, arch, context=None):
return arch.xpath('//*[@data-oe-model != "ir.ui.view"]')
- def save_embedded_field(self, cr, uid, el, context=None):
- Model = self.pool[el.get('data-oe-model')]
+ @api.model
+ def save_embedded_field(self, el):
+ Model = self.env[el.get('data-oe-model')]
field = el.get('data-oe-field')
- converter = self.pool['ir.qweb'].get_converter_for(el.get('data-oe-type'))
- value = converter.from_html(cr, uid, Model, Model._fields[field], el)
+ converter = self.env['ir.qweb'].get_field_for(el.get('data-oe-type'))
+ value = converter.from_html(Model, Model._fields[field], el)
if value is not None:
# TODO: batch writes?
- Model.write(cr, uid, [int(el.get('data-oe-id'))], {
- field: value
- }, context=context)
+ Model.browse(int(el.get('data-oe-id'))).write({field: value})
def _pretty_arch(self, arch):
# remove_blank_string does not seem to work on HTMLParser, and
diff --git a/addons/web_editor/models/test_models.py b/addons/web_editor/models/test_models.py
index 100b49b6448..716e42a4dc3 100644
--- a/addons/web_editor/models/test_models.py
+++ b/addons/web_editor/models/test_models.py
@@ -21,7 +21,7 @@ class test_converter(orm.Model):
(1, "réponse A"),
(2, "réponse B"),
(3, "réponse C"),
- (4, "réponse D"),
+ (4, "réponse <D>"),
]),
'selection_str': fields.selection([
('A', "Qu'il n'est pas arrivé à Toronto"),
diff --git a/addons/web_editor/static/src/js/snippets.editor.js b/addons/web_editor/static/src/js/snippets.editor.js
index ef033dc07f3..ee0e07e4514 100644
--- a/addons/web_editor/static/src/js/snippets.editor.js
+++ b/addons/web_editor/static/src/js/snippets.editor.js
@@ -213,8 +213,8 @@ var BuildingBlock = Widget.extend({
}).on('click', function () {$(this).tooltip('hide');});
// t-snippet
- $html.find('[data-oe-type="snippet"][data-oe-name]').each(function () {
- var $div = $('<div/>').insertAfter(this).append(this).attr('name', $(this).data('oe-name'));
+ $html.find('[data-oe-type="snippet"]').each(function () {
+ $(this).children().attr('data-oe-type', "snippet").attr('data-oe-thumbnail', $(this).data('oe-thumbnail'));
});
// end
diff --git a/addons/website/models/ir_http.py b/addons/website/models/ir_http.py
index a448b9d8554..434aa42de88 100644
--- a/addons/website/models/ir_http.py
+++ b/addons/website/models/ir_http.py
@@ -11,7 +11,7 @@ import werkzeug.utils
import openerp
from openerp.addons.base import ir
-from openerp.addons.base.ir import ir_qweb
+from odoo.exceptions import QWebException
from openerp.addons.website.models.website import slug, url_for, _UNSLUG_RE
from openerp.http import request
from openerp.tools import config
@@ -247,7 +247,7 @@ class ir_http(orm.AbstractModel):
# if parent excplicitely returns a plain response, then we don't touch it
return response
except Exception, e:
- if 'werkzeug' in config['dev_mode'] and (not isinstance(exception, ir_qweb.QWebException) or not exception.qweb.get('cause')):
+ if 'werkzeug' in config['dev_mode'] and (not isinstance(exception, QWebException) or not exception.qweb.get('cause')):
raise
exception = e
@@ -267,7 +267,7 @@ class ir_http(orm.AbstractModel):
if isinstance(exception, openerp.exceptions.AccessError):
code = 403
- if isinstance(exception, ir_qweb.QWebException):
+ if isinstance(exception, QWebException):
values.update(qweb_exception=exception)
if isinstance(exception.qweb.get('cause'), openerp.exceptions.AccessError):
code = 403
diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py
index fcfb93b59c0..c8c0fe64613 100644
--- a/addons/website/models/ir_qweb.py
+++ b/addons/website/models/ir_qweb.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
from openerp.addons.web.http import request
from openerp.osv import orm
+import ast
+from openerp.addons.base.ir.ir_qweb import utils
class QWeb(orm.AbstractModel):
@@ -26,24 +28,46 @@ class QWeb(orm.AbstractModel):
'style',
]
- def render_attribute(self, element, name, value, qwebcontext):
+ def _website_build_attribute(self, tagName, name, value, qwebcontext):
context = qwebcontext.context or {}
if not context.get('rendering_bundle'):
- if name == self.URL_ATTRS.get(element.tag) and qwebcontext.get('url_for'):
- value = qwebcontext.get('url_for')(value)
- elif request and getattr(request, 'website', None) and request.website.cdn_activated and (name == self.URL_ATTRS.get(element.tag) or name == self.CDN_TRIGGERS.get(element.tag)):
- value = request.website.get_cdn_url(value)
- return super(QWeb, self).render_attribute(element, name, value, qwebcontext)
-
- def render_text(self, text, element, qwebcontext):
- compress = request and not request.debug and getattr(request, 'website', None) and request.website.compress_html
- if compress and element.tag not in self.PRESERVE_WHITESPACE:
- text = self.re_remove_spaces.sub(' ', text)
- return super(QWeb, self).render_text(text, element, qwebcontext)
-
- def render_tail(self, tail, element, qwebcontext):
- compress = request and not request.debug and getattr(request, 'website', None) and request.website.compress_html
- if compress and element.getparent().tag not in self.PRESERVE_WHITESPACE:
- # No need to recurse because those tags children are not html5 parser friendly
- tail = self.re_remove_spaces.sub(' ', tail.rstrip())
- return super(QWeb, self).render_tail(tail, element, qwebcontext)
+ if name == self.URL_ATTRS.get(tagName) and qwebcontext.get('url_for'):
+ return qwebcontext.get('url_for')(value or '')
+ elif request and request.website and request.website.cdn_activated and (name == self.URL_ATTRS.get(tagName) or name == self.CDN_TRIGGERS.get(tagName)):
+ return request.website.get_cdn_url(value or '')
+ return value
+
+ def _wrap_build_attributes(self, el, items):
+ url_att = self.URL_ATTRS.get(el.tag)
+ cdn_att = self.CDN_TRIGGERS.get(el.tag)
+ for item in items:
+ if isinstance(item, tuple) and (item[0] == url_att or item[0] == cdn_att):
+ items[items.index(item)] = (item[0], ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='self', ctx=ast.Load()),
+ attr='_website_build_attribute',
+ ctx=ast.Load()
+ ),
+ args=[
+ ast.Str(el.tag),
+ ast.Str(item[0]),
+ item[1],
+ ast.Name(id='qwebcontext', ctx=ast.Load()),
+ ], keywords=[],
+ starargs=None, kwargs=None
+ ))
+ return items
+
+ def _serialize_static_attributes(self, el):
+ items = super(QWeb, self)._serialize_static_attributes(el)
+ return self._wrap_build_attributes(el, items)
+
+ def _compile_dynamic_attributes(self, el):
+ items = super(QWeb, self)._compile_dynamic_attributes(el)
+ return self._wrap_build_attributes(el, items)
+
+ def _compile_dynamic_att(self, tagName, atts, qwebcontext):
+ atts = super(QWeb, self)._compile_dynamic_att(tagName, atts, qwebcontext)
+ for name, value in atts.iteritems():
+ atts[name] = self.build_attribute(tagName, name, value, qwebcontext)
+ return atts
diff --git a/addons/website_blog/views/website_blog_templates.xml b/addons/website_blog/views/website_blog_templates.xml
index f74ecd39a17..ae4cea4fcff 100644
--- a/addons/website_blog/views/website_blog_templates.xml
+++ b/addons/website_blog/views/website_blog_templates.xml
@@ -139,7 +139,7 @@
<span t-field="blog_post.author_avatar" class="o_not_editable" t-field-options='{"widget": "image", "class": "img-circle pull-right mt16 media-object"}' />
</t>
<a t-attf-href="/blog/#{ slug(blog_post.blog_id) }/post/#{ slug(blog_post) }">
- <h2 t-field="blog_post.name" class="mb4"><t t-if="not blog_post.name">Untitled Post</t></h2>
+ <h2 t-field="blog_post.name" class="mb4">Untitled Post</h2>
</a>
<div class="text-muted">
<h4 t-field="blog_post.subtitle" class="mb4 mt4"></h4>
diff --git a/addons/website_form/controllers/main.py b/addons/website_form/controllers/main.py
index be1bf5aa53a..d30514bfb9f 100644
--- a/addons/website_form/controllers/main.py
+++ b/addons/website_form/controllers/main.py
@@ -10,7 +10,7 @@ from openerp.http import request
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
from openerp.tools.translate import _
from openerp.exceptions import ValidationError
-from openerp.addons.base.ir.ir_qweb import nl2br
+from openerp.addons.base.ir.ir_qweb.fields import nl2br
class WebsiteForm(http.Controller):
diff --git a/addons/website_payment/views/website_payment_templates.xml b/addons/website_payment/views/website_payment_templates.xml
index 1dc0d39a76a..efc234e81e4 100644
--- a/addons/website_payment/views/website_payment_templates.xml
+++ b/addons/website_payment/views/website_payment_templates.xml
@@ -106,7 +106,7 @@
<div class="col-sm-9">
<span name="form_amount" class="form-control" t-esc="amount"/>
<!--
- <span name="form_amount" class="form-control" t-esc="amount" t-esc-options='{"widget": "monetary", "display_currency": "currency"}'/>
+ <span name="form_amount" class="form-control" t-esc="format(amount, 'monetary', currency)"/>
-->
</div>
</div>
@@ -149,7 +149,7 @@
<div class="form-group">
<label for="form_amount" class="col-sm-3 control-label">Amount</label>
<div class="col-sm-9">
- <span name="form_amount" class="form-control" t-esc="tx.amount" t-esc-options='{"widget": "monetary", "display_currency": "tx.currency_id"}'/>
+ <span name="form_amount" class="form-control" t-esc="format(tx.amount, 'monetary', tx.currency_id)"/>
</div>
</div>
<div class="col-sm-12">
diff --git a/addons/website_quote/views/website_quotation.xml b/addons/website_quote/views/website_quotation.xml
index 0e5795aadba..19ea291b2ca 100644
--- a/addons/website_quote/views/website_quotation.xml
+++ b/addons/website_quote/views/website_quotation.xml
@@ -88,7 +88,7 @@
<td class="text-right" style="padding-bottom: 32px"><strong>Subtotal:</strong></td>
<td class="text-right" style="padding-bottom: 32px">
<t t-set="subtotal" t-value="sum(line.price_subtotal for line in layout_category['lines'])"/>
- <strong data-id="total_amount" t-esc="subtotal" t-esc-options='{"widget": "monetary","display_currency": "quotation.pricelist_id.currency_id"}'/>
+ <strong data-id="total_amount" t-esc="format(subtotal, 'monetary', quotation.pricelist_id.currency_id)"/>
</td>
<td></td>
</tr>
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index 88a69e08acd..c059ca2692c 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -615,7 +615,6 @@ class WebsiteSale(http.Controller):
@http.route(['/shop/confirm_order'], type='http', auth="public", website=True)
def confirm_order(self, **post):
-
order = request.website.sale_get_order()
if not order:
return request.redirect("/shop")
diff --git a/openerp/addons/base/ir/ir_qweb.py b/openerp/addons/base/ir/ir_qweb.py
deleted file mode 100644
index 903be756c1c..00000000000
--- a/openerp/addons/base/ir/ir_qweb.py
+++ /dev/null
@@ -1,1733 +0,0 @@
-# -*- coding: utf-8 -*-
-import hashlib
-import itertools
-import json
-import logging
-import math
-import os
-import re
-import sys
-import textwrap
-import uuid
-from collections import defaultdict, Mapping, Sized
-from copy import deepcopy
-from cStringIO import StringIO
-from datetime import datetime
-from subprocess import Popen, PIPE
-from urlparse import urlparse
-
-import babel
-import babel.dates
-import psycopg2
-import werkzeug
-from lxml import etree, html
-from PIL import Image
-
-from odoo import api, fields, models, tools, SUPERUSER_ID, _
-from odoo.exceptions import QWebException
-from odoo.http import request
-from odoo.modules.module import get_resource_path
-from odoo.tools import html_escape as escape, find_in_path, lazy_property, posix_to_ldml
-from odoo.tools.safe_eval import safe_eval as eval
-
-_logger = logging.getLogger(__name__)
-
-MAX_CSS_RULES = 4095
-
-
-#--------------------------------------------------------------------
-# QWeb template engine
-#--------------------------------------------------------------------
-class QWebTemplateNotFound(QWebException):
- pass
-
-
-def raise_qweb_exception(etype=None, **kw):
- if etype is None:
- etype = QWebException
- orig_type, original, tb = sys.exc_info()
- try:
- raise etype, original, tb
- except etype, e:
- # Will use `raise foo from bar` in python 3 and rename cause to __cause__
- e.qweb.update(kw, 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
- self.path = path
- self.doc = etree.parse(path).getroot()
-
- def __iter__(self):
- for node in self.doc:
- name = node.get('t-name')
- if name:
- yield name
-
- def __call__(self, name):
- for node in self.doc:
- if node.get('t-name') == name:
- root = etree.Element('templates')
- root.append(deepcopy(node))
- arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
- return arch
-
-
-class QWebContext(dict):
- def __init__(self, env, data, loader=None):
- self.env = env
- self.loader = loader
- super(QWebContext, self).__init__(data)
- self['defined'] = lambda key: key in self
-
- # deprecated, use 'env' instead
- cr = property(lambda self: self.env.cr)
- uid = property(lambda self: self.env.uid)
- context = property(lambda self: self.env.context)
-
- def safe_eval(self, expr):
- locals_dict = defaultdict(lambda: None)
- locals_dict.update(self)
- locals_dict.pop('cr', None)
- locals_dict.pop('loader', None)
- return eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
-
- def copy(self):
- """ Clone the current context, conserving all data and metadata
- (loader, template cache, ...)
- """
- return QWebContext(self.env, dict(self), loader=self.loader)
-
- def __copy__(self):
- return self.copy()
-
-
-class QWeb(models.AbstractModel):
- """ Base QWeb rendering engine
-
- * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
- create new models called :samp:`ir.qweb.field.{widget}`
- * alternatively, override :meth:`~.get_converter_for` and return an
- arbitrary model to use as field converter
-
- Beware that if you need extensions or alterations which could be
- incompatible with other subsystems, you should create a local object
- inheriting from ``ir.qweb`` and customize that.
- """
-
- _name = 'ir.qweb'
-
- _void_elements = frozenset([
- u'area', u'base', u'br', u'col', u'embed', u'hr', u'img', u'input',
- u'keygen', u'link', u'menuitem', u'meta', u'param', u'source',
- u'track', u'wbr'])
- # regex for ruby-style pattern or jinja-style pattern
- _format_regex = re.compile(r'(?:#\{(.+?)\})|(?:\{\{(.+?)\}\})')
-
- def __init__(self, pool, cr):
- super(QWeb, self).__init__(pool, cr)
- type(self)._render_tag = self.prefixed_methods('render_tag_')
- type(self)._render_att = self.prefixed_methods('render_att_')
-
- def prefixed_methods(self, prefix):
- """ Extracts all methods prefixed by ``prefix``, and returns a mapping
- of (t-name, method) where the t-name is the method name with prefix
- removed and underscore converted to dashes
-
- :param str prefix:
- :return: dict
- """
- n_prefix = len(prefix)
- return dict(
- (name[n_prefix:].replace('_', '-'), getattr(type(self), name))
- for name in dir(self)
- if name.startswith(prefix)
- )
-
- def register_tag(self, tag, func):
- self._render_tag[tag] = func
-
- def get_template(self, name, qwebcontext):
- origin_template = qwebcontext.get('__caller__') or qwebcontext['__stack__'][0]
- try:
- document = qwebcontext.loader(name)
- except ValueError:
- raise_qweb_exception(QWebTemplateNotFound, message="Loader could not find template %r" % name, template=origin_template)
-
- if hasattr(document, 'documentElement'):
- dom = document
- elif document.startswith("<?xml"):
- dom = etree.fromstring(document)
- else:
- dom = etree.parse(document).getroot()
-
- res_id = isinstance(name, (int, long)) and name or None
- for node in dom:
- if node.get('t-name') or (res_id and node.tag == "t"):
- return node
-
- raise QWebTemplateNotFound("Template %r not found" % name, template=origin_template)
-
- def eval(self, expr, qwebcontext):
- try:
- return qwebcontext.safe_eval(expr)
- except Exception:
- template = qwebcontext.get('__template__')
- raise_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template)
-
- def eval_object(self, expr, qwebcontext):
- return self.eval(expr, qwebcontext)
-
- def eval_str(self, expr, qwebcontext):
- if expr == "0":
- return qwebcontext.get(0, '')
- val = self.eval(expr, qwebcontext)
- if isinstance(val, unicode):
- return val.encode("utf8")
- if val is False or val is None:
- return ''
- return str(val)
-
- def eval_format(self, expr, qwebcontext):
- expr, replacements = self._format_regex.subn(
- lambda m: self.eval_str(m.group(1) or m.group(2), qwebcontext),
- expr
- )
-
- if replacements:
- return expr
-
- try:
- return str(expr % qwebcontext)
- except Exception:
- template = qwebcontext.get('__template__')
- raise_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template)
-
- def eval_bool(self, expr, qwebcontext):
- return int(bool(self.eval(expr, qwebcontext)))
-
- @api.model
- def render(self, template, qwebcontext=None, loader=None):
- """ render(template, qwebcontext=None, loader=None)
-
- Renders the template specified by the provided template name
-
- :param qwebcontext: context for rendering the template
- :type qwebcontext: dict or :class:`QWebContext` instance
- :param loader: if ``qwebcontext`` is a dict, loader set into the
- context instantiated for rendering
- """
- if qwebcontext is None:
- qwebcontext = {}
-
- if not isinstance(qwebcontext, QWebContext):
- qwebcontext = QWebContext(self.env, qwebcontext, loader=loader)
-
- qwebcontext['__template__'] = template
- stack = qwebcontext.setdefault('__stack__', [])
- if stack:
- qwebcontext['__caller__'] = stack[-1]
- stack.append(template)
- qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
-
- element = self.get_template(template, qwebcontext)
- element.attrib.pop("name", False)
- return self.render_node(element, qwebcontext, generated_attributes=qwebcontext.pop('generated_attributes', ''))
-
- def render_node(self, element, qwebcontext, generated_attributes=''):
- t_render = None
- template_attributes = {}
-
- debugger = element.get('t-debug')
- if debugger is not None:
- if 'qweb' in tools.config['dev_mode']:
- __import__(debugger).set_trace() # pdb, ipdb, pudb, ...
- else:
- _logger.warning("@t-debug in template '%s' is only available in --dev mode" % qwebcontext['__template__'])
-
- for (attribute_name, attribute_value) in element.attrib.iteritems():
- attribute_name = unicode(attribute_name)
- if attribute_name == "groups":
- can_see = (qwebcontext.cr and qwebcontext.uid and
- qwebcontext.env['ir.qweb'].user_has_groups(attribute_value))
- if not can_see:
- return element.tail and self.render_tail(element.tail, element, qwebcontext) or ''
-
- attribute_value = attribute_value.encode("utf8")
-
- if attribute_name.startswith("t-"):
- for attribute in self._render_att:
- if attribute_name[2:].startswith(attribute):
- attrs = self._render_att[attribute](
- self, element, attribute_name, attribute_value, qwebcontext)
- for att, val in attrs:
- if not val:
- continue
- generated_attributes += self.render_attribute(element, att, val, qwebcontext)
- break
- else:
- if attribute_name[2:] in self._render_tag:
- t_render = attribute_name[2:]
- template_attributes[attribute_name[2:]] = attribute_value
- else:
- generated_attributes += self.render_attribute(element, attribute_name, attribute_value, qwebcontext)
-
- if t_render:
- result = self._render_tag[t_render](self, element, template_attributes, generated_attributes, qwebcontext)
- else:
- result = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
-
- if element.tail:
- result += self.render_tail(element.tail, element, qwebcontext)
-
- if isinstance(result, unicode):
- return result.encode('utf-8')
- return result
-
- def render_element(self, element, template_attributes, generated_attributes, qwebcontext, inner=None):
- # element: element
- # template_attributes: t-* attributes
- # generated_attributes: generated attributes
- # qwebcontext: values
- # inner: optional innerXml
- name = unicode(element.tag)
- if inner:
- g_inner = inner.encode('utf-8') if isinstance(inner, unicode) else inner
- else:
- g_inner = [] if element.text is None else [self.render_text(element.text, element, qwebcontext)]
- for current_node in element.iterchildren(tag=etree.Element):
- try:
- g_inner.append(self.render_node(current_node, qwebcontext,
- generated_attributes=(name == "t" and generated_attributes or '')))
- except QWebException:
- raise
- except Exception:
- template = qwebcontext.get('__template__')
- raise_qweb_exception(message="Could not render element %r" % element.tag, node=element, template=template)
- inner = "".join(g_inner)
- trim = template_attributes.get("trim", 0)
- if trim == 0:
- pass
- elif trim == 'left':
- inner = inner.lstrip()
- elif trim == 'right':
- inner = inner.rstrip()
- elif trim == 'both':
- inner = inner.strip()
- if name == "t":
- return inner
- elif len(inner) or name not in self._void_elements:
- return "<%s%s>%s</%s>" % tuple(
- qwebcontext if isinstance(qwebcontext, str) else qwebcontext.encode('utf-8')
- for qwebcontext in (name, generated_attributes, inner, name)
- )
- else:
- return "<%s%s/>" % (name.encode("utf-8"), generated_attributes)
-
- def render_attribute(self, element, name, value, qwebcontext):
- return _build_attribute(name, value)
-
- def render_text(self, text, element, qwebcontext):
- return text.encode('utf-8')
-
- def render_tail(self, tail, element, qwebcontext):
- return tail.encode('utf-8')
-
- # Attributes
- def render_att_att(self, element, attribute_name, attribute_value, qwebcontext):
- if attribute_name.startswith("t-attf-"):
- return [(attribute_name[7:], self.eval_format(attribute_value, qwebcontext))]
-
- if attribute_name.startswith("t-att-"):
- return [(attribute_name[6:], self.eval(attribute_value, qwebcontext))]
-
- result = self.eval_object(attribute_value, qwebcontext)
- if isinstance(result, Mapping):
- return result.iteritems()
- # assume tuple
- return [result]
-
- # Tags
- def render_tag_raw(self, element, template_attributes, generated_attributes, qwebcontext):
- inner = self.eval_str(template_attributes["raw"], qwebcontext)
- return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
-
- def render_tag_esc(self, element, template_attributes, generated_attributes, qwebcontext):
- options = json.loads(template_attributes.get('esc-options') or '{}')
- widget = self.get_widget_for(options.get('widget'))
- inner = widget.format(template_attributes['esc'], options, qwebcontext)
- return self.render_element(element, template_attributes, generated_attributes, qwebcontext, inner)
-
- def _iterate(self, iterable):
- if isinstance(iterable, Mapping):
- return iterable.iteritems()
-
- return itertools.izip(*itertools.tee(iterable))
-
- def render_tag_foreach(self, element, template_attributes, generated_attributes, qwebcontext):
- expr = template_attributes["foreach"]
- enum = self.eval_object(expr, qwebcontext)
- if enum is None:
- template = qwebcontext.get('__template__')
- raise QWebException("foreach enumerator %r is not defined while rendering template %r" % (expr, template), template=template)
- if isinstance(enum, int):
- enum = range(enum)
-
- varname = template_attributes['as'].replace('.', '_')
- copy_qwebcontext = qwebcontext.copy()
-
- size = None
- if isinstance(enum, Sized):
- size = len(enum)
- copy_qwebcontext["%s_size" % varname] = size
-
- copy_qwebcontext["%s_all" % varname] = enum
- ru = []
- for index, (item, value) in enumerate(self._iterate(enum)):
- copy_qwebcontext.update({
- varname: item,
- '%s_value' % varname: value,
- '%s_index' % varname: index,
- '%s_first' % varname: index == 0,
- })
- if size is not None:
- copy_qwebcontext['%s_last' % varname] = index + 1 == size
- if index % 2:
- copy_qwebcontext.update({
- '%s_parity' % varname: 'odd',
- '%s_even' % varname: False,
- '%s_odd' % varname: True,
- })
- else:
- copy_qwebcontext.update({
- '%s_parity' % varname: 'even',
- '%s_even' % varname: True,
- '%s_odd' % varname: False,
- })
- ru.append(self.render_element(element, template_attributes, generated_attributes, copy_qwebcontext))
-
- for k in qwebcontext.keys():
- qwebcontext[k] = copy_qwebcontext[k]
-
- return "".join(ru)
-
- def render_tag_if(self, element, template_attributes, generated_attributes, qwebcontext):
- if self.eval_bool(template_attributes["if"], qwebcontext):
- return self.render_element(element, template_attributes, generated_attributes, qwebcontext)
- return ""
-
- def render_tag_call(self, element, template_attributes, generated_attributes, qwebcontext):
- d = qwebcontext.copy()
-
- if 'lang' in template_attributes:
- # add 'lang' in the context of d
- lang = template_attributes['lang']
- lang = self.eval(lang, d) or lang
-
- if not d.env['res.lang'].search_count([('code', '=', lang)]):
- lang_eval = lang
- lang = qwebcontext.get('res_company') and qwebcontext['res_company'].partner_id.lang or 'en_US'
- _logger.info("'%s' is not a valid language code, is an empty field or is not installed, falling back to %s", lang_eval, lang)
- d.env = d.env(context=dict(d.context, lang=lang))
-
- d[0] = self.render_element(element, template_attributes, generated_attributes, d)
-
- template = self.eval_format(template_attributes["call"], d)
- try:
- template = int(template)
- except ValueError:
- pass
-
- d['generated_attributes'] = generated_attributes
- res = d.env['ir.qweb'].render(template, d)
-
- return res
-
- def render_tag_call_assets(self, element, template_attributes, generated_attributes, qwebcontext):
- """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
- if len(element):
- # An asset bundle is rendered in two differents contexts (when genereting html and
- # when generating the bundle itself) so they must be qwebcontext free
- # even '0' variable is forbidden
- template = qwebcontext.get('__template__')
- raise QWebException("t-call-assets cannot contain children nodes", template=template)
- xmlid = template_attributes['call-assets']
- bundle = AssetsBundle(xmlid, env=qwebcontext.env)
- css = self.get_attr_bool(template_attributes.get('css'), default=True)
- js = self.get_attr_bool(template_attributes.get('js'), default=True)
- async = self.get_attr_bool(template_attributes.get('async'), default=False)
- return bundle.to_html(css=css, js=js, debug=qwebcontext.get('debug'), async=async, qwebcontext=qwebcontext)
-
- def render_tag_set(self, element, template_attributes, generated_attributes, qwebcontext):
- if "value" in template_attributes:
- qwebcontext[template_attributes["set"]] = self.eval_object(template_attributes["value"], qwebcontext)
- elif "valuef" in template_attributes:
- qwebcontext[template_attributes["set"]] = self.eval_format(template_attributes["valuef"], qwebcontext)
- else:
- qwebcontext[template_attributes["set"]] = self.render_element(element, template_attributes, generated_attributes, qwebcontext)
- return ""
-
- def render_tag_field(self, element, template_attributes, generated_attributes, qwebcontext):
- """ eg: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
- node_name = element.tag
- assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
- "li", "ul", "ol", "dl", "dt", "dd"),\
- "RTE widgets do not work correctly on %r elements" % node_name
- assert node_name != 't',\
- "t-field can not be used on a t element, provide an actual HTML node"
-
- record, field_name = template_attributes["field"].rsplit('.', 1)
- record = self.eval_object(record, qwebcontext)
-
- foptions = self.eval_format(template_attributes.get('field-options') or '{}', qwebcontext)
- options = json.loads(foptions)
- field_type = get_field_type(record._fields[field_name], options)
-
- converter = self.get_converter_for(field_type)
- return converter.to_html(field_name, record, options, element,
- template_attributes, generated_attributes, qwebcontext)
-
- def get_converter_for(self, field_type):
- """ Return a model instance to render a ``t-field``. The result is of
- the same kind (old/new api) as ``self``
-
- By default, tries to get the model named
- :samp:`ir.qweb.field.{field_type}`, falling back on ``ir.qweb.field``.
-
- :param str field_type: type or widget of field to render
- """
- registry = getattr(self, 'env', self.pool)
- try:
- return registry['ir.qweb.field.' + field_type]
- except KeyError:
- return registry['ir.qweb.field']
-
- def get_widget_for(self, widget):
- """ Return a model instance to render a ``t-esc``. The result is of the
- same kind (old/new api) as ``self``
-
- :param str widget: name of the widget to use, or ``None``
- """
- registry = getattr(self, 'env', self.pool)
- try:
- return registry['ir.qweb.widget.' + (widget or '')]
- except KeyError:
- return registry['ir.qweb.widget']
-
- def get_attr_bool(self, attr, default=False):
- if attr:
- attr = attr.lower()
- if attr in ('false', '0'):
- return False
- elif attr in ('true', '1'):
- return True
- return default
-
-
-#--------------------------------------------------------------------
-# QWeb Fields converters
-#--------------------------------------------------------------------
-
-class FieldConverter(models.AbstractModel):
- """ Used to convert a t-field specification into an output HTML field.
-
- :meth:`~.to_html` is the entry point of this conversion from QWeb, it:
-
- * converts the record value to html using :meth:`~.record_to_html`
- * generates the metadata attributes (``data-oe-``) to set on the root
- result node
- * generates the root result node itself through :meth:`~.render_element`
- """
- _name = 'ir.qweb.field'
-
- @api.model
- def attributes(self, field_name, record, options, source_element, g_att, t_att, qweb_context):
- """ attributes(field_name, record, options, source_element, g_att, t_att, qweb_context)
-
- Generates the metadata attributes (prefixed by ``data-oe-`` for the
- root node of the field conversion. Attribute values are escaped by the
- parent.
-
- The default attributes are:
-
- * ``model``, the name of the record's model
- * ``id`` the id of the record to which the field belongs
- * ``field`` the name of the converted field
- * ``type`` the logical field type (widget, may not match the field's
- ``type``, may not be any Field subclass name)
- * ``translate``, a boolean flag (``0`` or ``1``) denoting whether the
- field is translatable
- * ``readonly``, has this attribute if the field is readonly
- * ``expression``, the original expression
-
- :returns: iterable of (attribute name, attribute value) pairs.
- """
- field = record._fields[field_name]
- field_type = get_field_type(field, options)
- data = [
- ('data-oe-model', record._name),
- ('data-oe-id', record.id),
- ('data-oe-field', field_name),
- ('data-oe-type', field_type),
- ('data-oe-expression', t_att['field']),
- ]
- if field.readonly:
- data.append(('data-oe-readonly', 1))
- return data
-
- @api.model
- def value_to_html(self, value, field, options=None):
- """ value_to_html(value, field, options=None)
-
- Converts a single value to its HTML version/output
- """
- return value or ''
-
- @api.model
- def record_to_html(self, field_name, record, options=None):
- """ record_to_html(field_name, record, options=None)
-
- Converts the specified field of the browse_record ``record`` to HTML
- """
- field = record._fields[field_name]
- return self.value_to_html(record[field_name], field, options=options)
-
- @api.model
- def to_html(self, field_name, record, options, source_element, t_att, g_att, qweb_context):
- """ to_html(field_name, record, options, source_element, t_att, g_att, qweb_context)
-
- Converts a ``t-field`` to its HTML output. A ``t-field`` may be
- extended by a ``t-field-options``, which is a JSON-serialized mapping
- of configuration values.
-
- A default configuration key is ``widget`` which can override the
- field's own ``_type``.
- """
- try:
- content = self.record_to_html(field_name, record, options)
- if options.get('html-escape', True):
- content = escape(content)
- elif hasattr(content, '__html__'):
- content = content.__html__()
- except Exception:
- _logger.warning("Could not get field %s for model %s",
- field_name, record._name, exc_info=True)
- content = None
-
- inherit_branding = self._context.get('inherit_branding')
- if not inherit_branding and self._context.get('inherit_branding_auto'):
- inherit_branding = record.check_access_rights('write', False)
- translate = self._context.get('edit_translations') and self._context.get('translatable') and getattr(record._fields[field_name], 'translate', False)
-
- if inherit_branding or translate:
- # add branding attributes
- g_att += ''.join(
- _build_attribute(name, value)
- for name, value in self.attributes(
- field_name, record, options,
- source_element, g_att, t_att, qweb_context)
- )
-
- return self.render_element(source_element, t_att, g_att, qweb_context, content)
-
- @api.model
- def render_element(self, source_element, t_att, g_att, qweb_context, content):
- """ render_element(source_element, t_att, g_att, qweb_context, content)
-
- Final rendering hook, by default just calls ir.qweb's ``render_element``
- """
- return self.env['ir.qweb'].render_element(
- source_element, t_att, g_att, qweb_context, content or '')
-
- @api.model
- def user_lang(self):
- """ user_lang()
-
- Fetches the res.lang record corresponding to the language code stored
- in the user's context. Fallbacks to en_US if no lang is present in the
- context *or the language code is not valid*.
-
- :returns: res.lang browse_record
- """
- lang_code = self._context.get('lang') or 'en_US'
- return self.env['res.lang']._lang_get(lang_code)
-
-
-class IntegerConverter(models.AbstractModel):
- _name = 'ir.qweb.field.integer'
- _inherit = 'ir.qweb.field'
-
- @api.model
- def value_to_html(self, value, field, options=None):
- return self.user_lang().format('%d', value, grouping=True)
-
-
-class FloatConverter(models.AbstractModel):
- _name = 'ir.qweb.field.float'
- _inherit = 'ir.qweb.field'
-
- @api.model
- def precision(self, field, options=None):
- _, precision = field.digits or (None, None)
- return precision
-
- @api.model
- def value_to_html(self, value, field, options=None):
- precision = self.precision(field, options=options)
- fmt = '%f' if precision is None else '%.{precision}f'
- lang = self.user_lang()
- formatted = lang.format(fmt.format(precision=precision), value, grouping=True)
-
- # %f does not strip trailing zeroes. %g does but its precision causes
- # it to switch to scientific notation starting at a million *and* to
- # strip decimals. So use %f and if no precision was specified manually
- # strip trailing 0.
- if precision is None:
- formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
- return formatted
-
-
-class DateConverter(models.AbstractModel):
- _name = 'ir.qweb.field.date'
- _inherit = 'ir.qweb.field'
-
- @api.model
- def value_to_html(self, value, field, options=None):
- if not value or len(value) < 10:
- return ''
- lang = self.user_lang()
- locale = babel.Locale.parse(lang.code)
-
- if isinstance(value, basestring):
- value = fields.Datetime.from_string(value[:10])
-
- if options and 'format' in options:
- pattern = options['format']
- else:
- strftime_pattern = lang.date_format
- pattern = posix_to_ldml(strftime_pattern, locale=locale)
-
- return babel.dates.format_date(value, format=pattern, locale=locale)
-
-
-class DateTimeConverter(models.AbstractModel):
- _name = 'ir.qweb.field.datetime'
- _inherit = 'ir.qweb.field'
-
- @api.model
- def value_to_html(self, value, field, options=None):
- if not value:
- return ''
- lang = self.user_lang()
- locale = babel.Locale.parse(lang.code)
-
- if isinstance(value, basestring):
- value = fields.Datetime.from_string(value)
-
- value = fields.Datetime.context_timestamp(self, value)
-
- if options and 'format' in options:
- pattern = options['format']
- else:
- strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
- pattern = posix_to_ldml(strftime_pattern, locale=locale)
-
- if options and options.get('hide_seconds'):
- pattern = pattern.replace(":ss", "").replace(":s", "")
-
- return babel.dates.format_datetime(value, format=pattern, locale=locale)
-
- @api.model
- def record_to_html(self, field_name, record, options=None):
- field = field = record._fields[field_name]
- value = record[field_name]
- return self.with_context(**record.env.context).value_to_html(value, field, options=options)
-
-class TextConverter(models.AbstractModel):
- _name = 'ir.qweb.field.text'
- _inherit = 'ir.qweb.field'
-
- @api.model
- def value_to_html(self, value, field, options=None):
- """
- Escapes the value and converts newlines to br. This is bullshit.
- """
- return nl2br(value, options=options) if value else ''
-
-
-class SelectionConverter(models.AbstractModel):
- _name = 'ir.qweb.field.selection'
- _inherit = 'ir.qweb.field'
-
- @api.model
- def record_to_html(self, field_name, record, options=None):
- value = record[field_name]
- if not value:
- return ''
- field = record._fields[field_name]
- selection = dict(field.get_description(record.env)['selection'])
- return self.value_to_html(selection[value], field, options=options)
-
-
-class ManyToOneConverter(models.AbstractModel):
- _name = 'ir.qweb.field.many2one'
- _inherit = 'ir.qweb.field'
-
- @api.model
- def record_to_html(self, field_name, record, options=None):
- value = record[field_name].sudo().display_name
- return nl2br(value, options=options) if value else ''
-
-
-class HTMLConverter(models.AbstractModel):
- _name = 'ir.qweb.field.html'
- _inherit = 'ir.qweb.field'
-
- @api.model
- def value_to_html(self, value, field, options=None):
- return HTMLSafe(value or '')
-
-
-class ImageConverter(models.AbstractModel):
- """ ``image`` widget rendering, inserts a data:uri-using image tag in the
- document. May be overridden by e.g. the website module to generate links
- instead.
-
- .. todo:: what happens if different output need different converters? e.g.
- reports may need embedded images or FS links whereas website
- needs website-aware
- """
- _name = 'ir.qweb.field.image'
- _inherit = 'ir.qweb.field'
-
- @api.model
- def value_to_html(self, value, field, options=None):
- try:
- image = Image.open(StringIO(value.decode('base64')))
- image.verify()
- except IOError:
- raise ValueError("Non-image binary fields can not be converted to HTML")
- except: # image.verify() throws "suitable exceptions", I have no idea what they are
- raise ValueError("Invalid image content")
-
- return HTMLSafe('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
-
-
-class MonetaryConverter(models.AbstractModel):
- """ ``monetary`` converter, has a mandatory option
- ``display_currency`` only if field is not of type Monetary.
- Otherwise, if we are in presence of a monetary field, the field definition must
- have a currency_field attribute set.
-
- The currency is used for formatting *and rounding* of the float value. It
- is assumed that the linked res_currency has a non-empty rounding value and
- res.currency's ``round`` method is used to perform rounding.
-
- .. note:: the monetary converter internally adds the qweb context to its
- options mapping, so that the context is available to callees.
- It's set under the ``_qweb_context`` key.
- """
- _name = 'ir.qweb.field.monetary'
- _inherit = 'ir.qweb.field'
-
- @api.model
- def to_html(self, field_name, record, options, source_element, t_att, g_att, qweb_context):
- options['_qweb_context'] = qweb_context
- return super(MonetaryConverter, self).to_html(
- field_name, record, options,
- source_element, t_att, g_att, qweb_context)
-
- @api.model
- def record_to_html(self, field_name, record, options):
- cur_field = record._fields[field_name]
- display_currency = False
- #currency should be specified by monetary field
- if cur_field.type == 'monetary' and cur_field.currency_field:
- display_currency = record[cur_field.currency_field]
- #otherwise fall back to old method
- if not display_currency:
- display_currency = self.display_currency(options['display_currency'], options)
-
- # lang.format mandates a sprintf-style format. These formats are non-
- # minimal (they have a default fixed precision instead), and
- # lang.format will not set one by default. currency.round will not
- # provide one either. So we need to generate a precision value
- # (integer > 0) from the currency's rounding (a float generally < 1.0).
- fmt = "%.{0}f".format(display_currency.decimal_places)
-
- from_amount = record[field_name]
-
- if options.get('from_currency'):
- from_currency = self.display_currency(options['from_currency'], options)
- from_amount = from_currency.compute(from_amount, display_currency)
-
- lang = self.user_lang()
- formatted_amount = lang.format(fmt, display_currency.round(from_amount),
- grouping=True, monetary=True).replace(r' ', u'\N{NO-BREAK SPACE}')
-
- pre = post = u''
- if display_currency.position == 'before':
- pre = u'{symbol}\N{NO-BREAK SPACE}'
- else:
- post = u'\N{NO-BREAK SPACE}{symbol}'
-
- return HTMLSafe(u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(
- formatted_amount, pre=pre, post=post,
- ).format(
- symbol=display_currency.symbol,
- ))
-
- @api.model
- def display_currency(self, currency, options):
- return self.env['ir.qweb'].eval_object(currency, options['_qweb_context'])
-
-
-TIMEDELTA_UNITS = (
- ('year', 3600 * 24 * 365),
- ('month', 3600 * 24 * 30),
- ('week', 3600 * 24 * 7),
- ('day', 3600 * 24),
- ('hour', 3600),
- ('minute', 60),
- ('second', 1)
-)
-
-
-class DurationConverter(models.AbstractModel):
- """ ``duration`` converter, to display integral or fractional values as
- human-readable time spans (e.g. 1.5 as "1 hour 30 minutes").
-
- Can be used on any numerical field.
-
- Has a mandatory option ``unit`` which can be one of ``second``, ``minute``,
- ``hour``, ``day``, ``week`` or ``year``, used to interpret the numerical
- field value before converting it.
-
- Sub-second values will be ignored.
- """
- _name = 'ir.qweb.field.duration'
- _inherit = 'ir.qweb.field'
-
- @api.model
- def value_to_html(self, value, field, options=None):
- units = dict(TIMEDELTA_UNITS)
- if value < 0:
- raise ValueError(_("Durations can't be negative"))
- if not options or options.get('unit') not in units:
- raise ValueError(_("A unit must be provided to duration widgets"))
-
- locale = babel.Locale.parse(self.user_lang().code)
- factor = units[options['unit']]
-
- sections = []
- r = value * factor
- for unit, secs_per_unit in TIMEDELTA_UNITS:
- v, r = divmod(r, secs_per_unit)
- if not v:
- continue
- section = babel.dates.format_timedelta(
- v*secs_per_unit, threshold=1, locale=locale)
- if section:
- sections.append(section)
- return ' '.join(sections)
-
-
-class RelativeDatetimeConverter(models.AbstractModel):
- _name = 'ir.qweb.field.relative'
- _inherit = 'ir.qweb.field'
-
- @api.model
- def value_to_html(self, value, field, options=None):
- locale = babel.Locale.parse(self.user_lang().code)
-
- if isinstance(value, basestring):
- value = fields.Datetime.from_string(value)
-
- # value should be a naive datetime in UTC. So is fields.Datetime.now()
- reference = fields.Datetime.from_string(field.now())
-
- return babel.dates.format_timedelta(value - reference, add_direction=True, locale=locale)
-
-
-class Contact(models.AbstractModel):
- _name = 'ir.qweb.field.contact'
- _inherit = 'ir.qweb.field.many2one'
-
- @api.model
- def record_to_html(self, field_name, record, options=None):
- if options is None:
- options = {}
- opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
-
- value_rec = record[field_name]
- if not value_rec:
- return None
- value_rec = value_rec.sudo().with_context(show_address=True)
- value = value_rec.name_get()[0][1]
-
- val = {
- 'name': value.split("\n")[0],
- 'address': escape("\n".join(value.split("\n")[1:])).strip(),
- 'phone': value_rec.phone,
- 'mobile': value_rec.mobile,
- 'fax': value_rec.fax,
- 'city': value_rec.city,
- 'country_id': value_rec.country_id.display_name,
- 'website': value_rec.website,
- 'email': value_rec.email,
- 'fields': opf,
- 'object': value_rec,
- 'options': options
- }
-
- view = self.env.ref('base.contact')
- html = view.render(val, engine='ir.qweb').decode('utf8')
-
- return HTMLSafe(html)
-
-
-class QwebView(models.AbstractModel):
- _name = 'ir.qweb.field.qweb'
- _inherit = 'ir.qweb.field.many2one'
-
- @api.model
- def record_to_html(self, field_name, record, options=None):
- if not getattr(record, field_name):
- return None
-
- view = getattr(record, field_name)
-
- if view._name != "ir.ui.view":
- _logger.warning("%s.%s must be a 'ir.ui.view' model." % (record, field_name))
- return None
-
- view = view.with_context(object=record)
- html = view.render(view._context, engine='ir.qweb').decode('utf8')
-
- return HTMLSafe(html)
-
-
-class QwebWidget(models.AbstractModel):
- _name = 'ir.qweb.widget'
-
- def _format(self, inner, options, qwebcontext):
- return self.pool['ir.qweb'].eval_str(inner, qwebcontext)
-
- def format(self, inner, options, qwebcontext):
- return escape(self._format(inner, options, qwebcontext))
-
-
-class QwebWidgetMonetary(models.AbstractModel):
- _name = 'ir.qweb.widget.monetary'
- _inherit = 'ir.qweb.widget'
-
- def _format(self, inner, options, qwebcontext):
- inner = qwebcontext.env['ir.qweb'].eval(inner, qwebcontext)
- display = qwebcontext.env['ir.qweb'].eval_object(options['display_currency'], qwebcontext)
- precision = int(round(math.log10(display.rounding)))
- fmt = "%.{0}f".format(-precision if precision < 0 else 0)
- lang_code = qwebcontext.context.get('lang') or 'en_US'
- lang = qwebcontext.env['res.lang']._lang_get(lang_code)
- formatted_amount = lang.format(fmt, inner, grouping=True, monetary=True).replace(r' ', u'\N{NO-BREAK SPACE}')
- pre = post = u''
- if display.position == 'before':
- pre = u'{symbol}\N{NO-BREAK SPACE}'
- else:
- post = u'\N{NO-BREAK SPACE}{symbol}'
-
- return u'{pre}{0}{post}'.format(
- formatted_amount, pre=pre, post=post
- ).format(symbol=display.symbol,)
-
-
-class HTMLSafe(object):
- """ HTMLSafe string wrapper, Werkzeug's escape() has special handling for
- objects with a ``__html__`` methods but AFAIK does not provide any such
- object.
-
- Wrapping a string in HTML will prevent its escaping
- """
- __slots__ = ['string']
-
- def __init__(self, string):
- self.string = string
-
- def __html__(self):
- return self.string
-
- def __str__(self):
- s = self.string
- if isinstance(s, unicode):
- return s.encode('utf-8')
- return s
-
- def __unicode__(self):
- s = self.string
- if isinstance(s, str):
- return s.decode('utf-8')
- return s
-
-
-def nl2br(string, options=None):
- """ Converts newlines to HTML linebreaks in ``string``. Automatically
- escapes content unless options['html-escape'] is set to False, and returns
- the result wrapped in an HTMLSafe object.
-
- :param str string:
- :param dict options:
- :rtype: HTMLSafe
- """
- if options is None:
- options = {}
-
- if options.get('html-escape', True):
- string = escape(string)
- return HTMLSafe(string.replace('\n', '<br>\n'))
-
-
-def get_field_type(field, options):
- """ Gets a t-field's effective type from the field definition and its options """
- return options.get('widget', field.type)
-
-
-class AssetError(Exception):
- pass
-
-
-class AssetNotFound(AssetError):
- pass
-
-
-class AssetsBundle(object):
- rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
- rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
- rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
-
- def __init__(self, xmlid, env=None, max_css_rules=MAX_CSS_RULES):
- self.xmlid = xmlid
- self.env = request.env if env is None else env
- self.max_css_rules = max_css_rules
- self.javascripts = []
- self.stylesheets = []
- self.css_errors = []
- self.remains = []
- self._checksum = None
-
- env = self.env(context=dict(self.env.context,
- inherit_branding=False,
- inherit_branding_auto=False,
- rendering_bundle=True))
- self.html = env.ref(xmlid).render()
- self.parse()
-
- def parse(self):
- fragments = html.fragments_fromstring(self.html)
- for el in fragments:
- if isinstance(el, basestring):
- self.remains.append(el)
- elif isinstance(el, html.HtmlElement):
- src = el.get('src', '')
- href = el.get('href', '')
- atype = el.get('type')
- media = el.get('media')
- if el.tag == 'style':
- if atype == 'text/sass' or src.endswith('.sass'):
- self.stylesheets.append(SassStylesheetAsset(self, inline=el.text, media=media))
- elif atype == 'text/less' or src.endswith('.less'):
- self.stylesheets.append(LessStylesheetAsset(self, inline=el.text, media=media))
- else:
- self.stylesheets.append(StylesheetAsset(self, inline=el.text, media=media))
- elif el.tag == 'link' and el.get('rel') == 'stylesheet' and self.can_aggregate(href):
- if href.endswith('.sass') or atype == 'text/sass':
- self.stylesheets.append(SassStylesheetAsset(self, url=href, media=media))
- elif href.endswith('.less') or atype == 'text/less':
- self.stylesheets.append(LessStylesheetAsset(self, url=href, media=media))
- else:
- self.stylesheets.append(StylesheetAsset(self, url=href, media=media))
- elif el.tag == 'script' and not src:
- self.javascripts.append(JavascriptAsset(self, inline=el.text))
- elif el.tag == 'script' and self.can_aggregate(src):
- self.javascripts.append(JavascriptAsset(self, url=src))
- else:
- self.remains.append(html.tostring(el))
- else:
- try:
- self.remains.append(html.tostring(el))
- except Exception:
- # notYETimplementederror
- raise NotImplementedError
-
- def can_aggregate(self, url):
- return not urlparse(url).netloc and not url.startswith('/web/content')
-
- def to_html(self, sep=None, css=True, js=True, debug=False, async=False, qwebcontext=None):
- if sep is None:
- sep = '\n '
- response = []
- if debug == 'assets':
- if css and self.stylesheets:
- if not self.is_css_preprocessed():
- self.preprocess_css(debug=debug)
- if self.css_errors:
- msg = '\n'.join(self.css_errors)
- self.stylesheets.append(StylesheetAsset(self, inline=self.css_message(msg)))
- for style in self.stylesheets:
- response.append(style.to_html())
- if js:
- for jscript in self.javascripts:
- response.append(jscript.to_html())
- else:
- if qwebcontext is None:
- qwebcontext = QWebContext(self.env(context={}), {})
- if css and self.stylesheets:
- css_attachments = self.css()
- if not self.css_errors:
- for attachment in css_attachments:
- el = etree.fromstring('<link href="%s" rel="stylesheet"/>' % attachment.url)
- response.append(self.env['ir.qweb'].render_node(el, qwebcontext))
- else:
- msg = '\n'.join(self.css_errors)
- self.stylesheets.append(StylesheetAsset(self, inline=self.css_message(msg)))
- for style in self.stylesheets:
- response.append(style.to_html())
- if js and self.javascripts:
- el = etree.fromstring('<script %s type="text/javascript" src="%s"></script>' % (async and 'async="async"' or '', self.js().url))
- response.append(self.env['ir.qweb'].render_node(el, qwebcontext))
- response.extend(self.remains)
- return sep + sep.join(response)
-
- @lazy_property
- def last_modified(self):
- """Returns last modified date of linked files"""
- return max(itertools.chain(
- (asset.last_modified for asset in self.javascripts),
- (asset.last_modified for asset in self.stylesheets),
- ))
-
- @lazy_property
- def version(self):
- return self.checksum[0:7]
-
- @lazy_property
- def checksum(self):
- """
- Not really a full checksum.
- We compute a SHA1 on the rendered bundle + max linked files last_modified date
- """
- check = self.html + str(self.last_modified)
- return hashlib.sha1(check).hexdigest()
-
- def clean_attachments(self, type):
- """ Takes care of deleting any outdated ir.attachment records associated to a bundle before
- saving a fresh one.
-
- When `type` is css we need to check that we are deleting a different version (and not *any*
- version) because css may be paginated and, therefore, may produce multiple attachments for
- the same bundle's version.
-
- When `type` is js we need to check that we are deleting a different version (and not *any*
- version) because, as one of the creates in `save_attachment` can trigger a rollback, the
- call to `clean_attachments ` is made at the end of the method in order to avoid the rollback
- of an ir.attachment unlink (because we cannot rollback a removal on the filestore), thus we
- must exclude the current bundle.
- """
- ira = self.env['ir.attachment']
- domain = [
- ('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.xmlid, type)), # The wilcards are id, version and pagination number (if any)
- '!', ('url', '=like', '/web/content/%-{}/%'.format(self.version))
- ]
- return ira.sudo().search(domain).unlink()
-
- def get_attachments(self, type, inc=None):
- ira = self.env['ir.attachment']
- domain = [('url', '=like', '/web/content/%%-%s/%s%s.%s' % (self.version, self.xmlid, ('%%' if inc is None else '.%s' % inc), type))]
- return ira.sudo().search(domain, order='name asc')
-
- def save_attachment(self, type, content, inc=None):
- ira = self.env['ir.attachment']
-
- fname = '%s%s.%s' % (self.xmlid, ('' if inc is None else '.%s' % inc), type)
- values = {
- 'name': "/web/content/%s" % type,
- 'datas_fname': fname,
- 'res_model': 'ir.ui.view',
- 'res_id': False,
- 'type': 'binary',
- 'public': True,
- 'datas': content.encode('utf8').encode('base64'),
- }
- attachment = ira.sudo().create(values)
-
- url = '/web/content/%s-%s/%s' % (attachment.id, self.version, fname)
- values = {
- 'name': url,
- 'url': url,
- }
- attachment.write(values)
-
- if self.env.context.get('commit_assetsbundle') is True:
- self.env.cr.commit()
-
- self.clean_attachments(type)
-
- return attachment
-
- def js(self):
- attachments = self.get_attachments('js')
- if not attachments:
- content = ';\n'.join(asset.minify() for asset in self.javascripts)
- return self.save_attachment('js', content)
- return attachments[0]
-
- def css(self):
- attachments = self.get_attachments('css')
- if not attachments:
- # get css content
- css = self.preprocess_css()
- if self.css_errors:
- return
-
- # move up all @import rules to the top
- matches = []
- css = re.sub(self.rx_css_import, lambda matchobj: matches.append(matchobj.group(0)) and '', css)
- matches.append(css)
- css = u'\n'.join(matches)
-
- # split for browser max file size and browser max expression
- re_rules = '([^{]+\{(?:[^{}]|\{[^{}]*\})*\})'
- re_selectors = '()(?:\s*@media\s*[^{]*\{)?(?:\s*(?:[^,{]*(?:,|\{(?:[^}]*\}))))'
- page = []
- pages = [page]
- page_selectors = 0
- for rule in re.findall(re_rules, css):
- selectors = len(re.findall(re_selectors, rule))
- if page_selectors + selectors <= self.max_css_rules:
- page_selectors += selectors
- page.append(rule)
- else:
- pages.append([rule])
- page = pages[-1]
- page_selectors = selectors
- for idx, page in enumerate(pages):
- self.save_attachment("css", ' '.join(page), inc=idx)
- attachments = self.get_attachments('css')
- return attachments
-
- def css_message(self, message):
- # '\A' == css content carriage return
- message = message.replace('\n', '\\A ').replace('"', '\\"')
- return """
- body:before {
- background: #ffc;
- width: 100%%;
- font-size: 14px;
- font-family: monospace;
- white-space: pre;
- content: "%s";
- }
- """ % message
-
- def is_css_preprocessed(self):
- preprocessed = True
- for atype in (SassStylesheetAsset, LessStylesheetAsset):
- outdated = False
- assets = dict((asset.html_url, asset) for asset in self.stylesheets if isinstance(asset, atype))
- if assets:
- assets_domain = [('url', 'in', assets.keys())]
- attachments = self.env['ir.attachment'].sudo().search(assets_domain)
- for attachment in attachments:
- asset = assets[attachment.url]
- if asset.last_modified > fields.Datetime.from_string(attachment['__last_update']):
- outdated = True
- break
- if asset._content is None:
- asset._content = attachment.datas and attachment.datas.decode('base64').decode('utf8') or ''
- if not asset._content and attachment.file_size > 0:
- asset._content = None # file missing, force recompile
-
- if any(asset._content is None for asset in assets.itervalues()):
- outdated = True
-
- if outdated:
- if attachments:
- attachments.unlink()
- preprocessed = False
-
- return preprocessed
-
- def preprocess_css(self, debug=False):
- """
- Checks if the bundle contains any sass/less content, then compiles it to css.
- Returns the bundle's flat css.
- """
- for atype in (SassStylesheetAsset, LessStylesheetAsset):
- assets = [asset for asset in self.stylesheets if isinstance(asset, atype)]
- if assets:
- cmd = assets[0].get_command()
- source = '\n'.join([asset.get_source() for asset in assets])
- compiled = self.compile_css(cmd, source)
-
- fragments = self.rx_css_split.split(compiled)
- at_rules = fragments.pop(0)
- if at_rules:
- # Sass and less moves @at-rules to the top in order to stay css 2.1 compatible
- self.stylesheets.insert(0, StylesheetAsset(self, inline=at_rules))
- while fragments:
- asset_id = fragments.pop(0)
- asset = next(asset for asset in self.stylesheets if asset.id == asset_id)
- asset._content = fragments.pop(0)
-
- if debug:
- try:
- fname = os.path.basename(asset.url)
- url = asset.html_url
- with self.env.cr.savepoint():
- self.env['ir.attachment'].sudo().create(dict(
- datas=asset.content.encode('utf8').encode('base64'),
- mimetype='text/css',
- type='binary',
- name=url,
- url=url,
- datas_fname=fname,
- res_model=False,
- res_id=False,
- ))
-
- if self.env.context.get('commit_assetsbundle') is True:
- self.env.cr.commit()
- except psycopg2.Error:
- pass
-
- return '\n'.join(asset.minify() for asset in self.stylesheets)
-
- def compile_css(self, cmd, source):
- """Sanitizes @import rules, remove duplicates @import rules, then compile"""
- imports = []
-
- def sanitize(matchobj):
- ref = matchobj.group(2)
- line = '@import "%s"%s' % (ref, matchobj.group(3))
- if '.' not in ref and line not in imports and not ref.startswith(('.', '/', '~')):
- imports.append(line)
- return line
- msg = "Local import '%s' is forbidden for security reasons." % ref
- _logger.warning(msg)
- self.css_errors.append(msg)
- return ''
- source = re.sub(self.rx_preprocess_imports, sanitize, source)
-
- try:
- compiler = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
- except Exception:
- msg = "Could not execute command %r" % cmd[0]
- _logger.error(msg)
- self.css_errors.append(msg)
- return ''
- result = compiler.communicate(input=source.encode('utf-8'))
- if compiler.returncode:
- error = self.get_preprocessor_error(''.join(result), source=source)
- _logger.warning(error)
- self.css_errors.append(error)
- return ''
- compiled = result[0].strip().decode('utf8')
- return compiled
-
- def get_preprocessor_error(self, stderr, source=None):
- """Improve and remove sensitive information from sass/less compilator error messages"""
- error = stderr.split('Load paths')[0].replace(' Use --trace for backtrace.', '')
- if 'Cannot load compass' in error:
- error += "Maybe you should install the compass gem using this extra argument:\n\n" \
- " $ sudo gem install compass --pre\n"
- error += "This error occured while compiling the bundle '%s' containing:" % self.xmlid
- for asset in self.stylesheets:
- if isinstance(asset, PreprocessedCSS):
- error += '\n - %s' % (asset.url if asset.url else '<inline sass>')
- return error
-
-
-class WebAsset(object):
- html_url_format = '%s'
-
- def __init__(self, bundle, inline=None, url=None):
- self.id = str(uuid.uuid4())
- self.bundle = bundle
- self.inline = inline
- self.url = url
- self.html_url_args = url
- self.env = bundle.env
- self._content = None
- self._filename = None
- self._ir_attach = None
- name = '<inline asset>' if inline else url
- self.name = "%s defined in bundle '%s'" % (name, bundle.xmlid)
- if not inline and not url:
- raise Exception("An asset should either be inlined or url linked")
-
- @property
- def html_url(self):
- return self.html_url_format % self.html_url_args
-
- def stat(self):
- if not (self.inline or self._filename or self._ir_attach):
- path = filter(None, self.url.split('/'))
- self._filename = get_resource_path(*path)
- if self._filename:
- return
- try:
- # Test url against ir.attachments
- fields = ['__last_update', 'datas', 'mimetype']
- domain = [('type', '=', 'binary'), ('url', '=', self.url)]
- attach = self.env['ir.attachment'].sudo().search_read(domain, fields)
- self._ir_attach = attach[0]
- except Exception:
- raise AssetNotFound("Could not find %s" % self.name)
-
- def to_html(self):
- raise NotImplementedError()
-
- @lazy_property
- def last_modified(self):
- try:
- self.stat()
- if self._filename:
- return datetime.fromtimestamp(os.path.getmtime(self._filename))
- elif self._ir_attach:
- server_format = tools.DEFAULT_SERVER_DATETIME_FORMAT
- last_update = self._ir_attach['__last_update']
- try:
- return datetime.strptime(last_update, server_format + '.%f')
- except ValueError:
- return datetime.strptime(last_update, server_format)
- except Exception:
- pass
- return datetime(1970, 1, 1)
-
- @property
- def content(self):
- if self._content is None:
- self._content = self.inline or self._fetch_content()
- return self._content
-
- def _fetch_content(self):
- """ Fetch content from file or database"""
- try:
- self.stat()
- if self._filename:
- with open(self._filename, 'rb') as fp:
- return fp.read().decode('utf-8')
- else:
- return self._ir_attach['datas'].decode('base64')
- except UnicodeDecodeError:
- raise AssetError('%s is not utf-8 encoded.' % self.name)
- except IOError:
- raise AssetNotFound('File %s does not exist.' % self.name)
- except:
- raise AssetError('Could not get content for %s.' % self.name)
-
- def minify(self):
- return self.content
-
- def with_header(self, content=None):
- if content is None:
- content = self.content
- return '\n/* %s */\n%s' % (self.name, content)
-
-
-class JavascriptAsset(WebAsset):
- def minify(self):
- return self.with_header(rjsmin(self.content))
-
- def _fetch_content(self):
- try:
- return super(JavascriptAsset, self)._fetch_content()
- except AssetError, e:
- return "console.error(%s);" % json.dumps(e.message)
-
- def to_html(self):
- if self.url:
- return '<script type="text/javascript" src="%s"></script>' % (self.html_url)
- else:
- return '<script type="text/javascript" charset="utf-8">%s</script>' % self.with_header()
-
-
-class StylesheetAsset(WebAsset):
- rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
- rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
- rx_sourceMap = re.compile(r'(/\*# sourceMappingURL=.*)', re.U)
- rx_charset = re.compile(r'(@charset "[^"]+";)', re.U)
-
- def __init__(self, *args, **kw):
- self.media = kw.pop('media', None)
- super(StylesheetAsset, self).__init__(*args, **kw)
-
- @property
- def content(self):
- content = super(StylesheetAsset, self).content
- if self.media:
- content = '@media %s { %s }' % (self.media, content)
- return content
-
- def _fetch_content(self):
- try:
- content = super(StylesheetAsset, self)._fetch_content()
- web_dir = os.path.dirname(self.url)
-
- if self.rx_import:
- content = self.rx_import.sub(
- r"""@import \1%s/""" % (web_dir,),
- content,
- )
-
- if self.rx_url:
- content = self.rx_url.sub(
- r"url(\1%s/" % (web_dir,),
- content,
- )
-
- if self.rx_charset:
- # remove charset declarations, we only support utf-8
- content = self.rx_charset.sub('', content)
-
- return content
- except AssetError, e:
- self.bundle.css_errors.append(e.message)
- return ''
-
- def minify(self):
- # remove existing sourcemaps, make no sense after re-mini
- content = self.rx_sourceMap.sub('', self.content)
- # comments
- content = re.sub(r'/\*.*?\*/', '', content, flags=re.S)
- # space
- content = re.sub(r'\s+', ' ', content)
- content = re.sub(r' *([{}]) *', r'\1', content)
- return self.with_header(content)
-
- def to_html(self):
- media = (' media="%s"' % werkzeug.utils.escape(self.media)) if self.media else ''
- if self.url:
- href = self.html_url
- return '<link rel="stylesheet" href="%s" type="text/css"%s/>' % (href, media)
- else:
- return '<style type="text/css"%s>%s</style>' % (media, self.with_header())
-
-
-class PreprocessedCSS(StylesheetAsset):
- rx_import = None
-
- def __init__(self, *args, **kw):
- super(PreprocessedCSS, self).__init__(*args, **kw)
- self.html_url_format = '%%s/%s/%%s.css' % self.bundle.xmlid
- self.html_url_args = tuple(self.url.rsplit('/', 1))
-
- def minify(self):
- return self.with_header()
-
- def get_source(self):
- content = self.inline or self._fetch_content()
- return "/*! %s */\n%s" % (self.id, content)
-
- def get_command(self):
- raise NotImplementedError
-
-
-class SassStylesheetAsset(PreprocessedCSS):
- rx_indent = re.compile(r'^( +|\t+)', re.M)
- indent = None
- reindent = ' '
-
- def get_source(self):
- content = textwrap.dedent(self.inline or self._fetch_content())
-
- def fix_indent(m):
- # Indentation normalization
- ind = m.group()
- if self.indent is None:
- self.indent = ind
- if self.indent == self.reindent:
- # Don't reindent the file if identation is the final one (reindent)
- raise StopIteration()
- return ind.replace(self.indent, self.reindent)
-
- try:
- content = self.rx_indent.sub(fix_indent, content)
- except StopIteration:
- pass
- return "/*! %s */\n%s" % (self.id, content)
-
- def get_command(self):
- try:
- sass = find_in_path('sass')
- except IOError:
- sass = 'sass'
- return [sass, '--stdin', '-t', 'compressed', '--unix-newlines', '--compass',
- '-r', 'bootstrap-sass']
-
-
-class LessStylesheetAsset(PreprocessedCSS):
- def get_command(self):
- try:
- if os.name == 'nt':
- lessc = find_in_path('lessc.cmd')
- else:
- lessc = find_in_path('lessc')
- except IOError:
- lessc = 'lessc'
- lesspath = get_resource_path('web', 'static', 'lib', 'bootstrap', 'less')
- return [lessc, '-', '--clean-css', '--no-js', '--no-color', '--include-path=%s' % lesspath]
-
-
-def rjsmin(script):
- """ Minify js with a clever regex.
- Taken from http://opensource.perlig.de/rjsmin
- Apache License, Version 2.0 """
- def subber(match):
- """ Substitution callback """
- groups = match.groups()
- return (
- groups[0] or
- groups[1] or
- groups[2] or
- groups[3] or
- (groups[4] and '\n') or
- (groups[5] and ' ') or
- (groups[6] and ' ') or
- (groups[7] and ' ') or
- ''
- )
-
- result = re.sub(
- r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
- r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
- r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
- r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
- r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
- r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
- r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
- r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
- r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
- r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
- r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
- r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
- r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
- r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
- r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
- r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
- r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
- r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
- r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
- r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
- r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
- ).strip()
- return result
diff --git a/openerp/addons/base/ir/ir_qweb/__init__.py b/openerp/addons/base/ir/ir_qweb/__init__.py
new file mode 100644
index 00000000000..926d8047576
--- /dev/null
+++ b/openerp/addons/base/ir/ir_qweb/__init__.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+from openerp.tools import safe_eval, html_escape as escape
+
+from . import fields
+from . import assetsbundle
+from . import utils
+
+from .assetsbundle import AssetsBundle
+from .utils import QWebContext, unicodifier
+from .qweb import QWeb
diff --git a/openerp/addons/base/ir/ir_qweb/assetsbundle.py b/openerp/addons/base/ir/ir_qweb/assetsbundle.py
new file mode 100644
index 00000000000..b1b5ee3b27d
--- /dev/null
+++ b/openerp/addons/base/ir/ir_qweb/assetsbundle.py
@@ -0,0 +1,615 @@
+# -*- coding: utf-8 -*-
+import os
+import re
+import hashlib
+import itertools
+import json
+import textwrap
+import uuid
+from datetime import datetime
+from subprocess import Popen, PIPE
+from odoo import fields, tools
+from openerp.http import request
+from odoo.modules.module import get_resource_path
+import psycopg2
+import werkzeug
+from openerp.tools import func, misc
+
+import logging
+_logger = logging.getLogger(__name__)
+
+MAX_CSS_RULES = 4095
+
+
+def rjsmin(script):
+ """ Minify js with a clever regex.
+ Taken from http://opensource.perlig.de/rjsmin
+ Apache License, Version 2.0 """
+ def subber(match):
+ """ Substitution callback """
+ groups = match.groups()
+ return (
+ groups[0] or
+ groups[1] or
+ groups[2] or
+ groups[3] or
+ (groups[4] and '\n') or
+ (groups[5] and ' ') or
+ (groups[6] and ' ') or
+ (groups[7] and ' ') or
+ ''
+ )
+
+ result = re.sub(
+ r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
+ r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
+ r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?:(?<=[(,=:\[!&|?{};\r\n]'
+ r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
+ r'))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
+ r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
+ r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
+ r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
+ r'\\\[\r\n]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]'
+ r'*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*))|(?<=[^\000-!#%&(*,./'
+ r':-@\[\\^`{|~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/'
+ r'*][^*]*\*+)*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\01'
+ r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
+ r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
+ r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
+ r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
+ r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
+ r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
+ r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
+ r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
+ r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
+ ).strip()
+ return result
+
+
+class AssetError(Exception):
+ pass
+
+
+class AssetNotFound(AssetError):
+ pass
+
+
+class AssetsBundle(object):
+ rx_css_import = re.compile("(@import[^;{]+;?)", re.M)
+ rx_preprocess_imports = re.compile("""(@import\s?['"]([^'"]+)['"](;?))""")
+ rx_css_split = re.compile("\/\*\! ([a-f0-9-]+) \*\/")
+
+ def __init__(self, name, files, remains, env=None):
+ self.name = name
+ self.env = request.env if env is None else env
+ self.max_css_rules = self.env.context.get('max_css_rules', MAX_CSS_RULES)
+ self.javascripts = []
+ self.stylesheets = []
+ self.css_errors = []
+ self.remains = []
+ self._checksum = None
+ self.files = files
+ self.remains = remains
+ for f in files:
+ if f['atype'] == 'text/sass':
+ self.stylesheets.append(SassStylesheetAsset(self, url=f['url'], inline=f['content'], media=f['media']))
+ elif f['atype'] == 'text/less':
+ self.stylesheets.append(LessStylesheetAsset(self, url=f['url'], inline=f['content'], media=f['media']))
+ elif f['atype'] == 'text/css':
+ self.stylesheets.append(StylesheetAsset(self, url=f['url'], inline=f['content'], media=f['media']))
+ elif f['atype'] == 'text/javascript':
+ self.javascripts.append(JavascriptAsset(self, url=f['url'], inline=f['content']))
+
+ def to_html(self, sep=None, css=True, js=True, debug=False, async=False, qwebcontext=None):
+ url_for = qwebcontext and qwebcontext.get('url_for', lambda url: url) or (lambda url: url)
+ if sep is None:
+ sep = '\n '
+ response = []
+ if debug == 'assets':
+ if css and self.stylesheets:
+ if not self.is_css_preprocessed():
+ self.preprocess_css(debug=debug)
+ if self.css_errors:
+ msg = '\n'.join(self.css_errors)
+ self.stylesheets.append(StylesheetAsset(self, inline=self.css_message(msg)))
+ for style in self.stylesheets:
+ response.append(style.to_html())
+ if js:
+ for jscript in self.javascripts:
+ response.append(jscript.to_html())
+ else:
+ if css and self.stylesheets:
+ css_attachments = self.css()
+ if not self.css_errors:
+ for attachment in css_attachments:
+ response.append('<link href="%s" rel="stylesheet"/>' % url_for(attachment.url))
+ else:
+ msg = '\n'.join(self.css_errors)
+ self.stylesheets.append(StylesheetAsset(self, inline=self.css_message(msg)))
+ for style in self.stylesheets:
+ response.append(style.to_html())
+ if js and self.javascripts:
+ response.append('<script %s type="text/javascript" src="%s"></script>' % (async and 'async="async"' or '', url_for(self.js().url)))
+ response.extend(self.remains)
+
+ return sep + sep.join(response)
+
+ @func.lazy_property
+ def last_modified(self):
+ """Returns last modified date of linked files"""
+ return max(itertools.chain(
+ (asset.last_modified for asset in self.javascripts),
+ (asset.last_modified for asset in self.stylesheets),
+ ))
+
+ @func.lazy_property
+ def version(self):
+ return self.checksum[0:7]
+
+ @func.lazy_property
+ def checksum(self):
+ """
+ Not really a full checksum.
+ We compute a SHA1 on the rendered bundle + max linked files last_modified date
+ """
+ check = json.dumps(self.files) + ",".join(self.remains) + str(self.last_modified)
+ return hashlib.sha1(check).hexdigest()
+
+ def clean_attachments(self, type):
+ """ Takes care of deleting any outdated ir.attachment records associated to a bundle before
+ saving a fresh one.
+
+ When `type` is css we need to check that we are deleting a different version (and not *any*
+ version) because css may be paginated and, therefore, may produce multiple attachments for
+ the same bundle's version.
+
+ When `type` is js we need to check that we are deleting a different version (and not *any*
+ version) because, as one of the creates in `save_attachment` can trigger a rollback, the
+ call to `clean_attachments ` is made at the end of the method in order to avoid the rollback
+ of an ir.attachment unlink (because we cannot rollback a removal on the filestore), thus we
+ must exclude the current bundle.
+ """
+ ira = self.env['ir.attachment']
+ domain = [
+ ('url', '=like', '/web/content/%-%/{0}%.{1}'.format(self.name, type)), # The wilcards are id, version and pagination number (if any)
+ '!', ('url', '=like', '/web/content/%-{}/%'.format(self.version))
+ ]
+ return ira.sudo().search(domain).unlink()
+
+ def get_attachments(self, type, inc=None):
+ ira = self.env['ir.attachment']
+ domain = [('url', '=like', '/web/content/%%-%s/%s%s.%s' % (self.version, self.name, ('%%' if inc is None else '.%s' % inc), type))]
+ return ira.sudo().search(domain, order='name asc')
+
+ def save_attachment(self, type, content, inc=None):
+ ira = self.env['ir.attachment']
+
+ fname = '%s%s.%s' % (self.name, ('' if inc is None else '.%s' % inc), type)
+ values = {
+ 'name': "/web/content/%s" % type,
+ 'datas_fname': fname,
+ 'res_model': 'ir.ui.view',
+ 'res_id': False,
+ 'type': 'binary',
+ 'public': True,
+ 'datas': content.encode('utf8').encode('base64'),
+ }
+ attachment = ira.sudo().create(values)
+
+ url = '/web/content/%s-%s/%s' % (attachment.id, self.version, fname)
+ values = {
+ 'name': url,
+ 'url': url,
+ }
+ attachment.write(values)
+
+ if self.env.context.get('commit_assetsbundle') is True:
+ self.env.cr.commit()
+
+ self.clean_attachments(type)
+
+ return attachment
+
+ def js(self):
+ attachments = self.get_attachments('js')
+ if not attachments:
+ content = ';\n'.join(asset.minify() for asset in self.javascripts)
+ return self.save_attachment('js', content)
+ return attachments[0]
+
+ def css(self):
+ attachments = self.get_attachments('css')
+ if not attachments:
+ # get css content
+ css = self.preprocess_css()
+ if self.css_errors:
+ return
+
+ # move up all @import rules to the top
+ matches = []
+ css = re.sub(self.rx_css_import, lambda matchobj: matches.append(matchobj.group(0)) and '', css)
+ matches.append(css)
+ css = u'\n'.join(matches)
+
+ # split for browser max file size and browser max expression
+ re_rules = '([^{]+\{(?:[^{}]|\{[^{}]*\})*\})'
+ re_selectors = '()(?:\s*@media\s*[^{]*\{)?(?:\s*(?:[^,{]*(?:,|\{(?:[^}]*\}))))'
+ page = []
+ pages = [page]
+ page_selectors = 0
+ for rule in re.findall(re_rules, css):
+ selectors = len(re.findall(re_selectors, rule))
+ if page_selectors + selectors <= self.max_css_rules:
+ page_selectors += selectors
+ page.append(rule)
+ else:
+ pages.append([rule])
+ page = pages[-1]
+ page_selectors = selectors
+ for idx, page in enumerate(pages):
+ self.save_attachment("css", ' '.join(page), inc=idx)
+ attachments = self.get_attachments('css')
+ return attachments
+
+ def css_message(self, message):
+ # '\A' == css content carriage return
+ message = message.replace('\n', '\\A ').replace('"', '\\"')
+ return """
+ body:before {
+ background: #ffc;
+ width: 100%%;
+ font-size: 14px;
+ font-family: monospace;
+ white-space: pre;
+ content: "%s";
+ }
+ """ % message
+
+ def is_css_preprocessed(self):
+ preprocessed = True
+ for atype in (SassStylesheetAsset, LessStylesheetAsset):
+ outdated = False
+ assets = dict((asset.html_url, asset) for asset in self.stylesheets if isinstance(asset, atype))
+ if assets:
+ assets_domain = [('url', 'in', assets.keys())]
+ attachments = self.env['ir.attachment'].sudo().search(assets_domain)
+ for attachment in attachments:
+ asset = assets[attachment.url]
+ if asset.last_modified > fields.Datetime.from_string(attachment['__last_update']):
+ outdated = True
+ break
+ if asset._content is None:
+ asset._content = attachment.datas and attachment.datas.decode('base64').decode('utf8') or ''
+ if not asset._content and attachment.file_size > 0:
+ asset._content = None # file missing, force recompile
+
+ if any(asset._content is None for asset in assets.itervalues()):
+ outdated = True
+
+ if outdated:
+ if attachments:
+ attachments.unlink()
+ preprocessed = False
+
+ return preprocessed
+
+ def preprocess_css(self, debug=False):
+ """
+ Checks if the bundle contains any sass/less content, then compiles it to css.
+ Returns the bundle's flat css.
+ """
+ for atype in (SassStylesheetAsset, LessStylesheetAsset):
+ assets = [asset for asset in self.stylesheets if isinstance(asset, atype)]
+ if assets:
+ cmd = assets[0].get_command()
+ source = '\n'.join([asset.get_source() for asset in assets])
+ compiled = self.compile_css(cmd, source)
+
+ fragments = self.rx_css_split.split(compiled)
+ at_rules = fragments.pop(0)
+ if at_rules:
+ # Sass and less moves @at-rules to the top in order to stay css 2.1 compatible
+ self.stylesheets.insert(0, StylesheetAsset(self, inline=at_rules))
+ while fragments:
+ asset_id = fragments.pop(0)
+ asset = next(asset for asset in self.stylesheets if asset.id == asset_id)
+ asset._content = fragments.pop(0)
+
+ if debug:
+ try:
+ fname = os.path.basename(asset.url)
+ url = asset.html_url
+ with self.env.cr.savepoint():
+ self.env['ir.attachment'].sudo().create(dict(
+ datas=asset.content.encode('utf8').encode('base64'),
+ mimetype='text/css',
+ type='binary',
+ name=url,
+ url=url,
+ datas_fname=fname,
+ res_model=False,
+ res_id=False,
+ ))
+
+ if self.env.context.get('commit_assetsbundle') is True:
+ self.env.cr.commit()
+ except psycopg2.Error:
+ pass
+
+ return '\n'.join(asset.minify() for asset in self.stylesheets)
+
+ def compile_css(self, cmd, source):
+ """Sanitizes @import rules, remove duplicates @import rules, then compile"""
+ imports = []
+
+ def sanitize(matchobj):
+ ref = matchobj.group(2)
+ line = '@import "%s"%s' % (ref, matchobj.group(3))
+ if '.' not in ref and line not in imports and not ref.startswith(('.', '/', '~')):
+ imports.append(line)
+ return line
+ msg = "Local import '%s' is forbidden for security reasons." % ref
+ _logger.warning(msg)
+ self.css_errors.append(msg)
+ return ''
+ source = re.sub(self.rx_preprocess_imports, sanitize, source)
+
+ try:
+ compiler = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
+ except Exception:
+ msg = "Could not execute command %r" % cmd[0]
+ _logger.error(msg)
+ self.css_errors.append(msg)
+ return ''
+ result = compiler.communicate(input=source.encode('utf-8'))
+ if compiler.returncode:
+ error = self.get_preprocessor_error(''.join(result), source=source)
+ _logger.warning(error)
+ self.css_errors.append(error)
+ return ''
+ compiled = result[0].strip().decode('utf8')
+ return compiled
+
+ def get_preprocessor_error(self, stderr, source=None):
+ """Improve and remove sensitive information from sass/less compilator error messages"""
+ error = stderr.split('Load paths')[0].replace(' Use --trace for backtrace.', '')
+ if 'Cannot load compass' in error:
+ error += "Maybe you should install the compass gem using this extra argument:\n\n" \
+ " $ sudo gem install compass --pre\n"
+ error += "This error occured while compiling the bundle '%s' containing:" % self.name
+ for asset in self.stylesheets:
+ if isinstance(asset, PreprocessedCSS):
+ error += '\n - %s' % (asset.url if asset.url else '<inline sass>')
+ return error
+
+
+class WebAsset(object):
+ html_url_format = '%s'
+
+ def __init__(self, bundle, inline=None, url=None):
+ self.id = str(uuid.uuid4())
+ self.bundle = bundle
+ self.inline = inline
+ self.url = url
+ self.html_url_args = url
+ self.env = bundle.env
+ self._content = None
+ self._filename = None
+ self._ir_attach = None
+ name = '<inline asset>' if inline else url
+ self.name = "%s defined in bundle '%s'" % (name, bundle.name)
+ if not inline and not url:
+ raise Exception("An asset should either be inlined or url linked, defined in bundle '%s'" % bundle.name)
+
+ @property
+ def html_url(self):
+ return self.html_url_format % self.html_url_args
+
+ def stat(self):
+ if not (self.inline or self._filename or self._ir_attach):
+ path = filter(None, self.url.split('/'))
+ self._filename = get_resource_path(*path)
+ if self._filename:
+ return
+ try:
+ # Test url against ir.attachments
+ fields = ['__last_update', 'datas', 'mimetype']
+ domain = [('type', '=', 'binary'), ('url', '=', self.url)]
+ attach = self.env['ir.attachment'].sudo().search_read(domain, fields)
+ self._ir_attach = attach[0]
+ except Exception:
+ raise AssetNotFound("Could not find %s" % self.name)
+
+ def to_html(self):
+ raise NotImplementedError()
+
+ @func.lazy_property
+ def last_modified(self):
+ try:
+ self.stat()
+ if self._filename:
+ return datetime.fromtimestamp(os.path.getmtime(self._filename))
+ elif self._ir_attach:
+ server_format = tools.DEFAULT_SERVER_DATETIME_FORMAT
+ last_update = self._ir_attach['__last_update']
+ try:
+ return datetime.strptime(last_update, server_format + '.%f')
+ except ValueError:
+ return datetime.strptime(last_update, server_format)
+ except Exception:
+ pass
+ return datetime(1970, 1, 1)
+
+ @property
+ def content(self):
+ if self._content is None:
+ self._content = self.inline or self._fetch_content()
+ return self._content
+
+ def _fetch_content(self):
+ """ Fetch content from file or database"""
+ try:
+ self.stat()
+ if self._filename:
+ with open(self._filename, 'rb') as fp:
+ return fp.read().decode('utf-8')
+ else:
+ return self._ir_attach['datas'].decode('base64')
+ except UnicodeDecodeError:
+ raise AssetError('%s is not utf-8 encoded.' % self.name)
+ except IOError:
+ raise AssetNotFound('File %s does not exist.' % self.name)
+ except:
+ raise AssetError('Could not get content for %s.' % self.name)
+
+ def minify(self):
+ return self.content
+
+ def with_header(self, content=None):
+ if content is None:
+ content = self.content
+ return '\n/* %s */\n%s' % (self.name, content)
+
+
+class JavascriptAsset(WebAsset):
+ def minify(self):
+ return self.with_header(rjsmin(self.content))
+
+ def _fetch_content(self):
+ try:
+ return super(JavascriptAsset, self)._fetch_content()
+ except AssetError, e:
+ return "console.error(%s);" % json.dumps(e.message)
+
+ def to_html(self):
+ if self.url:
+ return '<script type="text/javascript" src="%s"></script>' % (self.html_url)
+ else:
+ return '<script type="text/javascript" charset="utf-8">%s</script>' % self.with_header()
+
+
+class StylesheetAsset(WebAsset):
+ rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
+ rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://|data:)""", re.U)
+ rx_sourceMap = re.compile(r'(/\*# sourceMappingURL=.*)', re.U)
+ rx_charset = re.compile(r'(@charset "[^"]+";)', re.U)
+
+ def __init__(self, *args, **kw):
+ self.media = kw.pop('media', None)
+ super(StylesheetAsset, self).__init__(*args, **kw)
+
+ @property
+ def content(self):
+ content = super(StylesheetAsset, self).content
+ if self.media:
+ content = '@media %s { %s }' % (self.media, content)
+ return content
+
+ def _fetch_content(self):
+ try:
+ content = super(StylesheetAsset, self)._fetch_content()
+ web_dir = os.path.dirname(self.url)
+
+ if self.rx_import:
+ content = self.rx_import.sub(
+ r"""@import \1%s/""" % (web_dir,),
+ content,
+ )
+
+ if self.rx_url:
+ content = self.rx_url.sub(
+ r"url(\1%s/" % (web_dir,),
+ content,
+ )
+
+ if self.rx_charset:
+ # remove charset declarations, we only support utf-8
+ content = self.rx_charset.sub('', content)
+
+ return content
+ except AssetError, e:
+ self.bundle.css_errors.append(e.message)
+ return ''
+
+ def minify(self):
+ # remove existing sourcemaps, make no sense after re-mini
+ content = self.rx_sourceMap.sub('', self.content)
+ # comments
+ content = re.sub(r'/\*.*?\*/', '', content, flags=re.S)
+ # space
+ content = re.sub(r'\s+', ' ', content)
+ content = re.sub(r' *([{}]) *', r'\1', content)
+ return self.with_header(content)
+
+ def to_html(self):
+ media = (' media="%s"' % werkzeug.utils.escape(self.media)) if self.media else ''
+ if self.url:
+ href = self.html_url
+ return '<link rel="stylesheet" href="%s" type="text/css"%s/>' % (href, media)
+ else:
+ return '<style type="text/css"%s>%s</style>' % (media, self.with_header())
+
+
+class PreprocessedCSS(StylesheetAsset):
+ rx_import = None
+
+ def __init__(self, *args, **kw):
+ super(PreprocessedCSS, self).__init__(*args, **kw)
+ self.html_url_format = '%%s/%s/%%s.css' % self.bundle.name
+ self.html_url_args = tuple(self.url.rsplit('/', 1))
+
+ def minify(self):
+ return self.with_header()
+
+ def get_source(self):
+ content = self.inline or self._fetch_content()
+ return "/*! %s */\n%s" % (self.id, content)
+
+ def get_command(self):
+ raise NotImplementedError
+
+
+class SassStylesheetAsset(PreprocessedCSS):
+ rx_indent = re.compile(r'^( +|\t+)', re.M)
+ indent = None
+ reindent = ' '
+
+ def get_source(self):
+ content = textwrap.dedent(self.inline or self._fetch_content())
+
+ def fix_indent(m):
+ # Indentation normalization
+ ind = m.group()
+ if self.indent is None:
+ self.indent = ind
+ if self.indent == self.reindent:
+ # Don't reindent the file if identation is the final one (reindent)
+ raise StopIteration()
+ return ind.replace(self.indent, self.reindent)
+
+ try:
+ content = self.rx_indent.sub(fix_indent, content)
+ except StopIteration:
+ pass
+ return "/*! %s */\n%s" % (self.id, content)
+
+ def get_command(self):
+ try:
+ sass = misc.find_in_path('sass')
+ except IOError:
+ sass = 'sass'
+ return [sass, '--stdin', '-t', 'compressed', '--unix-newlines', '--compass',
+ '-r', 'bootstrap-sass']
+
+
+class LessStylesheetAsset(PreprocessedCSS):
+ def get_command(self):
+ try:
+ if os.name == 'nt':
+ lessc = misc.find_in_path('lessc.cmd')
+ else:
+ lessc = misc.find_in_path('lessc')
+ except IOError:
+ lessc = 'lessc'
+ lesspath = get_resource_path('web', 'static', 'lib', 'bootstrap', 'less')
+ return [lessc, '-', '--clean-css', '--no-js', '--no-color', '--include-path=%s' % lesspath]
diff --git a/openerp/addons/base/ir/ir_qweb/fields.py b/openerp/addons/base/ir/ir_qweb/fields.py
new file mode 100644
index 00000000000..a1b6b32f281
--- /dev/null
+++ b/openerp/addons/base/ir/ir_qweb/fields.py
@@ -0,0 +1,463 @@
+# -*- coding: utf-8 -*-
+import re
+from collections import OrderedDict
+from odoo import api, fields, models, _
+from PIL import Image
+from cStringIO import StringIO
+import babel
+from openerp.tools import html_escape as escape, posix_to_ldml
+from .utils import QWebContext, eval_expr, unicodifier
+
+import logging
+_logger = logging.getLogger(__name__)
+
+
+def nl2br(string):
+ """ Converts newlines to HTML linebreaks in ``string``. returns
+ the unicode result
+
+ :param str string:
+ :rtype: unicode
+ """
+ return unicodifier(string).replace(u'\n', u'<br>\n')
+
+def html_escape(string, options):
+ """ Automatically escapes content unless options['html-escape']
+ is set to False
+
+ :param str string:
+ :param dict options:
+ """
+ return escape(string) if not options or options.get('html-escape', True) else string
+
+#--------------------------------------------------------------------
+# QWeb Fields converters
+#--------------------------------------------------------------------
+
+class FieldConverter(models.AbstractModel):
+ """ Used to convert a t-field specification into an output HTML field.
+
+ :meth:`~.to_html` is the entry point of this conversion from QWeb, it:
+
+ * converts the record value to html using :meth:`~.record_to_html`
+ * generates the metadata attributes (``data-oe-``) to set on the root
+ result node
+ * generates the root result node itself through :meth:`~.render_element`
+ """
+ _name = 'ir.qweb.field'
+
+ @api.model
+ def attributes(self, record, field_name, options, qwebcontext=None):
+ """ attributes(record, field_name, field, options, qwebcontext)
+
+ Generates the metadata attributes (prefixed by ``data-oe-``) for the
+ root node of the field conversion.
+
+ The default attributes are:
+
+ * ``model``, the name of the record's model
+ * ``id`` the id of the record to which the field belongs
+ * ``type`` the logical field type (widget, may not match the field's
+ ``type``, may not be any Field subclass name)
+ * ``translate``, a boolean flag (``0`` or ``1``) denoting whether the
+ field is translatable
+ * ``readonly``, has this attribute if the field is readonly
+ * ``expression``, the original expression
+
+ :returns: OrderedDict (attribute name, attribute value).
+ """
+ data = OrderedDict()
+ field = record._fields[field_name]
+
+ if not options['inherit_branding'] and not options['translate']:
+ return data
+
+ data['data-oe-model'] = record._name
+ data['data-oe-id'] = record.id
+ data['data-oe-field'] = field.name
+ data['data-oe-type'] = options.get('type')
+ data['data-oe-expression'] = options.get('expression')
+ if field.readonly:
+ data['data-oe-readonly'] = 1
+ return data
+
+ @api.model
+ def value_to_html(self, value, field, options=None):
+ """ value_to_html(value, field, options=None)
+
+ Converts a single value to its HTML version/output
+ :rtype: unicode
+ """
+ return html_escape(unicodifier(value) or u'', options)
+
+ @api.model
+ def record_to_html(self, record, field_name, options, qwebcontext=None):
+ """ record_to_html(record, field_name, options, qwebcontext)
+
+ Converts the specified field of the browse_record ``record`` to HTML
+ :rtype: unicode
+ """
+ field = record._fields[field_name]
+
+ try:
+ value = record[field_name]
+ return False if value is False else record.env[self._name].value_to_html(value, field, options=options)
+ except Exception:
+ _logger.warning("Could not get field %s for model %s",
+ field_name, record._name, exc_info=True)
+ return None
+
+ @api.model
+ def user_lang(self):
+ """ user_lang()
+
+ Fetches the res.lang record corresponding to the language code stored
+ in the user's context. Fallbacks to en_US if no lang is present in the
+ context *or the language code is not valid*.
+
+ :returns: res.lang browse_record
+ """
+ lang_code = self._context.get('lang') or 'en_US'
+ return self.env['res.lang']._lang_get(lang_code)
+
+
+class IntegerConverter(models.AbstractModel):
+ _name = 'ir.qweb.field.integer'
+ _inherit = 'ir.qweb.field'
+
+ @api.model
+ def value_to_html(self, value, field, options=None):
+ return unicodifier(self.user_lang().format('%d', value, grouping=True))
+
+
+class FloatConverter(models.AbstractModel):
+ _name = 'ir.qweb.field.float'
+ _inherit = 'ir.qweb.field'
+
+ @api.model
+ def precision(self, field, options=None):
+ _, precision = field.digits or (None, None)
+ return precision
+
+ @api.model
+ def value_to_html(self, value, field, options=None):
+ precision = self.precision(field, options=options)
+ fmt = '%f' if precision is None else '%.{precision}f'
+ lang = self.user_lang()
+ formatted = lang.format(fmt.format(precision=precision), value, grouping=True)
+
+ # %f does not strip trailing zeroes. %g does but its precision causes
+ # it to switch to scientific notation starting at a million *and* to
+ # strip decimals. So use %f and if no precision was specified manually
+ # strip trailing 0.
+ if precision is None:
+ formatted = re.sub(r'(?:(0|\d+?)0+)$', r'\1', formatted)
+ return unicodifier(formatted)
+
+
+class DateConverter(models.AbstractModel):
+ _name = 'ir.qweb.field.date'
+ _inherit = 'ir.qweb.field'
+
+ @api.model
+ def value_to_html(self, value, field, options=None):
+ if not value or len(value) < 10:
+ return ''
+ lang = self.user_lang()
+ locale = babel.Locale.parse(lang.code)
+
+ if isinstance(value, basestring):
+ value = fields.Datetime.from_string(value[:10])
+
+ if options and 'format' in options:
+ pattern = options['format']
+ else:
+ strftime_pattern = lang.date_format
+ pattern = posix_to_ldml(strftime_pattern, locale=locale)
+
+ return babel.dates.format_date(value, format=pattern, locale=locale)
+
+
+class DateTimeConverter(models.AbstractModel):
+ _name = 'ir.qweb.field.datetime'
+ _inherit = 'ir.qweb.field'
+
+ @api.model
+ def value_to_html(self, value, field, options=None):
+ if not value:
+ return ''
+ lang = self.user_lang()
+ locale = babel.Locale.parse(lang.code)
+
+ if isinstance(value, basestring):
+ value = fields.Datetime.from_string(value)
+
+ value = fields.Datetime.context_timestamp(self, value)
+
+ if options and 'format' in options:
+ pattern = options['format']
+ else:
+ strftime_pattern = (u"%s %s" % (lang.date_format, lang.time_format))
+ pattern = posix_to_ldml(strftime_pattern, locale=locale)
+
+ if options and options.get('hide_seconds'):
+ pattern = pattern.replace(":ss", "").replace(":s", "")
+
+ return unicodifier(babel.dates.format_datetime(value, format=pattern, locale=locale))
+
+
+class TextConverter(models.AbstractModel):
+ _name = 'ir.qweb.field.text'
+ _inherit = 'ir.qweb.field'
+
+ @api.model
+ def value_to_html(self, value, field, options=None):
+ """
+ Escapes the value and converts newlines to br. This is bullshit.
+ """
+ return nl2br(html_escape(value, options)) if value else ''
+
+
+class SelectionConverter(models.AbstractModel):
+ _name = 'ir.qweb.field.selection'
+ _inherit = 'ir.qweb.field'
+
+ @api.model
+ def value_to_html(self, value, field, options=None):
+ if not value:
+ return ''
+ selection = dict(field.get_description(self.env)['selection'])
+ return html_escape(unicodifier(selection[value]) or u'', options)
+
+
+class ManyToOneConverter(models.AbstractModel):
+ _name = 'ir.qweb.field.many2one'
+ _inherit = 'ir.qweb.field'
+
+ @api.model
+ def record_to_html(self, record, field_name, options, qwebcontext=None):
+ value = record[field_name]
+ if not value:
+ return False
+ value = value.sudo().display_name
+ if not value:
+ return False
+ return nl2br(html_escape(value, options)) if value else ''
+
+
+class HTMLConverter(models.AbstractModel):
+ _name = 'ir.qweb.field.html'
+ _inherit = 'ir.qweb.field'
+
+ @api.model
+ def value_to_html(self, value, field, options=None):
+ return unicodifier(value) or u''
+
+
+class ImageConverter(models.AbstractModel):
+ """ ``image`` widget rendering, inserts a data:uri-using image tag in the
+ document. May be overridden by e.g. the website module to generate links
+ instead.
+
+ .. todo:: what happens if different output need different converters? e.g.
+ reports may need embedded images or FS links whereas website
+ needs website-aware
+ """
+ _name = 'ir.qweb.field.image'
+ _inherit = 'ir.qweb.field'
+
+ @api.model
+ def value_to_html(self, value, field, options=None):
+ try:
+ image = Image.open(StringIO(value.decode('base64')))
+ image.verify()
+ except IOError:
+ raise ValueError("Non-image binary fields can not be converted to HTML")
+ except: # image.verify() throws "suitable exceptions", I have no idea what they are
+ raise ValueError("Invalid image content")
+
+ return unicodifier('<img src="data:%s;base64,%s">' % (Image.MIME[image.format], value))
+
+
+class MonetaryConverter(models.AbstractModel):
+ """ ``monetary`` converter, has a mandatory option
+ ``display_currency`` only if field is not of type Monetary.
+ Otherwise, if we are in presence of a monetary field, the field definition must
+ have a currency_field attribute set.
+
+ The currency is used for formatting *and rounding* of the float value. It
+ is assumed that the linked res_currency has a non-empty rounding value and
+ res.currency's ``round`` method is used to perform rounding.
+
+ .. note:: the monetary converter internally adds the qweb context to its
+ options mapping, so that the context is available to callees.
+ It's set under the ``_qwebcontext`` key.
+ """
+ _name = 'ir.qweb.field.monetary'
+ _inherit = 'ir.qweb.field'
+
+ @api.model
+ def record_to_html(self, record, field_name, options, qwebcontext=None):
+ field = record._fields[field_name]
+ if qwebcontext is None:
+ qwebcontext = QWebContext(self.env, qwebcontext or {})
+
+ display_currency = False
+ #currency should be specified by monetary field
+ if field.type == 'monetary' and field.currency_field:
+ display_currency = record[field.currency_field]
+ #otherwise fall back to old method
+ if not display_currency:
+ display_currency = self.display_currency(options['display_currency'], qwebcontext)
+
+ # lang.format mandates a sprintf-style format. These formats are non-
+ # minimal (they have a default fixed precision instead), and
+ # lang.format will not set one by default. currency.round will not
+ # provide one either. So we need to generate a precision value
+ # (integer > 0) from the currency's rounding (a float generally < 1.0).
+ fmt = "%.{0}f".format(display_currency.decimal_places)
+
+ from_amount = record[field_name]
+
+ if options.get('from_currency'):
+ from_currency = self.display_currency(options['from_currency'], qwebcontext)
+ from_amount = from_currency.compute(from_amount, display_currency)
+
+ lang = self.user_lang()
+ formatted_amount = lang.format(fmt, display_currency.round(from_amount),
+ grouping=True, monetary=True).replace(r' ', u'\N{NO-BREAK SPACE}')
+
+ pre = post = u''
+ if display_currency.position == 'before':
+ pre = u'{symbol}\N{NO-BREAK SPACE}'
+ else:
+ post = u'\N{NO-BREAK SPACE}{symbol}'
+
+ return u'{pre}<span class="oe_currency_value">{0}</span>{post}'.format(
+ formatted_amount, pre=pre, post=post,
+ ).format(
+ symbol=display_currency.symbol,
+ )
+
+ @api.model
+ def display_currency(self, currency, qwebcontext):
+ return eval_expr(currency, qwebcontext)
+
+
+TIMEDELTA_UNITS = (
+ ('year', 3600 * 24 * 365),
+ ('month', 3600 * 24 * 30),
+ ('week', 3600 * 24 * 7),
+ ('day', 3600 * 24),
+ ('hour', 3600),
+ ('minute', 60),
+ ('second', 1)
+)
+
+
+class DurationConverter(models.AbstractModel):
+ """ ``duration`` converter, to display integral or fractional values as
+ human-readable time spans (e.g. 1.5 as "1 hour 30 minutes").
+
+ Can be used on any numerical field.
+
+ Has a mandatory option ``unit`` which can be one of ``second``, ``minute``,
+ ``hour``, ``day``, ``week`` or ``year``, used to interpret the numerical
+ field value before converting it.
+
+ Sub-second values will be ignored.
+ """
+ _name = 'ir.qweb.field.duration'
+ _inherit = 'ir.qweb.field'
+
+ @api.model
+ def value_to_html(self, value, field, options=None):
+ units = dict(TIMEDELTA_UNITS)
+ if value < 0:
+ raise ValueError(_("Durations can't be negative"))
+ if not options or options.get('unit') not in units:
+ raise ValueError(_("A unit must be provided to duration widgets"))
+
+ locale = babel.Locale.parse(self.user_lang().code)
+ factor = units[options['unit']]
+
+ sections = []
+ r = value * factor
+ for unit, secs_per_unit in TIMEDELTA_UNITS:
+ v, r = divmod(r, secs_per_unit)
+ if not v:
+ continue
+ section = babel.dates.format_timedelta(
+ v*secs_per_unit, threshold=1, locale=locale)
+ if section:
+ sections.append(section)
+ return u' '.join(sections)
+
+
+class RelativeDatetimeConverter(models.AbstractModel):
+ _name = 'ir.qweb.field.relative'
+ _inherit = 'ir.qweb.field'
+
+ @api.model
+ def value_to_html(self, value, field, options=None):
+ locale = babel.Locale.parse(self.user_lang().code)
+
+ if isinstance(value, basestring):
+ value = fields.Datetime.from_string(value)
+
+ # value should be a naive datetime in UTC. So is fields.Datetime.now()
+ reference = fields.Datetime.from_string(field.now())
+
+ return unicodifier(babel.dates.format_timedelta(value - reference, add_direction=True, locale=locale))
+
+
+class Contact(models.AbstractModel):
+ _name = 'ir.qweb.field.contact'
+ _inherit = 'ir.qweb.field.many2one'
+
+ @api.model
+ def record_to_html(self, record, field_name, options, qwebcontext=None):
+ opf = options and options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
+
+ value_rec = record[field_name]
+ if not value_rec:
+ return None
+ value_rec = value_rec.sudo().with_context(show_address=True)
+ value = value_rec.name_get()[0][1]
+
+ val = {
+ 'name': value.split("\n")[0],
+ 'address': escape("\n".join(value.split("\n")[1:])).strip(),
+ 'phone': value_rec.phone,
+ 'mobile': value_rec.mobile,
+ 'fax': value_rec.fax,
+ 'city': value_rec.city,
+ 'country_id': value_rec.country_id.display_name,
+ 'website': value_rec.website,
+ 'email': value_rec.email,
+ 'fields': opf,
+ 'object': value_rec,
+ 'options': options
+ }
+
+ return unicodifier(self.env.ref('base.contact').render(val, engine='ir.qweb'))
+
+
+class QwebView(models.AbstractModel):
+ _name = 'ir.qweb.field.qweb'
+ _inherit = 'ir.qweb.field.many2one'
+
+ @api.model
+ def record_to_html(self, record, field_name, options, qwebcontext=None):
+ if not getattr(record, field_name):
+ return None
+
+ view = getattr(record, field_name)
+
+ if view._name != "ir.ui.view":
+ _logger.warning("%s.%s must be a 'ir.ui.view' model." % (record, field_name))
+ return None
+
+ view = view.with_context(object=record)
+
+ return unicodifier(view.render(view._context, engine='ir.qweb'))
diff --git a/openerp/addons/base/ir/ir_qweb/ir_qweb.py b/openerp/addons/base/ir/ir_qweb/ir_qweb.py
new file mode 100644
index 00000000000..141ec916d91
--- /dev/null
+++ b/openerp/addons/base/ir/ir_qweb/ir_qweb.py
@@ -0,0 +1,266 @@
+# -*- coding: utf-8 -*-
+import ast
+from urlparse import urlparse
+from lxml import html
+import math
+
+from .qweb import QWeb, Contextifier
+from .assetsbundle import AssetsBundle
+from lxml import etree
+
+from openerp import api, models, tools
+from openerp.tools import safe_eval
+from openerp.addons.web.http import request
+import json
+
+import logging
+_logger = logging.getLogger(__name__)
+
+
+class ir_QWeb(models.AbstractModel, QWeb):
+ """ Base QWeb rendering engine
+ * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
+ create new models called :samp:`ir.qweb.field.{widget}`
+ Beware that if you need extensions or alterations which could be
+ incompatible with other subsystems, you should create a local object
+ inheriting from ``ir.qweb`` and customize that.
+ """
+
+ _name = 'ir.qweb'
+
+ @api.model
+ def render(self, id_or_xml_id, values=None, **options):
+ """ render(id_or_xml_id, values=None, **options)
+ Renders the template specified by the provided template name
+ :param values: context for rendering the template
+ :type values: dict or :class:`values` instance
+ """
+ for method in dir(self):
+ if method.startswith('render_'):
+ _logger.warning("Unused method '%s' is found in ir.qweb." % method)
+
+ body = []
+
+ import time
+ _ = []
+ ap = _.append
+ t = time.time()
+ def append(data):
+ ap([time.time()-t, data])
+ body.append(data)
+
+ context = dict(self.env.context, dev_mode='qweb' in tools.config['dev_mode'])
+ context.update(options)
+
+ self.compile(id_or_xml_id, context)(self, append, values or {})
+
+ # t = _[0][0]
+ # for v in _:
+ # print round((v[0]-t)*1000000)/1000, v[1]
+
+ return u''.join(body)
+
+ # assume cache will be invalidated by third party on write to ir.ui.view
+ def _get_template_cache_keys(self):
+ """ Return the list of context keys to use for caching ``_get_template``. """
+ return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations']
+
+ # apply ormcache_context decorator unless in dev mode...
+ @tools.conditional(
+ 'xml' not in tools.config['dev_mode'],
+ tools.ormcache('id_or_xml_id', 'tuple(map(options.get, self._get_template_cache_keys()))'),
+ )
+ def compile(self, id_or_xml_id, options):
+ return super(ir_QWeb, self).compile(id_or_xml_id, options=options)
+
+ def load(self, name, options):
+ lang = options.get('lang', 'en_US')
+ env = self.env
+ if lang != env.context.get('lang'):
+ env = env(context=dict(env.context, lang=lang))
+ template = env['ir.ui.view'].read_template(name)
+
+ res_id = isinstance(name, (int, long)) and name or None
+ if res_id:
+ for node in etree.fromstring(template):
+ if node.get('t-name'):
+ return node
+ elif res_id and node.tag == "t":
+ node.set('t-name', str(res_id))
+ return node
+
+ return template
+
+ # order
+
+ def _directives_eval_order(self):
+ directives = super(ir_QWeb, self)._directives_eval_order()
+ directives.insert(directives.index('call'), 'lang')
+ directives.insert(directives.index('field'), 'call-assets')
+ return directives
+
+ # compile directives
+
+ def _compile_directive_lang(self, el, options):
+ el.set('t-call-options', '{"lang": %s}' % el.attrib.pop('t-lang', 'en_US'))
+ return self._compile_node(el, options)
+
+ def _compile_directive_call_assets(self, el, options):
+ """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
+ if len(el):
+ raise "t-call-assets cannot contain children nodes"
+
+ # self._get_asset(xmlid, options).to_html(css, js, debug, async, values)
+ return [
+ self._append(ast.Call(
+ func=ast.Attribute(
+ value=ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='self', ctx=ast.Load()),
+ attr='_get_asset',
+ ctx=ast.Load()
+ ),
+ args=[
+ ast.Str(el.get('t-call-assets')),
+ ast.Name(id='options', ctx=ast.Load()),
+ ],
+ keywords=[], starargs=None, kwargs=None
+ ),
+ attr='to_html',
+ ctx=ast.Load()
+ ),
+ args=[],
+ keywords=[
+ ast.keyword('css', self._get_attr_bool(el.get('t-css', True))),
+ ast.keyword('js', self._get_attr_bool(el.get('t-js', True))),
+ ast.keyword('debug', ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='values', ctx=ast.Load()),
+ attr='get',
+ ctx=ast.Load()
+ ),
+ args=[ast.Str('debug')],
+ keywords=[], starargs=None, kwargs=None
+ )),
+ ast.keyword('async', self._get_attr_bool(el.get('async', False))),
+ ast.keyword('values', ast.Name(id='values', ctx=ast.Load())),
+ ],
+ starargs=None, kwargs=None
+ ))
+ ]
+
+ # method called by computing code
+
+ def _get_asset(self, xmlid, options):
+ files, remains = self._get_asset_content(xmlid, options)
+ return AssetsBundle(xmlid, files, remains, env=self.env)
+
+ @tools.ormcache('xmlid', 'options["lang"]')
+ def _get_asset_content(self, xmlid, options):
+ options = dict(options,
+ inherit_branding=False, inherit_branding_auto=False,
+ edit_translations=False, translatable=False,
+ rendering_bundle=True)
+
+ env = self.env(context=options)
+
+ # TODO: This helper can be used by any template that wants to embedd the backend.
+ # It is currently necessary because the ir.ui.view bundle inheritance does not
+ # match the module dependency graph.
+ def get_modules_order():
+ if request:
+ from odoo.addons.web.controllers.main import module_boot
+ return json.dumps(module_boot())
+ return '[]'
+ template = env['ir.qweb'].render(xmlid, {"get_modules_order": get_modules_order})
+
+ files = []
+ remains = []
+ for el in html.fragments_fromstring(template):
+ if isinstance(el, basestring):
+ remains.append(el)
+ elif isinstance(el, html.HtmlElement):
+ href = el.get('href', '')
+ atype = el.get('type')
+ media = el.get('media')
+
+ can_aggregate = not urlparse(href).netloc and not href.startswith('/web/content')
+ if el.tag == 'style' or (el.tag == 'link' and el.get('rel') == 'stylesheet' and can_aggregate):
+ if href.endswith('.sass'):
+ atype = 'text/sass'
+ elif href.endswith('.less'):
+ atype = 'text/less'
+ if atype not in ('text/less', 'text/sass'):
+ atype = 'text/css'
+ files.append({'atype': atype, 'url': href, 'content': el.text, 'media': media})
+ elif el.tag == 'script':
+ atype = 'text/javascript'
+ files.append({'atype': atype, 'url': el.get('src', ''), 'content': el.text, 'media': media})
+ else:
+ remains.append(html.tostring(el))
+ else:
+ try:
+ remains.append(html.tostring(el))
+ except Exception:
+ # notYETimplementederror
+ raise NotImplementedError
+
+ return (files, remains)
+
+ def _get_field(self, record, field_name, expression, default_content, tagName, field_options, options, values):
+ field = record._fields[field_name]
+
+ field_options['tagName'] = tagName
+ field_options['expression'] = expression
+ field_options['default_content'] = default_content
+ field_options['type'] = field_options.get('widget', field.type)
+ inherit_branding = options.get('inherit_branding', options.get('inherit_branding_auto') and record.check_access_rights('write', False))
+ field_options['inherit_branding'] = inherit_branding
+ translate = options.get('edit_translations') and options.get('translatable') and getattr(field, 'translate', False)
+ field_options['translate'] = translate
+
+ # field converter
+ model = 'ir.qweb.field.' + field_options['type']
+ converter = self.env[model] if model in self.env else self.env['ir.qweb.field']
+
+ # get content
+ content = converter.record_to_html(record, field_name, field_options, values)
+ attributes = converter.attributes(record, field_name, field_options, values)
+
+ return (attributes, content or default_content or (u"" if inherit_branding or translate else None))
+
+ # formating methods
+
+ def _format_func_monetary(self, value, display_currency=None, from_currency=None):
+ env = display_currency.env
+ precision = int(round(math.log10(display_currency.rounding)))
+ fmt = "%.{0}f".format(-precision if precision < 0 else 0)
+ lang = env['res.lang']._lang_get(env.context.get('lang', 'en_US'))
+ formatted_amount = lang.format(fmt, value, grouping=True, monetary=True).replace(r' ', u'\N{NO-BREAK SPACE}')
+ pre = post = u''
+ if display_currency.position == 'before':
+ pre = u'{symbol}\N{NO-BREAK SPACE}'
+ else:
+ post = u'\N{NO-BREAK SPACE}{symbol}'
+ return u'{pre}{0}{post}'.format(
+ formatted_amount, pre=pre, post=post
+ ).format(symbol=display_currency.symbol,)
+
+ # compile expression add safe_eval
+
+ def _compile_expr(self, expr):
+ """ Compiles a purported Python expression to ast, verifies that it's safe
+ (according to safe_eval's semantics) and alter its variable references to
+ access values data instead
+ """
+ # string must be stripped otherwise whitespace before the start for
+ # formatting purpose are going to break parse/compile
+ st = ast.parse(expr.strip(), mode='eval')
+ safe_eval.assert_valid_codeobj(
+ safe_eval._SAFE_OPCODES,
+ compile(st, '<>', 'eval'), # could be expr, but eval *should* be fine
+ expr
+ )
+
+ # ast.Expression().body -> expr
+ return Contextifier().visit(st).body
diff --git a/openerp/addons/base/ir/ir_qweb/qweb.py b/openerp/addons/base/ir/ir_qweb/qweb.py
new file mode 100644
index 00000000000..8e6452b0fa8
--- /dev/null
+++ b/openerp/addons/base/ir/ir_qweb/qweb.py
@@ -0,0 +1,972 @@
+# -*- coding: utf-8 -*-
+
+import ast
+from collections import OrderedDict
+import json
+from urlparse import urlparse
+from lxml import etree, html
+import re
+import itertools
+import textwrap
+import traceback
+
+import openerp.tools
+from odoo.exceptions import QWebException
+
+from openerp.tools import html_escape
+from . import utils
+from .utils import QWebContext, unicodifier
+from .assetsbundle import AssetsBundle
+
+from odoo import models
+
+try:
+ import astor
+except ImportError:
+ astor = None
+
+import logging
+_logger = logging.getLogger(__name__)
+
+
+class QWeb(models.AbstractModel):
+ """ Base QWeb rendering engine
+ * to customize ``t-field`` rendering, subclass ``ir.qweb.field`` and
+ create new models called :samp:`ir.qweb.field.{widget}`
+ * alternatively, override :meth:`~.get_field_for` and return an
+ arbitrary model to use as field converter
+ Beware that if you need extensions or alterations which could be
+ incompatible with other subsystems, you should create a local object
+ inheriting from ``ir.qweb`` and customize that.
+ """
+
+ _name = 'ir.qweb'
+
+ def get_template(self, name, loader, origin_template=None):
+ try:
+ document = loader(name)
+ except ValueError:
+ raise QWebException("%s\nLoader could not find template %r" % (traceback.format_exc(), name), template=origin_template)
+ else:
+ if document is not None:
+ if hasattr(document, 'documentElement'):
+ dom = document
+ elif document.startswith("<?xml"):
+ dom = etree.fromstring(document)
+ else:
+ dom = etree.parse(document).getroot()
+
+ res_id = isinstance(name, (int, long)) and name or None
+ for node in dom:
+ if node.get('t-name') or (res_id and node.tag == "t"):
+ return node
+
+ raise QWebException("%s\nTemplate %r not found" % (traceback.format_exc(), name), template=origin_template)
+
+ def render(self, cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None):
+ """ render(cr, uid, id_or_xml_id, qwebcontext=None, loader=None, context=None)
+ Renders the template specified by the provided template name
+ :param qwebcontext: context for rendering the template
+ :type qwebcontext: dict or :class:`QWebContext` instance
+ :param loader: if ``qwebcontext`` is a dict, loader set into the
+ context instantiated for rendering
+ """
+ # noinspection PyMethodFirstArgAssignment
+ self = self.browse(cr, uid, [], context=context)
+ if qwebcontext is None:
+ qwebcontext = {}
+
+ if not isinstance(qwebcontext, QWebContext):
+ qwebcontext = QWebContext(self.env, qwebcontext, loader=loader)
+
+ qwebcontext['__template__'] = id_or_xml_id
+ stack = qwebcontext.get('__stack__', [])
+ if stack:
+ qwebcontext['__caller__'] = stack[-1]
+ stack.append(id_or_xml_id)
+ qwebcontext['__stack__'] = stack
+ qwebcontext['xmlid'] = str(stack[0]) # Temporary fix
+
+ template_function = self._get_template(id_or_xml_id, qwebcontext)
+
+ for method in dir(self):
+ if method.startswith('render_'):
+ _logger.warning("Method '%s' found in ir.qweb, please remove it." % method)
+
+ return template_function(qwebcontext).encode('utf-8')
+
+ def get_field_for(self, field_type, cr=None, uid=None, context=None):
+ """ returns a :class:`~openerp.models.Model` used to render a
+ ``t-field``.
+ By default, tries to get the model named
+ :samp:`ir.qweb.field.{field_type}`, falling back on ``ir.qweb.field``.
+ :param str field_type: type or widget of field to render
+ :param cursor cr
+ :param int uid
+ :param dict context
+ """
+ model = 'ir.qweb.field.' + field_type
+ env = self.env(cr=cr, user=uid, context=context)
+ return env[model] if model in env else env['ir.qweb.field']
+
+ def format(value, formating, *args, **kwargs):
+ format = getattr(self, '_format_func_%s' % formating, None)
+ if not format:
+ raise "Unknown formating '%s'" % (formating,)
+ return format(value, *args, **kwargs)
+
+ def _format_func_monetary(self, value, display_currency=None, from_currency=None):
+ precision = int(round(math.log10(display_currency.rounding)))
+ fmt = "%.{0}f".format(-precision if precision < 0 else 0)
+ lang_code = self.env.context.get('lang') or 'en_US'
+ lang = self.env['res.lang']._lang_get(lang_code)
+ formatted_amount = lang.format(fmt, value, grouping=True, monetary=True)
+ pre = post = u''
+ if display_currency.position == 'before':
+ pre = u'{symbol}\N{NO-BREAK SPACE}'
+ else:
+ post = u'\N{NO-BREAK SPACE}{symbol}'
+ return u'{pre}{0}{post}'.format(
+ formatted_amount, pre=pre, post=post
+ ).format(symbol=display_currency.symbol,)
+
+
+ _void_elements = frozenset([
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'keygen',
+ 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr'])
+
+ _name_gen = itertools.count()
+
+ def compile(self, element):
+ xmlid = element.get('t-name', 'unknown')
+ mod = utils.base_module()
+
+ string = etree.tostring(element)
+
+ try:
+ ast_calls = self._compile_document(element)
+ except Exception, e:
+ raise QWebException("%s%s\nError when compiling AST" % (e, traceback.format_exc()), template=xmlid)
+
+ mod.body.extend(ast_calls)
+ ast.fix_missing_locations(mod)
+
+ def formatException(e, message):
+ if isinstance(e, QWebException):
+ return e
+ stack = traceback.format_exc()
+
+ if astor:
+ code = astor.to_source(mod)
+ else:
+ code = "Please install astor to display the compiled code"
+ stack += "\nInstall `astor` for compiled source information."
+
+ return QWebException(
+ "%s%s\n%s\nTemplate: %s\nCompiled code:\n%s\nTemplate:\n%s" % (
+ e, stack, message, xmlid, code, string), template=xmlid)
+
+ try:
+ # noinspection PyBroadException
+ ns = {}
+ eval(compile(mod, '<template>', 'exec'), ns)
+ except Exception, e:
+ raise formatException(e, "Error when compiling AST")
+
+ def _compiled_fn(qw):
+ try:
+ return ns[ast_calls[-1].name](self, qw)
+ except Exception, e:
+ raise formatException(e, "Error to render compiling AST")
+
+ return _compiled_fn
+
+ def _append(self, item):
+ assert isinstance(item, ast.expr) or isinstance(item, ast.Expr)
+ return ast.Expr(ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='output', ctx=ast.Load()),
+ attr='append',
+ ctx=ast.Load()
+ ), args=[item], keywords=[],
+ starargs=None, kwargs=None
+ ))
+
+ def _extend(self, items):
+ return ast.Expr(ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='output', ctx=ast.Load()),
+ attr='extend',
+ ctx=ast.Load()
+ ), args=[items], keywords=[],
+ starargs=None, kwargs=None
+ ))
+
+ def _if_content_is_not_Falsy(self, body, orelse):
+ return ast.If(
+ # if content is not None and content is not False
+ test=ast.BoolOp(
+ op=ast.And(),
+ values=[
+ ast.Compare(
+ left=ast.Name(id='content', ctx=ast.Load()),
+ ops=[ast.IsNot()],
+ comparators=[ast.Name(id='None', ctx=ast.Load())]
+ ),
+ ast.Compare(
+ left=ast.Name(id='content', ctx=ast.Load()),
+ ops=[ast.IsNot()],
+ comparators=[ast.Name(id='False', ctx=ast.Load())]
+ )
+ ]
+ ),
+ # output.append(escape($content))
+ body=body,
+ # output.append(body default value)
+ orelse=orelse,
+ )
+
+ def _call_body(self, ast_calls, body, args=('self', 'qwebcontext',), prefix='fn', lineno=None):
+ """
+ If ``body`` is non-empty, generates (and globally store) the
+ corresponding function definition and returns the relevant ast.Call
+ node.
+ If ``body`` is empty, doesn't do anything and returns ``None``.
+ """
+ if not body:
+ return None
+ name = self._make_name(prefix)
+ ast_calls.append(utils.base_fn_def(body, name=name, lineno=lineno))
+ return ast.Call(
+ func=ast.Name(id=name, ctx=ast.Load()),
+ args=[ast.Name(id=arg, ctx=ast.Load()) for arg in args],
+ keywords=[], starargs=None, kwargs=None
+ )
+
+ def _make_name(self, prefix='var'):
+ return "%s_%s" % (prefix, next(self._name_gen))
+
+ def _compile_document(self, element):
+ """
+ Compiles a document rooted in ``element`` to a Python AST.
+ Calls the following hooks:
+ * :samp:`_compile_directive_{name}` compiles the directive of the
+ specified name to AST
+ In either case, the current etree._Element is received as first
+ parameter and the list of ast_calls as second. A brand new list
+ of AST nodes *must* be returned (can be an empty list).
+ :return a list of AST nodes, the last AST node is the template call
+ """
+ ast_calls = []
+ self._call_body(ast_calls,
+ self._compile_directive_content(element, ast_calls),
+ prefix='template_%s' % element.get('t-name', 'unknown').replace('.', '_'))
+ return ast_calls
+
+ def _compile_node(self, el, ast_calls):
+ """
+ :return ast list
+ """
+ ignored = self._nondirectives_ignore()
+
+ if el.get("groups"):
+ el.set("t-groups", el.attrib.pop("groups"))
+
+ directives = {
+ att[2:]
+ for att in el.attrib
+ if att.startswith('t-')
+ if not att.startswith('t-att')
+ if att not in ignored
+ }
+
+ for name in self._directives_eval_order():
+ # skip directives not present on the element
+ if name not in directives:
+ continue
+ directives.remove(name)
+ mname = name.replace('-', '_')
+ compile_handler = getattr(self, '_compile_directive_%s' % mname, None)
+ interpret_handler = 'render_tag_%s' % mname
+ if hasattr(self, interpret_handler):
+ _logger.warning(
+ "Directive '%s' must be AOT-compiled. Dynamic interpreter %s will ignored",
+ name, interpret_handler
+ )
+ return compile_handler(el, ast_calls)
+ if directives:
+ raise "Unknown directive '%s' on %s" % ("', '".join(directives), etree.tostring(el))
+
+ return []
+
+ def _directives_eval_order(self):
+ """ Should list all supported directives in the order in which they
+ should evaluate when set on the same element. E.g. if a node bearing
+ both ``foreach`` and ``if`` should see ``foreach`` executed before
+ ``if`` aka
+ .. code-block:: xml
+ <el t-foreach="foo" t-as="bar" t-if="bar">
+ should be equivalent to
+ .. code-block:: xml
+ <t t-foreach="foo" t-as="bar">
+ <t t-if="bar">
+ <el>
+ then this method should return ``['foreach', 'if']``
+ """
+ return [
+ 'debug',
+ 'groups', 'foreach', 'if',
+ 'call-assets',
+ 'field',
+ 'tag',
+ 'call',
+ 'set',
+ 'esc', 'raw', 'content',
+ ]
+
+ def _nondirectives_ignore(self):
+ """
+ t-* attributes existing as support for actual directives, should just
+ be ignored
+ :returns: set
+ """
+ return {
+ 't-name', 't-field-options', 't-as', 't-lang',
+ 't-value', 't-valuef', 't-ignore', 't-js', 't-css', 't-async', 't-placeholder',
+ }
+
+ def _serialize_static_attributes(self, el):
+ nodes = []
+ for key, value in el.attrib.iteritems():
+ if not key.startswith('t-'):
+ nodes.append((key, ast.Str(value)))
+ return nodes
+
+ def _compile_dynamic_attributes(self, el):
+ nodes = []
+ for name, value in el.attrib.iteritems():
+ if name.startswith('t-attf-'):
+ nodes.append((name[7:], utils.compile_format(value)))
+ elif name.startswith('t-att-'):
+ nodes.append((name[6:], utils.compile_expr(value)))
+ elif name == 't-att':
+ nodes.append(ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='self', ctx=ast.Load()),
+ attr='_compile_dynamic_att',
+ ctx=ast.Load()
+ ),
+ args=[
+ ast.Str(el.tag),
+ utils.compile_expr(value),
+ ast.Name(id='qwebcontext', ctx=ast.Load()),
+ ], keywords=[],
+ starargs=None, kwargs=None
+ ))
+ return nodes
+
+ def _compile_dynamic_att(self, tagName, atts, qwebcontext):
+ if isinstance(atts, OrderedDict):
+ return atts
+ if isinstance(atts, (list, tuple)) and not isinstance(atts[0], (list, tuple)):
+ atts = [atts]
+ if isinstance(atts, (list, tuple)):
+ atts = OrderedDict(atts)
+ return atts
+
+ def _compile_all_attributes(self, el, attr_already_created=False):
+ body = []
+ if any(name.startswith('t-att') or not name.startswith('t-') for name, value in el.attrib.iteritems()):
+ if not attr_already_created:
+ attr_already_created = True
+ body.append(
+ ast.Assign(
+ targets=[ast.Name(id='t_attrs', ctx=ast.Store())],
+ value=ast.Call(
+ func=ast.Name(id='OrderedDict', ctx=ast.Load()),
+ args=[],
+ keywords=[], starargs=None, kwargs=None
+ )
+ )
+ )
+
+ items = self._serialize_static_attributes(el) + self._compile_dynamic_attributes(el)
+ for item in items:
+ if isinstance(item, tuple):
+ body.append(ast.Assign(
+ targets=[ast.Subscript(
+ value=ast.Name(id='t_attrs', ctx=ast.Load()),
+ slice=ast.Index(ast.Str(item[0])),
+ ctx=ast.Store()
+ )],
+ value=item[1]
+ ))
+ elif item:
+ body.append(ast.Expr(ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='t_attrs', ctx=ast.Load()),
+ attr='update',
+ ctx=ast.Load()
+ ),
+ args=[item],
+ keywords=[],
+ starargs=None, kwargs=None
+ )))
+
+ if attr_already_created:
+ # for name, value in t_attrs.iteritems():
+ # if value or isinstance(value, basestring)):
+ # output.append(u' ')
+ # output.append(name)
+ # output.append(u'="')
+ # output.append(escape(unicodifier(value)))
+ # output.append(u'"')
+ body.append(ast.For(
+ target=ast.Tuple(elts=[ast.Name(id='name', ctx=ast.Store()), ast.Name(id='value', ctx=ast.Store())], ctx=ast.Store()),
+ iter=ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='t_attrs', ctx=ast.Load()),
+ attr='iteritems',
+ ctx=ast.Load()
+ ),
+ args=[], keywords=[],
+ starargs=None, kwargs=None
+ ),
+ body=[ast.If(
+ test=ast.BoolOp(
+ op=ast.Or(),
+ values=[
+ ast.Name(id='value', ctx=ast.Load()),
+ ast.Call(
+ func=ast.Name(id='isinstance', ctx=ast.Load()),
+ args=[
+ ast.Name(id='value', ctx=ast.Load()),
+ ast.Name(id='basestring', ctx=ast.Load())
+ ],
+ keywords=[],
+ starargs=None, kwargs=None
+ )
+ ]
+ ),
+ body=[
+ self._append(ast.Str(u' ')),
+ self._append(ast.Name(id='name', ctx=ast.Load())),
+ self._append(ast.Str(u'="')),
+ self._append(ast.Call(
+ func=ast.Name(id='escape', ctx=ast.Load()),
+ args=[
+ ast.Call(
+ func=ast.Name(id='unicodifier', ctx=ast.Load()),
+ args=[ast.Name(id='value', ctx=ast.Load())], keywords=[],
+ starargs=None, kwargs=None
+ )
+ ], keywords=[],
+ starargs=None, kwargs=None
+ )),
+ self._append(ast.Str(u'"')),
+ ],
+ orelse=[]
+ )],
+ orelse=[]
+ ))
+
+ return body
+
+ def _compile_tag(self, el, content, attr_already_created=False):
+ body = [self._append(ast.Str(u'<%s' % el.tag))]
+ body.extend(self._compile_all_attributes(el, attr_already_created))
+ if el.tag in self._void_elements:
+ body.append(self._append(ast.Str(u'/>')))
+ body.extend(content)
+ else:
+ body.append(self._append(ast.Str(u'>')))
+ body.extend(content)
+ body.append(self._append(ast.Str(u'</%s>' % el.tag)))
+ return body
+
+ def _compile_directive_debug(self, el, ast_calls):
+ debugger = el.attrib.pop('t-debug')
+ body = self._compile_node(el, ast_calls)
+ if 'qweb' in openerp.tools.config['dev_mode']:
+ body = ast.parse("__import__('%s').set_trace()" % re.sub('[^a-zA-Z]', '', debugger)).body + body # pdb, ipdb, pudb, ...
+ else:
+ _logger.warning("@t-debug in template is only available in --dev mode")
+ return body
+
+ def _compile_directive_tag(self, el, ast_calls):
+ el.attrib.pop('t-tag', None)
+ content = self._compile_node(el, ast_calls)
+ if el.tag == 't':
+ return content
+ return self._compile_tag(el, content, False)
+
+ def _compile_directive_set(self, el, ast_calls):
+ if 't-value' in el.attrib:
+ value = utils.compile_expr(el.attrib.pop('t-value'))
+ elif 't-valuef' in el.attrib:
+ value = utils.compile_format(el.attrib.pop('t-valuef'))
+ else:
+ render = self._call_body(ast_calls, self._compile_directive_content(el, ast_calls), prefix='set', lineno=el.sourceline)
+ if render is None:
+ value = ast.Str(u'')
+ else:
+ # concat body render to string
+ value = ast.Call(
+ func=ast.Attribute(value=ast.Str(u''), attr='join', ctx=ast.Load()),
+ args=[render], keywords=[],
+ starargs=None, kwargs=None
+ )
+
+ return [
+ ast.Assign(
+ targets=[ast.Subscript(
+ value=ast.Name(id='qwebcontext', ctx=ast.Load()),
+ slice=ast.Index(ast.Str(el.attrib.pop('t-set'))),
+ ctx=ast.Store()
+ )],
+ value=value
+ )]
+
+ def _compile_directive_esc(self, el, ast_calls):
+ return [
+ # content = t-raw value
+ ast.Assign(
+ targets=[ast.Name(id='content', ctx=ast.Store())],
+ value=utils.compile_expr0(el.attrib.pop('t-esc')),
+ ),
+ # if content is not None: display
+ self._if_content_is_not_Falsy(
+ # output.append(escape($content))
+ body=[self._append(ast.Call(
+ func=ast.Name(id='escape', ctx=ast.Load()),
+ args=[ast.Call(
+ func=ast.Name(id='unicodifier', ctx=ast.Load()),
+ args=[ast.Name(id='content', ctx=ast.Load())], keywords=[],
+ starargs=None, kwargs=None
+ )],
+ keywords=[], starargs=None, kwargs=None
+ ))],
+ # output.append(body default value)
+ orelse=self._compile_directive_content(el, ast_calls),
+ )
+ ]
+
+ def _compile_directive_raw(self, el, ast_calls):
+ return [
+ # content = t-raw value
+ ast.Assign(
+ targets=[ast.Name(id='content', ctx=ast.Store())],
+ value=utils.compile_expr0(el.attrib.pop('t-raw')),
+ ),
+ self._if_content_is_not_Falsy(
+ # output.append($content)
+ body=[self._append(ast.Call(
+ func=ast.Name(id='unicodifier', ctx=ast.Load()),
+ args=[ast.Name(id='content', ctx=ast.Load())], keywords=[],
+ starargs=None, kwargs=None
+ ))],
+ # output.append(body default value)
+ orelse=self._compile_directive_content(el, ast_calls),
+ )
+ ]
+
+ def _compile_directive_content(self, el, ast_calls):
+ body = []
+ if el.text is not None:
+ body.append(self._append(ast.Str(unicodifier(el.text))))
+ if el.getchildren():
+ for item in el:
+ # ignore comments & processing instructions
+ if isinstance(item, etree._Comment):
+ continue
+ item.set('t-tag', item.tag)
+ if not (set(['t-esc', 't-raw', 't-field']) & set(item.attrib)):
+ item.set('t-content', 'True')
+ body.extend(self._compile_node(item, ast_calls))
+ body.extend(self._compile_tail(item))
+ return body
+
+ def _compile_directive_call(self, el, ast_calls):
+ """
+ :param etree._Element el:
+ :param list ast call:
+ :return: new body
+ :rtype: list(ast.AST)
+ """
+ tmpl = el.attrib.pop('t-call')
+ qw = self._make_name('qwebcontext_copy')
+ sub_content = self._call_body(ast_calls, self._compile_directive_content(el, ast_calls),
+ prefix='body_call_content', args=('self', qw,), lineno=el.sourceline)
+ lang = el.get('t-lang', "'en_US'")
+
+ content = [
+ # qwebcontext_copy = qwebcontext.copy()
+ ast.Assign(
+ targets=[ast.Name(id=qw, ctx=ast.Store())],
+ value=ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='qwebcontext', ctx=ast.Load()),
+ attr='copy',
+ ctx=ast.Load()
+ ),
+ args=[], keywords=[],
+ starargs=None, kwargs=None
+ )
+ )
+ ]
+
+ qw_0_ast = ast.Subscript(
+ value=ast.Name(id=qw, ctx=ast.Load()),
+ slice=ast.Index(ast.Num(0)),
+ ctx=ast.Store()
+ )
+
+ # qwebcontext_copy[0] = body_call_content or u''
+ content.append(
+ ast.Assign(
+ targets=[qw_0_ast],
+ value=sub_content or ast.Str(u'')
+ ))
+
+ # qwebcontext_copy.env = qwebcontext.env(context=dict(qwebcontext.env.context, lang=$lang))
+ if lang:
+ content.append(
+ ast.Assign(
+ targets=[ast.Attribute(
+ value=ast.Name(id=qw, ctx=ast.Load()),
+ attr='env',
+ ctx=ast.Store()
+ )],
+ value=ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='qwebcontext', ctx=ast.Load()),
+ attr='env',
+ ctx=ast.Load()
+ ),
+ args=[],
+ keywords=[ast.keyword('context', ast.Call(
+ func=ast.Name(id='dict', ctx=ast.Load()),
+ args=[ast.Attribute(
+ value=ast.Attribute(
+ value=ast.Name(id='qwebcontext', ctx=ast.Load()),
+ attr='env',
+ ctx=ast.Load()
+ ),
+ attr='context',
+ ctx=ast.Load()
+ )],
+ keywords=[ast.keyword('lang', utils.compile_strexpr(lang))],
+ starargs=None, kwargs=None
+ ))],
+ starargs=None, kwargs=None
+ )
+ ))
+
+ # qwebcontext.env['ir.qweb']
+ compute_self = ast.Subscript(
+ value=ast.Attribute(
+ value=ast.Name(id=qw, ctx=ast.Load()),
+ attr='env',
+ ctx=ast.Load()
+ ),
+ slice=ast.Index(ast.Str('ir.qweb')),
+ ctx=ast.Load()
+ )
+ else:
+ compute_self = ast.Name(id='self', ctx=ast.Load())
+
+ # ouput.append(self._get_template($tmpl, qwebcontext)(self, qwebcontext_copy))
+ content.append(
+ self._append(
+ ast.Call(
+ func=ast.Call(
+ func=ast.Attribute(
+ value=compute_self,
+ attr='_get_template',
+ ctx=ast.Load()
+ ),
+ args=[
+ ast.Str(str(tmpl)),
+ ast.Name(id=qw, ctx=ast.Load()),
+ ],
+ keywords=[], starargs=None, kwargs=None
+ ),
+ args=[ast.Name(id=qw, ctx=ast.Load())],
+ keywords=[], starargs=None, kwargs=None
+ )
+ ))
+ return content
+
+ def _compile_directive_if(self, el, ast_calls):
+ orelse = []
+ if el.tail and re.search(r'\S', el.tail):
+ next = el.getnext()
+ if next is not None and 't-else' in next.attrib:
+ el.tail = None
+ next.attrib.pop('t-else')
+ orelse = self._compile_node(next, ast_calls)
+ next.getparent().remove(next)
+ return [
+ ast.If(
+ test=utils.compile_expr(el.attrib.pop('t-if')),
+ body=self._compile_node(el, ast_calls),
+ orelse=orelse
+ )
+ ]
+
+ def _compile_directive_groups(self, el, ast_calls):
+ return [
+ ast.If(
+ test=ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='self', ctx=ast.Load()),
+ attr='user_has_groups',
+ ctx=ast.Load()
+ ),
+ args=[ast.Str(el.attrib.pop('t-groups'))], keywords=[],
+ starargs=None, kwargs=None
+ ),
+ body=self._compile_node(el, ast_calls),
+ orelse=[]
+ )
+ ]
+
+ def _compile_directive_foreach(self, el, ast_calls):
+ expr = utils.compile_expr(el.attrib.pop('t-foreach'))
+ varname = el.attrib.pop('t-as').replace('.', '_')
+
+ # foreach_iterator(qwebcontext, <expr>, varname)
+ it = ast.Call(
+ func=ast.Name(id='foreach_iterator', ctx=ast.Load()),
+ args=[ast.Name(id='qwebcontext', ctx=ast.Load()), expr, ast.Str(varname)],
+ keywords=[], starargs=None, kwargs=None
+ )
+ # itertools.repeat(self)
+ selfs = ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='itertools', ctx=ast.Load()),
+ attr='repeat',
+ ctx=ast.Load()
+ ), args=[ast.Name(id='self', ctx=ast.Load())], keywords=[],
+ starargs=None, kwargs=None
+ )
+
+ # create function $foreach
+ call = self._call_body(ast_calls, self._compile_node(el, ast_calls), prefix='foreach', lineno=el.sourceline)
+
+ # itertools.imap($foreach(), repeat(self), it)
+ it = ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='itertools', ctx=ast.Load()),
+ attr='imap',
+ ctx=ast.Load()
+ ), args=[call.func, selfs, it], keywords=[],
+ starargs=None, kwargs=None
+ )
+ # itertools.chain.from_iterable(previous)
+ it = ast.Call(
+ func=ast.Attribute(
+ value=ast.Attribute(
+ value=ast.Name(id='itertools', ctx=ast.Load()),
+ attr='chain',
+ ctx=ast.Load()
+ ),
+ attr='from_iterable',
+ ctx=ast.Load()
+ ), args=[it], keywords=[],
+ starargs=None, kwargs=None
+ )
+ return [self._extend(it)]
+
+ def _compile_tail(self, el):
+ return el.tail is not None and [self._append(ast.Str(unicodifier(el.tail)))] or []
+
+ def _compile_directive_call_assets(self, el, ast_calls):
+ """ This special 't-call' tag can be used in order to aggregate/minify javascript and css assets"""
+ if len(el):
+ raise "t-call-assets cannot contain children nodes"
+
+ # self._get_asset(xmlid, qwebcontext).to_html(css, js, debug, asunc qwebcontext)
+ return [
+ self._append(ast.Call(
+ func=ast.Attribute(
+ value=ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='self', ctx=ast.Load()),
+ attr='_get_asset',
+ ctx=ast.Load()
+ ),
+ args=[
+ ast.Str(el.get('t-call-assets')),
+ ast.Name(id='qwebcontext', ctx=ast.Load()),
+ ],
+ keywords=[], starargs=None, kwargs=None
+ ),
+ attr='to_html',
+ ctx=ast.Load()
+ ),
+ args=[],
+ keywords=[
+ ast.keyword('css', utils.get_attr_bool(el.get('t-css', True))),
+ ast.keyword('js', utils.get_attr_bool(el.get('t-js', True))),
+ ast.keyword('debug', ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='qwebcontext', ctx=ast.Load()),
+ attr='get',
+ ctx=ast.Load()
+ ),
+ args=[ast.Str('debug')],
+ keywords=[], starargs=None, kwargs=None
+ )),
+ ast.keyword('async', utils.get_attr_bool(el.get('async', False))),
+ ast.keyword('qwebcontext', ast.Name(id='qwebcontext', ctx=ast.Load())),
+ ],
+ starargs=None, kwargs=None
+ ))
+ ]
+
+ def _compile_directive_field(self, el, ast_calls):
+ """ eg: <span t-record="browse_record(res.partner, 1)" t-field="phone">+1 555 555 8069</span>"""
+ node_name = el.tag
+ assert node_name not in ("table", "tbody", "thead", "tfoot", "tr", "td",
+ "li", "ul", "ol", "dl", "dt", "dd"),\
+ "RTE widgets do not work correctly on %r elements" % node_name
+ assert node_name != 't',\
+ "t-field can not be used on a t element, provide an actual HTML node"
+ assert "." in el.get('t-field'),\
+ "t-field must have at least a dot like 'record.field_name'"
+
+ expression = el.attrib.pop('t-field')
+ options = el.attrib.pop('t-field-options', None)
+ record, field_name = expression.rsplit('.', 1)
+
+ return [
+ # record
+ ast.Assign(
+ targets=[ast.Name(id='record', ctx=ast.Store())],
+ value=utils.compile_expr(record)
+ ),
+ # default_content = u''.join($body_content())
+ ast.Assign(
+ targets=[ast.Name(id='default_content', ctx=ast.Store())],
+ value=self._call_body(ast_calls, self._compile_node(el, ast_calls), prefix='body_content', lineno=el.sourceline)
+ ),
+ # t_attrs, content = self._get_field(record, field_name, expression, options, qwebcontext)
+ ast.Assign(
+ targets=[ast.Tuple(elts=[ast.Name(id='t_attrs', ctx=ast.Store()), ast.Name(id='content', ctx=ast.Store())], ctx=ast.Store())],
+ value=ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='self', ctx=ast.Load()),
+ attr='_get_field',
+ ctx=ast.Load()
+ ),
+ args=[
+ ast.Name(id='record', ctx=ast.Load()),
+ ast.Str(field_name),
+ ast.Str(expression),
+ ast.Name(id='default_content', ctx=ast.Load()),
+ ast.Str(node_name),
+ options and utils.compile_format(options) or ast.Name(id='None', ctx=ast.Load()),
+ ast.Name(id='qwebcontext', ctx=ast.Load()),
+ ],
+ keywords=[], starargs=None, kwargs=None
+ ),
+ ),
+
+ # if content is not None: display the tag
+ self._if_content_is_not_Falsy(
+ body=self._compile_tag(el, [self._append(ast.Name(id='content', ctx=ast.Load()))], True),
+ orelse=[]
+ )
+ ]
+
+ # assume cache will be invalidated by third party on write to ir.ui.view
+ def _get_template_cache_keys(self):
+ """ Return the list of context keys to use for caching ``_get_template``. """
+ return ['lang', 'inherit_branding', 'editable', 'translatable', 'edit_translations']
+
+ # apply ormcache_context decorator unless in dev mode...
+ @openerp.tools.conditional(
+ 'xml' not in openerp.tools.config['dev_mode'],
+ openerp.tools.ormcache('xmlid', 'tuple(map(self._context.get, self._get_template_cache_keys()))'),
+ )
+ def _get_template(self, xmlid, qwebcontext):
+ element = self.get_template(xmlid, qwebcontext.loader, origin_template=qwebcontext.get('__caller__') or qwebcontext.get('__stack__', [xmlid])[0])
+ element.attrib.pop("name", False)
+ return self.compile(element)
+
+ def _get_asset(self, xmlid, qwebcontext):
+ files, remains = self._get_asset_content(xmlid, qwebcontext)
+ return AssetsBundle(xmlid, files, remains, env=qwebcontext.env)
+
+ @openerp.tools.ormcache('xmlid', 'qwebcontext.env.lang')
+ def _get_asset_content(self, xmlid, qwebcontext):
+ context = dict(self.env.context if qwebcontext is None else qwebcontext.env.context,
+ inherit_branding=False, inherit_branding_auto=False,
+ edit_translations=False, translatable=False,
+ rendering_bundle=True)
+
+ env = self.env(context=context)
+ if qwebcontext is None:
+ qwebcontext = QWebContext(env, {})
+ else:
+ qwebcontext = qwebcontext.copy()
+ qwebcontext.env = env
+
+ template = env['ir.qweb']._get_template(xmlid, qwebcontext)(qwebcontext)
+
+ files = []
+ remains = []
+ for el in html.fragments_fromstring(template):
+ if isinstance(el, basestring):
+ remains.append(el)
+ elif isinstance(el, html.HtmlElement):
+ href = el.get('href', '')
+ atype = el.get('type')
+ media = el.get('media')
+
+ can_aggregate = not urlparse(href).netloc and not href.startswith('/web/content')
+ if el.tag == 'style' or (el.tag == 'link' and el.get('rel') == 'stylesheet' and can_aggregate):
+ if href.endswith('.sass'):
+ atype = 'text/sass'
+ elif href.endswith('.less'):
+ atype = 'text/less'
+ if atype not in ('text/less', 'text/sass'):
+ atype = 'text/css'
+ files.append({'atype': atype, 'url': href, 'content': el.text, 'media': media})
+ elif el.tag == 'script':
+ atype = 'text/javascript'
+ files.append({'atype': atype, 'url': el.get('src', ''), 'content': el.text, 'media': media})
+ else:
+ remains.append(html.tostring(el))
+ else:
+ try:
+ remains.append(html.tostring(el))
+ except Exception:
+ # notYETimplementederror
+ raise NotImplementedError
+
+ return (files, remains)
+
+ def _get_field(self, record, field_name, expression, default_content, tagName, options, qwebcontext):
+ options = options and json.loads(options) or {}
+ field = record._fields[field_name]
+
+ field_type = options.get('widget', field.type)
+
+ inherit_branding = self._context.get('inherit_branding', self._context.get('inherit_branding_auto') and record.check_access_rights('write', False))
+ translate = self._context.get('edit_translations') and self._context.get('translatable') and getattr(field, 'translate', False)
+
+ options['tagName'] = tagName
+ options['type'] = field_type
+ options['expression'] = expression
+ options['default_content'] = default_content
+ options['inherit_branding'] = inherit_branding
+ options['translate'] = translate
+
+ converter = self.get_field_for(field_type, qwebcontext.cr, qwebcontext.uid, qwebcontext.context)
+
+ content = converter.record_to_html(record, field_name, options, qwebcontext)
+ attributes = converter.attributes(record, field_name, options, qwebcontext)
+
+ return (attributes, content or default_content or (u"" if inherit_branding or translate else None))
diff --git a/openerp/addons/base/ir/ir_qweb/utils.py b/openerp/addons/base/ir/ir_qweb/utils.py
new file mode 100644
index 00000000000..e4269e4fe1b
--- /dev/null
+++ b/openerp/addons/base/ir/ir_qweb/utils.py
@@ -0,0 +1,381 @@
+# -*- coding: utf-8 -*-
+
+import ast
+import itertools
+from collections import Sized, Mapping, defaultdict
+from openerp.tools import safe_eval
+from odoo.exceptions import QWebException
+from copy import deepcopy
+from lxml import etree
+import sys
+import re
+
+import __builtin__
+
+
+def base_module():
+ """ module base supporting qweb template functions (provides basic
+ imports and utilities)
+ Currently provides:
+ * collections
+ * itertools
+ * openerp.tools.html_escape as escape
+ * unicodifier (empty string for a None or False, otherwise unicode string)
+ """
+
+ return ast.parse("""from collections import OrderedDict
+import itertools
+from openerp.addons.base.ir.ir_qweb.utils import unicodifier, foreach_iterator
+from openerp.tools import safe_eval, html_escape as escape
+""")
+
+
+def unicodifier(val):
+ if val is None or val is False:
+ return u''
+ if isinstance(val, str):
+ return val.decode('utf-8')
+ return unicode(val)
+
+def foreach_iterator(base_ctx, enum, name):
+ ctx = base_ctx.copy()
+ if not enum:
+ return
+ if isinstance(enum, int):
+ enum = xrange(enum)
+ size = None
+ if isinstance(enum, Sized):
+ ctx["%s_size" % name] = size = len(enum)
+ if isinstance(enum, Mapping):
+ enum = enum.iteritems()
+ else:
+ enum = itertools.izip(*itertools.tee(enum))
+ value_key = '%s_value' % name
+ index_key = '%s_index' % name
+ first_key = '%s_first' % name
+ last_key = '%s_last' % name
+ parity_key = '%s_parity' % name
+ even_key = '%s_even' % name
+ odd_key = '%s_odd' % name
+ for index, (item, value) in enumerate(enum):
+ ctx[name] = item
+ ctx[value_key] = value
+ ctx[index_key] = index
+ ctx[first_key] = index == 0
+ if size is not None:
+ ctx[last_key] = index + 1 == size
+ if index % 2:
+ ctx[parity_key] = 'odd'
+ ctx[even_key] = False
+ ctx[odd_key] = True
+ else:
+ ctx[parity_key] = 'even'
+ ctx[even_key] = True
+ ctx[odd_key] = False
+ yield ctx
+ # copy changed items back into source context (?)
+ # FIXME: maybe QWebContext could provide a ChainMap-style clone?
+ for k in base_ctx.keys():
+ base_ctx[k] = ctx[k]
+
+def base_fn_def(body, name='fn', lineno=None):
+ """ Generates a "qweb function" definition:
+ * takes a single ``qwebcontext`` parameter
+ * defines a local ``output`` list
+ * returns ``u''.join(output)``
+ The provided body should be a list of ast nodes, they will be injected
+ between the initialization of ``body`` and its concatenation
+ """
+
+ body = [ast.Assign([ast.Name(id='output', ctx=ast.Store())], ast.List(elts=[], ctx=ast.Load()))] + \
+ body + \
+ [ast.Return(ast.Call(
+ func=ast.Attribute(value=ast.Str(u''), attr='join', ctx=ast.Load()),
+ args=[ast.Name(id='output', ctx=ast.Load())], keywords=[],
+ starargs=None, kwargs=None
+ ))]
+
+ fn = ast.FunctionDef(
+ name=name,
+ args=ast.arguments(args=[
+ ast.Name('self', ast.Param()),
+ ast.Name('qwebcontext', ast.Param())
+ ], defaults=[], vararg=None, kwarg=None),
+ body=body,
+ decorator_list=[])
+ if lineno is not None:
+ fn.lineno = lineno
+ return fn
+
+def get_attr_bool(attr, default=False):
+ if attr:
+ if attr is True:
+ return ast.Name(id='True', ctx=ast.Load())
+ attr = attr.lower()
+ if attr in ('false', '0'):
+ return ast.Name(id='False', ctx=ast.Load())
+ elif attr in ('true', '1'):
+ return ast.Name(id='True', ctx=ast.Load())
+ return ast.Name(id=str(attr if attr is False else default), ctx=ast.Load())
+
+
+class Contextifier(ast.NodeTransformer):
+ """ For user-provided template expressions, replaces any ``name`` by
+ :sampe:`qwebcontext.eval_dict['{name}']` so all variable accesses are
+ performed on the qwebcontext rather than in the "native" scope
+ """
+
+ # some people apparently put lambdas in template expressions. Turns out
+ # the AST -> bytecode compiler does *not* appreciate parameters of lambdas
+ # being converted from names to subscript expressions, and most likely the
+ # reference to those parameters inside the lambda's body should probably
+ # remain as-is. Because we're transforming an AST, the structure should
+ # be lexical, so just store a set of "safe" parameter names and recurse
+ # through the lambda using a new NodeTransformer
+ def __init__(self, params=()):
+ super(Contextifier, self).__init__()
+ self._safe_names = tuple(params)
+
+ def visit_Name(self, node):
+ if node.id in self._safe_names:
+ return node
+
+ return ast.copy_location(
+ ast.Subscript(
+ value=ast.Attribute(
+ value=ast.Name(id='qwebcontext', ctx=ast.Load()),
+ attr='eval_dict',
+ ctx=ast.Load()
+ ),
+ slice=ast.Index(ast.Str(node.id)),
+ ctx=ast.Load()),
+ node
+ )
+
+ def visit_Lambda(self, node):
+ args = node.args
+ # assume we don't have any tuple parameter, just names
+ names = [arg.id for arg in args.args]
+ if args.vararg: names.append(args.vararg)
+ if args.kwarg: names.append(args.kwarg)
+ # remap defaults in case there's any
+ return ast.copy_location(ast.Lambda(
+ args=ast.arguments(
+ args=args.args,
+ defaults=map(self.visit, args.defaults),
+ vararg=args.vararg,
+ kwarg=args.kwarg,
+ ),
+ body=Contextifier(self._safe_names + tuple(names)).visit(node.body)
+ ), node)
+
+ # "lambda problem" also exists with comprehensions
+ def _visit_comp(self, node):
+ # CompExp(?, comprehension* generators)
+ # comprehension = (expr target, expr iter, expr* ifs)
+
+ # collect names in generators.target
+ names = tuple(
+ node.id
+ for gen in node.generators
+ for node in ast.walk(gen.target)
+ if isinstance(node, ast.Name)
+ )
+ transformer = Contextifier(self._safe_names + names)
+ # copy node
+ newnode = ast.copy_location(type(node)(), node)
+ # then visit the comp ignoring those names, transformation is
+ # probably expensive but shouldn't be many comprehensions
+ for field, value in ast.iter_fields(node):
+ # map transformation of comprehensions
+ if isinstance(value, list):
+ setattr(newnode, field, map(transformer.visit, value))
+ else: # set transformation of key/value/expr fields
+ setattr(newnode, field, transformer.visit(value))
+ return newnode
+ visit_GeneratorExp = visit_ListComp = visit_SetComp = visit_DictComp = _visit_comp
+
+
+def compile_strexpr(expr):
+ # ensure result is unicode
+ return ast.Call(
+ func=ast.Name(id='unicodifier', ctx=ast.Load()),
+ args=[compile_expr(expr)], keywords=[],
+ starargs=None, kwargs=None
+ )
+
+def compile_expr0(expr):
+ if expr == "0":
+ return ast.Call(
+ func=ast.Attribute(
+ value=ast.Name(id='qwebcontext', ctx=ast.Load()),
+ attr='get',
+ ctx=ast.Load()
+ ),
+ args=[ast.Num(0), ast.Str('')], keywords=[],
+ starargs=None, kwargs=None
+ )
+ return compile_expr(expr)
+
+def compile_expr(expr):
+ """ Compiles a purported Python expression to ast, verifies that it's safe
+ (according to safe_eval's semantics) and alter its variable references to
+ access qwebcontext data instead
+ """
+ # string must be stripped otherwise whitespace before the start for
+ # formatting purpose are going to break parse/compile
+ st = ast.parse(expr.strip(), mode='eval')
+ safe_eval.assert_valid_codeobj(
+ safe_eval._SAFE_OPCODES,
+ compile(st, '<>', 'eval'), # could be expr, but eval *should* be fine
+ expr
+ )
+
+ # ast.Expression().body -> expr
+ return Contextifier().visit(st).body
+
+def compile_format(f):
+ """ Parses the provided format string and compiles it to a single
+ expression ast, uses string concatenation via "+"
+ """
+ elts = []
+ base_idx = 0
+ for m in _FORMAT_REGEX.finditer(f):
+ literal = f[base_idx:m.start()]
+ if literal:
+ elts.append(ast.Str(literal if isinstance(literal, unicode) else literal.decode('utf-8')))
+
+ expr = m.group(1) or m.group(2)
+ elts.append(compile_strexpr(expr))
+ base_idx = m.end()
+ # string past last regex match
+ literal = f[base_idx:]
+ if literal:
+ elts.append(ast.Str(literal if isinstance(literal, unicode) else literal.decode('utf-8')))
+
+ return reduce(lambda acc, it: ast.BinOp(
+ left=acc,
+ op=ast.Add(),
+ right=it
+ ), elts)
+
+
+_FORMAT_REGEX = re.compile(
+ '(?:'
+ # ruby-style pattern
+ '#\{(.+?)\}'
+ ')|(?:'
+ # jinja-style pattern
+ '\{\{(.+?)\}\}'
+ ')')
+
+
+class QWebTemplateNotFound(QWebException):
+ pass
+
+
+def eval_expr(expr, qwebcontext):
+ try:
+ if expr == "0":
+ return qwebcontext.get(0, '')
+ return qwebcontext.safe_eval(expr)
+ except Exception:
+ template = qwebcontext.get('__template__')
+ raise_qweb_exception(message="Could not evaluate expression %r" % expr, expression=expr, template=template)
+
+def eval_format_expr(expr, qwebcontext):
+ expr, replacements = _FORMAT_REGEX.subn(
+ lambda m: unicodifier(eval_expr(m.group(1) or m.group(2), qwebcontext)),
+ expr
+ )
+ if replacements:
+ return expr
+ try:
+ return str(expr % qwebcontext)
+ except Exception:
+ template = qwebcontext.get('__template__')
+ raise_qweb_exception(message="Format error for expression %r" % expr, expression=expr, template=template)
+
+
+class FileSystemLoader(object):
+ def __init__(self, path):
+ # TODO: support multiple files #add_file() + add cache
+ self.path = path
+ self.doc = etree.parse(path).getroot()
+
+ def __iter__(self):
+ for node in self.doc:
+ name = node.get('t-name')
+ if name:
+ yield name
+
+ def __call__(self, name):
+ for node in self.doc:
+ if node.get('t-name') == name:
+ root = etree.Element('templates')
+ root.append(deepcopy(node))
+ arch = etree.tostring(root, encoding='utf-8', xml_declaration=True)
+ return arch
+
+
+class EvalDict(Mapping):
+ """ Mapping proxying on an evaluation context, also builtins
+
+ * prevents access to cr and loader
+ * acts as a defaultdict(lambda: None) so code can try to access unset
+ variables and will just get None
+ """
+ def __init__(self, ctx):
+ self._ctx = ctx
+
+ def __iter__(self):
+ return (k for k in self._ctx if k not in ('cr', 'loader'))
+
+ def __len__(self):
+ l = len(self._ctx)
+ if 'cr' in self._ctx:
+ l -= 1
+ if 'loader' in self._ctx:
+ l -= 1
+ return l
+
+ def __getitem__(self, key):
+ if key in ('cr', 'loader'):
+ return None
+ try:
+ return self._ctx[key]
+ except KeyError:
+ return getattr(__builtin__, key, None)
+
+
+class QWebContext(dict):
+ def __init__(self, env, data, loader=None, templates=None):
+ self.env = env
+ self.loader = loader or (lambda name: env['ir.ui.view'].read_template(name))
+ super(QWebContext, self).__init__(data)
+ self['defined'] = lambda key: key in self
+ self.templates = templates or {}
+ self.eval_dict = EvalDict(self)
+
+ # deprecated, use 'env' instead
+ cr = property(lambda self: self.env.cr)
+ uid = property(lambda self: self.env.uid)
+ context = property(lambda self: self.env.context)
+
+ def safe_eval(self, expr):
+ locals_dict = defaultdict(lambda: None)
+ locals_dict.update(self)
+ locals_dict.pop('cr', None)
+ locals_dict.pop('loader', None)
+ return safe_eval.safe_eval(expr, None, locals_dict, nocopy=True, locals_builtins=True)
+
+ def copy(self):
+ """ Clone the current context, conserving all data and metadata
+ (loader, template cache, ...)
+ """
+ return QWebContext(self.env, dict.copy(self),
+ loader=self.loader,
+ templates=self.templates)
+
+ def __copy__(self):
+ return self.copy()
diff --git a/openerp/addons/base/tests/test_qweb.py b/openerp/addons/base/tests/test_qweb.py
index e7de2c3c3b3..65b1ffc6d54 100644
--- a/openerp/addons/base/tests/test_qweb.py
+++ b/openerp/addons/base/tests/test_qweb.py
@@ -111,7 +111,7 @@ class TestQWeb(TransactionCase):
ctx.update(params)
result = doc.find('result[@id="{}"]'.format(template)).text
self.assertEqual(
- qweb.render(template, qwebcontext=ctx).strip(),
+ qweb.render(template, qwebcontext=ctx).strip().encode('utf-8'),
(result or u'').strip().encode('utf-8'),
template,
)
diff --git a/openerp/addons/test_assetsbundle/tests/test_assetsbundle.py b/openerp/addons/test_assetsbundle/tests/test_assetsbundle.py
index 0067f006607..5eee3943413 100644
--- a/openerp/addons/test_assetsbundle/tests/test_assetsbundle.py
+++ b/openerp/addons/test_assetsbundle/tests/test_assetsbundle.py
@@ -4,7 +4,7 @@
from openerp import api
from openerp.tests import HttpCase
from openerp.tests.common import TransactionCase
-from openerp.addons.base.ir.ir_qweb import AssetsBundle
+from openerp.addons.base.ir.ir_qweb import AssetsBundle, QWebContext
from openerp.modules.module import get_resource_path
from collections import Counter
@@ -18,6 +18,9 @@ class TestJavascriptAssetsBundle(TransactionCase):
self.jsbundle_xmlid = 'test_assetsbundle.bundle1'
self.cssbundle_xmlid = 'test_assetsbundle.bundle2'
+ def _get_asset(self, xmlid, env=None):
+ return self.en['ir.qweb']._get_asset(xmlid, QWebContext(env or self.env, {}))
+
def _any_ira_for_bundle(self, type):
""" Returns all ir.attachments associated to a bundle, regardless of the verion.
"""
@@ -30,7 +33,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that a bundle creates an ir.attachment record when its `js` method is called
for the first time.
"""
- self.bundle = AssetsBundle(self.jsbundle_xmlid, env=self.env)
+ self.bundle = self._get_asset(self.jsbundle_xmlid, env=self.env)
# there shouldn't be any attachment associated to this bundle
self.assertEquals(len(self._any_ira_for_bundle('js')), 0)
@@ -47,7 +50,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that the bundle's cache is working, i.e. that the bundle creates only one
ir.attachment record when rendered multiple times.
"""
- bundle0 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
+ bundle0 = self._get_asset(self.jsbundle_xmlid)
bundle0.js()
self.assertEquals(len(self._any_ira_for_bundle('js')), 1)
@@ -56,7 +59,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
ira0 = self.registry['ir.attachment'].browse(self.cr, self.uid, self._any_ira_for_bundle('js')[0])
date0 = ira0.create_date
- bundle1 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
+ bundle1 = self._get_asset(self.jsbundle_xmlid)
bundle1.js()
self.assertEquals(len(self._any_ira_for_bundle('js')), 1)
@@ -71,7 +74,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_03_date_invalidation(self):
""" Checks that a bundle is invalidated when one of its assets' modification date is changed.
"""
- bundle0 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
+ bundle0 = self._get_asset(self.jsbundle_xmlid)
bundle0.js()
last_modified0 = bundle0.last_modified
version0 = bundle0.version
@@ -79,7 +82,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
path = get_resource_path('test_assetsbundle', 'static', 'src', 'js', 'test_jsfile1.js')
utime(path, None) # touch
- bundle1 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
+ bundle1 = self._get_asset(self.jsbundle_xmlid)
bundle1.js()
last_modified1 = bundle1.last_modified
version1 = bundle1.version
@@ -93,7 +96,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that a bundle is invalidated when its content is modified by adding a file to
source.
"""
- bundle0 = AssetsBundle(self.jsbundle_xmlid, env=self.env)
+ bundle0 = self._get_asset(self.jsbundle_xmlid)
bundle0.js()
html0 = bundle0.html
version0 = bundle0.version
@@ -115,7 +118,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
'inherit_id': bundle_id,
})
- bundle1 = AssetsBundle(self.jsbundle_xmlid, env=self.env(context={'check_view_ids': [newid]}))
+ bundle1 = self._get_asset(self.jsbundle_xmlid, env=self.env(context={'check_view_ids': [newid]}))
bundle1.js()
html1 = bundle1.html
version1 = bundle1.version
@@ -129,7 +132,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_05_debug(self):
""" Checks that a bundle rendered in debug mode outputs non-minified assets.
"""
- debug_bundle = AssetsBundle(self.jsbundle_xmlid, env=self.env)
+ debug_bundle = self._get_asset(self.jsbundle_xmlid)
content = debug_bundle.to_html(debug='assets')
# find back one of the original asset file
self.assertIn('/test_assetsbundle/static/src/js/test_jsfile1.js', content)
@@ -143,21 +146,21 @@ class TestJavascriptAssetsBundle(TransactionCase):
"""
# note: changing the max_css_rules of a bundle does not invalidate its attachments
# self.cssbundle_xlmid contains 3 rules
- self.bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
+ self.bundle = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'max_css_rules': 1}))
self.bundle.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 3)
self.assertEquals(len(self.bundle.get_attachments('css')), 3)
def test_07_paginated_css_generation2(self):
# self.cssbundle_xlmid contains 3 rules
- self.bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=2)
+ self.bundle = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'max_css_rules': 2}))
self.bundle.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 2)
self.assertEquals(len(self.bundle.get_attachments('css')), 2)
def test_08_paginated_css_generation3(self):
# self.cssbundle_xlmid contains 3 rules
- self.bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=3)
+ self.bundle = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'max_css_rules': 3}))
self.bundle.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 1)
self.assertEquals(len(self.bundle.get_attachments('css')), 1)
@@ -166,7 +169,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that the bundle's cache is working, i.e. that a bundle creates only enough
ir.attachment records when rendered multiple times.
"""
- bundle0 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
+ bundle0 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'max_css_rules': 1}))
bundle0.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 3)
@@ -179,7 +182,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
ira2 = self.registry['ir.attachment'].browse(self.cr, self.uid, self._any_ira_for_bundle('css')[2])
date2 = ira2.create_date
- bundle1 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
+ bundle1 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'max_css_rules': 1}))
bundle1.css()
self.assertEquals(len(self._any_ira_for_bundle('css')), 3)
@@ -200,7 +203,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_10_paginated_css_date_invalidation(self):
""" Checks that a bundle is invalidated when one of its assets' modification date is changed.
"""
- bundle0 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
+ bundle0 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'max_css_rules': 1}))
bundle0.css()
last_modified0 = bundle0.last_modified
version0 = bundle0.version
@@ -208,7 +211,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
path = get_resource_path('test_assetsbundle', 'static', 'src', 'css', 'test_cssfile1.css')
utime(path, None) # touch
- bundle1 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
+ bundle1 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'max_css_rules': 1}))
bundle1.css()
last_modified1 = bundle1.last_modified
version1 = bundle1.version
@@ -223,7 +226,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
""" Checks that a bundle is invalidated when its content is modified by adding a file to
source.
"""
- bundle0 = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
+ bundle0 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'max_css_rules': 1}))
bundle0.css()
html0 = bundle0.html
version0 = bundle0.version
@@ -245,7 +248,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
'inherit_id': bundle_id,
})
- bundle1 = AssetsBundle(self.cssbundle_xmlid, env=self.env(context={'check_view_ids': [newid]}), max_css_rules=1)
+ bundle1 = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'check_view_ids': [newid], 'max_css_rules': 1}))
bundle1.css()
html1 = bundle1.html
version1 = bundle1.version
@@ -259,7 +262,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_12_paginated_css_debug(self):
""" Check that a bundle in debug mode outputs non-minified assets.
"""
- debug_bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
+ debug_bundle = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'max_css_rules': 1}))
content = debug_bundle.to_html(debug='assets')
# find back one of the original asset file
self.assertIn('/test_assetsbundle/static/src/css/test_cssfile1.css', content)
@@ -269,7 +272,7 @@ class TestJavascriptAssetsBundle(TransactionCase):
def test_13_paginated_css_order(self):
# self.cssbundle_xlmid contains 3 rules
- self.bundle = AssetsBundle(self.cssbundle_xmlid, env=self.env, max_css_rules=1)
+ self.bundle = self._get_asset(self.cssbundle_xmlid, env=self.env(context={'max_css_rules': 1}))
stylesheets = self.bundle.css()
self.assertTrue(stylesheets[0].url.endswith('.0.css'))
@@ -339,7 +342,7 @@ class TestAssetsBundleWithIRAMock(TransactionCase):
def _bundle(self, should_create, should_unlink):
self.counter.clear()
- AssetsBundle(self.lessbundle_xmlid, env=self.env).to_html(debug='assets')
+ self.en['ir.qweb']._get_asset(self.lessbundle_xmlid, QWebContext(self.env, {})).to_html(debug='assets')
self.assertEquals(self.counter['create'], int(should_create))
self.assertEquals(self.counter['unlink'], int(should_unlink))