diff options
| author | Christophe Matthieu <chm@odoo.com> | 2016-05-02 16:03:48 +0200 |
|---|---|---|
| committer | Christophe Matthieu <chm@odoo.com> | 2016-06-14 09:46:25 +0200 |
| commit | 7ede9bcb2de9d52994b3a6fcb84edc3f81d60284 (patch) | |
| tree | bf91070143134b5b5ef31334c913bf52089c6710 | |
| parent | 3ebf46fcc0ca048fcc1d433a073b94e64fb7049c (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
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é</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'é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é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é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éances rattachées à 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és de l'activité 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é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ê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ères premiè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é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é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éances clients et comptes rattaché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é , non versé</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é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é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é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 à ré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>É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é...]</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'é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>Écarts de réé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>Écart d'é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ÉSERVES</strong></td> @@ -569,37 +569,37 @@ </tr> <tr> <td>Réserve lé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é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éserves réglementées</td><td><span t-esc="bpvar7" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td></tr> + <td>Réserves réglementées</td><td><span t-esc="format(bpvar7, 'monetary', res_company.currency_id)"/></td></tr> <tr> <td>Autres ré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 à 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ÉSULTAT DE L'EXERCICE [béné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églementé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ès des établissements de cré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è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ç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é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é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é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é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>É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ÉNÉ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>&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ères premiè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é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é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ôts, taxes et versements assimilé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épré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épré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ésultat sur opé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épré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érêts et charges assimilé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érences né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è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é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é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épré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é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é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épré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ésultat sur opé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ères et créances de l'actif immobilisé</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érêts et produits assimilé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épré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é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è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é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é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épré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)) |
