[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
This commit is contained in:
Christophe Matthieu
2016-06-14 09:46:25 +02:00
parent 3ebf46fcc0
commit 7ede9bcb2d
33 changed files with 3134 additions and 2154 deletions
@@ -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>
+5 -5
View File
@@ -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>
@@ -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 ''"/>
+7 -7
View File
@@ -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>
+6 -6
View File
@@ -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>
@@ -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>
+3 -3
View File
@@ -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>
+137 -137
View File
@@ -33,11 +33,11 @@
<tr>
<td>Capital souscrit - non appel&#xE9;</td>
<td class="text-right">
<span t-esc="bavar1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar1, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
<span t-esc="bavar1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar1, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -59,83 +59,83 @@
<tr>
<td>Frais d'&#xE9;tablissement</td>
<td class="text-right">
<span t-esc="bavar2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar2, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar2b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar2b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar2+bavar2b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar2+bavar2b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Frais de recherche et de d&#xE9;veloppement</td>
<td class="text-right">
<span t-esc="bavar3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar3, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar3b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar3b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar3+bavar3b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar3+bavar3b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Concessions, brevets, licences,..., droits et valeurs similaires</td>
<td class="text-right">
<span t-esc="bavar4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar4, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar4b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar4b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar4+bavar4b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar4+bavar4b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Fonds commercial</td>
<td class="text-right">
<span t-esc="bavar5" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar5, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar5b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar5b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar5+bavar5b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar5+bavar5b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres</td>
<td class="text-right">
<span t-esc="bavar6" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar6, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar6b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar6b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar6+bavar6b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar6+bavar6b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Immobilisations incorporelles en cours</td>
<td class="text-right">
<span t-esc="bavar7" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar7, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar7b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar7b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar7+bavar7b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar7+bavar7b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Avances et acomptes</td>
<td class="text-right">
<span t-esc="bavar8" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar8, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
<span t-esc="bavar8" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar8, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -149,71 +149,71 @@
<tr>
<td>Terrains</td>
<td class="text-right">
<span t-esc="bavar9" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar9, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar9b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar9b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar9+bavar9b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar9+bavar9b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Constructions</td>
<td class="text-right">
<span t-esc="bavar10" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar10, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar10b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar10b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar10+bavar10b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar10+bavar10b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Installations techniques,mat&#xE9;riel et outillage</td>
<td class="text-right">
<span t-esc="bavar11" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar11, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar11b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar11b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar11+bavar11b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar11+bavar11b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres</td>
<td class="text-right">
<span t-esc="bavar12" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar12, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar12b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar12b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar12+bavar12b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar12+bavar12b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Immobilisations corporelles en cours</td>
<td class="text-right">
<span t-esc="bavar13" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar13, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar13b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar13b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar13+bavar13b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar13+bavar13b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Avances et acomptes</td>
<td class="text-right">
<span t-esc="bavar14" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar14, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
<span t-esc="bavar14" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar14, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -227,73 +227,73 @@
<tr>
<td>Participations</td>
<td class="text-right">
<span t-esc="bavar15" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar15, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar15b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar15b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar15+bavar15b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar15+bavar15b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Cr&#xE9;ances rattach&#xE9;es &#xE0; des participations</td>
<td class="text-right">
<span t-esc="bavar16" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar16, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar16b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar16b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar16+bavar16b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar16+bavar16b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Titres immobilis&#xE9;s de l'activit&#xE9; de portefeuille</td>
<td class="text-right">
<span t-esc="bavar17" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar17, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar17b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar17b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar17+bavar17b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar17+bavar17b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres titres immobilis&#xE9;s</td>
<td class="text-right">
<span t-esc="bavar18" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar18, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar18b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar18b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar18+bavar18b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar18+bavar18b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Pr&#xEA;ts</td>
<td class="text-right">
<span t-esc="bavar19" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar19, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar19b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar19b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar19+bavar19b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar19+bavar19b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres</td>
<td class="text-right">
<span t-esc="bavar20" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar20, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar20b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar20b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar20+bavar20b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar20+bavar20b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -301,13 +301,13 @@
<strong>TOTAL I</strong>
</td>
<td class="text-right">
<span t-esc="at1a" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(at1a, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-at1b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-at1b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="at1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(at1, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -325,59 +325,59 @@
<tr>
<td>Mati&#xE8;res premi&#xE8;res et autres approvisionnements</td>
<td class="text-right">
<span t-esc="bavar21" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar21, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar21b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar21b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar21+bavar21b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar21+bavar21b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>En-cours de production [biens et services]</td>
<td class="text-right">
<span t-esc="bavar22" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar22, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar22b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar22b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar22+bavar22b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar22+bavar22b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Produits interm&#xE9;diaires et finis</td>
<td class="text-right">
<span t-esc="bavar23" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar23, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar23b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar23b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar23+bavar23b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar23+bavar23b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Marchandises</td>
<td class="text-right">
<span t-esc="bavar24" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar24, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar24b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar24b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar24+bavar24b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar24+bavar24b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Avances et acomptes vers&#xE9;s sur commandes</td>
<td class="text-right">
<span t-esc="bavar25" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar25, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
<span t-esc="bavar25" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar25, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -389,35 +389,35 @@
<tr>
<td>Cr&#xE9;ances clients et comptes rattach&#xE9;s</td>
<td class="text-right">
<span t-esc="bavar26" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar26, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar26b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar26b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar26+bavar26b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar26+bavar26b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres</td>
<td class="text-right">
<span t-esc="bavar27" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar27, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar27b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar27b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar27+bavar27b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar27+bavar27b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Capital souscrit - appel&#xE9; , non vers&#xE9;</td>
<td class="text-right">
<span t-esc="bavar28" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar28, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
<span t-esc="bavar28" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar28, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -429,55 +429,55 @@
<tr>
<td>Actions propres</td>
<td class="text-right">
<span t-esc="bavar29" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar29, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar29b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar29b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar29+bavar29b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar29+bavar29b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres titres</td>
<td class="text-right">
<span t-esc="bavar30" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar30, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-bavar30b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-bavar30b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="bavar30+bavar30b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar30+bavar30b, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Instruments de tr&#xE9;sorerie</td>
<td class="text-right">
<span t-esc="bavar31" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar31, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
<span t-esc="bavar31" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar31, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Disponibilit&#xE9;s</td>
<td class="text-right">
<span t-esc="bavar32" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar32, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
<span t-esc="bavar32" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar32, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Charges constat&#xE9;s d'avance</td>
<td class="text-right">
<span t-esc="bavar33" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar33, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
<span t-esc="bavar33" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar33, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -485,43 +485,43 @@
<strong>TOTAL II</strong>
</td>
<td class="text-right">
<span t-esc="at2a" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(at2a, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-at2b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-at2b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="at2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(at2, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Charges &#xE0; r&#xE9;partir sur plusieurs exercices ( III )</td>
<td class="text-right">
<span t-esc="bavar34" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar34, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
<span t-esc="bavar34" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar34, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Primes de remboursement des emprunts ( IV )</td>
<td class="text-right">
<span t-esc="bavar35" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar35, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
<span t-esc="bavar35" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar35, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>&#xC9;carts de conversion actif ( V )</td>
<td class="text-right">
<span t-esc="bavar36" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar36, 'monetary', res_company.currency_id)"/>
</td>
<td></td>
<td class="text-right">
<span t-esc="bavar36" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(bavar36, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -529,13 +529,13 @@
<strong>TOTAL ACTIF ( I + II + III + IV + V )</strong>
</td>
<td class="text-right">
<span t-esc="at1a+at2a" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(at1a+at2a, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="-at1b-at2b" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(-at1b-at2b, 'monetary', res_company.currency_id)"/>
</td>
<td class="text-right">
<span t-esc="actif" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span t-esc="format(actif, 'monetary', res_company.currency_id)"/>
</td>
</tr>
</table>
@@ -549,19 +549,19 @@
</tr>
<tr>
<td>Capital [dont vers&#xE9;...]</td>
<td><span t-esc="bpvar1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar1, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Primes d'&#xE9;mission, de fusion, d'apport</td>
<td><span t-esc="bpvar2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar2, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>&#xC9;carts de r&#xE9;&#xE9;valuation</td>
<td><span t-esc="bpvar3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar3, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>&#xC9;cart d'&#xE9;quivalence</td>
<td><span t-esc="bpvar4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar4, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td><strong>R&#xC9;SERVES</strong></td>
@@ -569,37 +569,37 @@
</tr>
<tr>
<td>R&#xE9;serve l&#xE9;gale</td>
<td><span t-esc="bpvar5" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar5, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>R&#xE9;serves statutaires ou contractuelles</td>
<td><span t-esc="bpvar6" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar6, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>R&#xE9;serves r&#xE9;glement&#xE9;es</td><td><span t-esc="bpvar7" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td></tr>
<td>R&#xE9;serves r&#xE9;glement&#xE9;es</td><td><span t-esc="format(bpvar7, 'monetary', res_company.currency_id)"/></td></tr>
<tr>
<td>Autres r&#xE9;serves</td>
<td><span t-esc="bpvar8" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar8, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Report &#xE0; nouveau</td>
<td><span t-esc="bpvar9" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar9, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td><strong>R&#xC9;SULTAT DE L'EXERCICE [b&#xE9;n&#xE9;fice ou perte]</strong></td>
<td><span t-esc="bpvar10" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar10, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Subventions d'investissement</td>
<td><span t-esc="bpvar11" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar11, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Provisions r&#xE9;glement&#xE9;es</td>
<td><span t-esc="bpvar12" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar12, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL I</strong></td>
<td><span t-esc="pt1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(pt1, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td><strong>PROVISIONS</strong></td>
@@ -607,15 +607,15 @@
</tr>
<tr>
<td>Provisions pour risques</td>
<td><span t-esc="bpvar13" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar13, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Provisions pour charges</td>
<td><span t-esc="bpvar14" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar14, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL II</strong></td>
<td><span t-esc="pt2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(pt2, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td><strong>DETTES</strong></td>
@@ -623,59 +623,59 @@
</tr>
<tr>
<td>Emprunts obligataires convertibles</td>
<td><span t-esc="bpvar15" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar15, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Autres emprunts obligataires</td>
<td><span t-esc="bpvar16" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar16, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Emprunts et dettes aupr&#xE8;s des &#xE9;tablissements de cr&#xE9;dit</td>
<td><span t-esc="bpvar17" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar17, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Emprunts et dettes financi&#xE8;res diverses</td>
<td><span t-esc="bpvar18" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar18, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Avances et acomptes re&#xE7;us sur commandes en cours</td>
<td><span t-esc="bpvar19" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar19, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Dettes fournisseurs et comptes rattach&#xE9;s </td>
<td><span t-esc="bpvar20" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar20, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Dettes fiscales et sociales</td>
<td><span t-esc="bpvar21" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar21, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Dettes sur immobilisations et comptes rattach&#xE9;s</td>
<td><span t-esc="bpvar22" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar22, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Autres dettes</td>
<td><span t-esc="bpvar23" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar23, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Instruments de tr&#xE9;sorerie</td>
<td><span t-esc="bpvar24" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar24, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>Produits constat&#xE9;s d'avance</td>
<td><span t-esc="bpvar25" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar25, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL III</strong></td>
<td><span t-esc="pt3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(pt3, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>&#xC9;carts de conversion passif <font face="Times-Roman">( IV )</font></td>
<td><span t-esc="bpvar26" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(bpvar26, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td><strong>TOTAL G&#xC9;N&#xC9;RAL (I + II + III + IV)</strong></td>
<td><span t-esc="passif" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(passif, 'monetary', res_company.currency_id)"/></td>
</tr>
<tr>
<td>&amp;nbsp;</td>
@@ -683,7 +683,7 @@
</tr>
<tr>
<td><strong>ACTIF - PASSIF</strong></td>
<td><span t-esc="round(actif-passif,2)" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/></td>
<td><span t-esc="format(round(actif-passif,2), 'monetary', res_company.currency_id)"/></td>
</tr>
</tbody>
</table>
+53 -53
View File
@@ -35,61 +35,61 @@
<tr>
<td>Achat de marchandises</td>
<td>
<span class="text-right" t-esc="cdrc1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc1, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Variation des stocks</td>
<td>
<span class="text-right" t-esc="cdrc2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc2, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Achats de mati&#xE8;res premi&#xE8;res et autres approvisionnements</td>
<td>
<span class="text-right" t-esc="cdrc3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc3, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Variation des stocks</td>
<td>
<span class="text-right" t-esc="cdrc4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc4, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres achats et charges externes</td>
<td>
<span class="text-right" t-esc="cdrc5" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc5, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Redevances de cr&#xE9;dit-bail mobilier</td>
<td>
<span class="text-right" t-esc="cdrc6" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc6, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Redevances de cr&#xE9;dit-bail immobilier</td>
<td>
<span class="text-right" t-esc="cdrc7" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc7, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Imp&#xF4;ts, taxes et versements assimil&#xE9;s</td>
<td>
<span class="text-right" t-esc="cdrc8" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc8, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Salaires et traitements</td>
<td>
<span class="text-right" t-esc="cdrc9" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc9, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Charges sociales</td>
<td>
<span class="text-right" t-esc="cdrc10" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc10, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -99,43 +99,43 @@
<tr>
<td>Sur immobilisations : dotations aux amortissements</td>
<td>
<span class="text-right" t-esc="cdrc11" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc11, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Sur immobilisations : dotations aux d&#xE9;pr&#xE9;ciations</td>
<td>
<span class="text-right" t-esc="cdrc12" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc12, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Sur actif circulant : dotations aux d&#xE9;pr&#xE9;ciations</td>
<td>
<span class="text-right" t-esc="cdrc13" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc13, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Dotations aux provisions</td>
<td>
<span class="text-right" t-esc="cdrc14" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc14, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres charges</td>
<td>
<span class="text-right" t-esc="cdrc15" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc15, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL I</strong></td>
<td>
<span class="text-right" t-esc="ct1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(ct1, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td><strong>Quotes-parts de r&#xE9;sultat sur op&#xE9;rations faites en commun ( II )</strong></td>
<td>
<span class="text-right" t-esc="cdrc16" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc16, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -145,31 +145,31 @@
<tr>
<td>Dotations aux amortissements, aux d&#xE9;pr&#xE9;ciations et aux provisions</td>
<td>
<span class="text-right" t-esc="cdrc17" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc17, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Int&#xE9;r&#xEA;ts et charges assimil&#xE9;es</td>
<td>
<span class="text-right" t-esc="cdrc18" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc18, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Diff&#xE9;rences n&#xE9;gatives de change</td>
<td>
<span class="text-right" t-esc="cdrc19" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc19, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Charges nettes sur cessions de valeurs mobili&#xE8;res de placement</td>
<td>
<span class="text-right" t-esc="cdrc20" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc20, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL III</strong></td>
<td>
<span class="text-right" t-esc="ct3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(ct3, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -179,43 +179,43 @@
<tr>
<td>Sur op&#xE9;rations de gestion</td>
<td>
<span class="text-right" t-esc="cdrc21" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc21, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Sur op&#xE9;rations en capital</td>
<td>
<span class="text-right" t-esc="cdrc22" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc22, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Dotations aux amortissements, aux d&#xE9;pr&#xE9;ciations et aux provisions</td>
<td>
<span class="text-right" t-esc="cdrc23" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc23, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL IV</strong></td>
<td>
<span class="text-right" t-esc="ct4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(ct4, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td><strong>Participation des salariés aux résultats ( V )</strong></td>
<td>
<span class="text-right" t-esc="cdrc24" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc24, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td><strong>Impôts sur les bénéfices ( VI )</strong></td>
<td>
<span class="text-right" t-esc="cdrc25" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrc25, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL CHARGES ( I + II + III + IV+ V+ VI )</strong></td>
<td>
<span class="text-right" t-esc="charges" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(charges, 'monetary', res_company.currency_id)"/>
</td>
</tr>
@@ -236,67 +236,67 @@
<tr>
<td>Vente de marchandises</td>
<td>
<span class="text-right" t-esc="cdrp1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp1, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Production vendue [biens et services]</td>
<td>
<span class="text-right" t-esc="cdrp2" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp2, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td><strong>Sous-total A - Montant net du chiffre d'affaires</strong></td>
<td>
<span class="text-right" t-esc="pta" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(pta, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Production stock&#xE9;e</td>
<td>
<span class="text-right" t-esc="cdrp3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp3, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Production immobilis&#xE9;e</td>
<td>
<span class="text-right" t-esc="cdrp4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp4, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Subventions d'exploitation</td>
<td>
<span class="text-right" t-esc="cdrp5" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp5, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Reprises sur provisions, d&#xE9;pr&#xE9;ciations (et amortissements) et transferts de charges</td>
<td>
<span class="text-right" t-esc="cdrp6" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp6, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres produits</td>
<td>
<span class="text-right" t-esc="cdrp7" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp7, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td><strong>Sous-total B</strong></td>
<td>
<span class="text-right" t-esc="ptb" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(ptb, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL I ( A + B )</strong></td>
<td>
<span class="text-right" t-esc="pt1" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(pt1, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td><strong>Quotes-parts de r&#xE9;sultat sur op&#xE9;rations faites en commun (II)</strong></td>
<td>
<span class="text-right" t-esc="cdrp8" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp8, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -306,43 +306,43 @@
<tr>
<td>De participation</td>
<td>
<span class="text-right" t-esc="cdrp9" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp9, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>D'autres valeurs mobili&#xE8;res et cr&#xE9;ances de l'actif immobilis&#xE9;</td>
<td>
<span class="text-right" t-esc="cdrp10" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp10, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Autres int&#xE9;r&#xEA;ts et produits assimil&#xE9;s</td>
<td>
<span class="text-right" t-esc="cdrp11" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp11, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Reprises sur provisions, d&#xE9;pr&#xE9;ciations et transferts de charges</td>
<td>
<span class="text-right" t-esc="cdrp12" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp12, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Diff&#xE9;rences positives de change</td>
<td>
<span class="text-right" t-esc="cdrp13" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp13, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Produits nets sur cessions de valeurs mobili&#xE8;res de placement</td>
<td>
<span class="text-right" t-esc="cdrp14" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp14, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL III</strong></td>
<td>
<span class="text-right" t-esc="pt3" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(pt3, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
@@ -352,37 +352,37 @@
<tr>
<td>Sur op&#xE9;rations de gestion</td>
<td>
<span class="text-right" t-esc="cdrp15" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp15, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Sur op&#xE9;rations en capital</td>
<td>
<span class="text-right" t-esc="cdrp16" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp16, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td>Reprises sur provisions, d&#xE9;pr&#xE9;ciations et transferts de charges</td>
<td>
<span class="text-right" t-esc="cdrp17" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(cdrp17, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL IV</strong></td>
<td>
<span class="text-right" t-esc="pt4" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(pt4, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>TOTAL DES PRODUITS ( I + II + III + IV )</strong></td>
<td>
<span class="text-right" t-esc="produits" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(produits, 'monetary', res_company.currency_id)"/>
</td>
</tr>
<tr>
<td class="text-right"><strong>PRODUITS - CHARGES</strong></td>
<td>
<span class="text-right" t-esc="produits-charges" t-esc-options='{"widget": "monetary", "display_currency": "res_company.currency_id"}'/>
<span class="text-right" t-esc="format(produits-charges, 'monetary', res_company.currency_id)"/>
</td>
</tr>
</tbody>
+3 -2
View File
@@ -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
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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 _
+100 -120
View File
@@ -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>'))]
template_attributes['call'] = template_attributes['snippet']
return self.render_tag_call(element, template_attributes, generated_attributes, qwebcontext)
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)
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
+6 -7
View File
@@ -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
+1 -1
View File
@@ -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"),
@@ -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
+3 -3
View File
@@ -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
+41 -17
View File
@@ -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)
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 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 _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 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)
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
@@ -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>
+1 -1
View File
@@ -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):
@@ -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">
@@ -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>
-1
View File
@@ -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")
File diff suppressed because it is too large Load Diff
@@ -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
@@ -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]
+463
View File
@@ -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'))
+266
View File
@@ -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
+972
View File
@@ -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))
+381
View File
@@ -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()
+1 -1
View File
@@ -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,
)
@@ -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))