summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaphael Collet <rco@openerp.com>2015-07-06 17:01:33 +0200
committerRaphael Collet <rco@openerp.com>2015-07-06 17:01:33 +0200
commitb2176210d9f0749bbdcc9e8c4cd1476e8f2ebda9 (patch)
tree39708e89570536de313e49487c0ebfe339fc370d
parentf03b3e18392088bf745065294734f190f5185f9d (diff)
parent9bd60ca93510e410a0136b8b433f596330900593 (diff)
Merge pull request #7202 from odoo-dev/master-i18n-rco
Make XML/HTML fields translatable like views Add support for translating fields not as a whole, but as a value containing terms to translate: - for such fields, the table `ir_translation` contains a set of (term, translation) for each record; - use this support for the `arch` field of views, and convert translations of type 'view' into those; - add "fuzzy" remapping of translations when English value of such a field is updated. Convert translation types 'field', 'help' into corresponding translated fields of `ir.model.fields`.
-rw-r--r--addons/account/views/account_invoice_view.xml2
-rw-r--r--addons/account/views/account_view.xml2
-rw-r--r--addons/account/views/product_view.xml2
-rw-r--r--addons/auth_oauth/res_users.xml2
-rw-r--r--addons/base_gengo/ir_translation.py19
-rw-r--r--addons/base_gengo/ir_translation.xml2
-rw-r--r--addons/base_setup/base_setup_views.xml2
-rw-r--r--addons/base_setup/res_config_view.xml2
-rw-r--r--addons/crm/crm_lead_view.xml12
-rw-r--r--addons/crm/sales_team_view.xml2
-rw-r--r--addons/crm_partner_assign/crm_lead_view.xml8
-rw-r--r--addons/crm_partner_assign/res_partner_view.xml2
-rw-r--r--addons/delivery/delivery_view.xml50
-rw-r--r--addons/document/document_view.xml2
-rw-r--r--addons/document/wizard/document_configuration_view.xml2
-rw-r--r--addons/event/event_view.xml2
-rw-r--r--addons/event_sale/views/event.xml2
-rw-r--r--addons/hr_gamification/views/gamification.xml2
-rw-r--r--addons/hr_payroll/hr_payroll_view.xml2
-rw-r--r--addons/hr_payroll_account/hr_payroll_account_view.xml2
-rw-r--r--addons/hr_timesheet/hr_timesheet_view.xml2
-rw-r--r--addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml2
-rw-r--r--addons/l10n_in_hr_payroll/l10n_in_hr_payroll_view.xml4
-rw-r--r--addons/mail/views/res_config_views.xml2
-rw-r--r--addons/mail/views/res_partner_views.xml2
-rw-r--r--addons/mrp/mrp_view.xml2
-rw-r--r--addons/mrp_byproduct/mrp_byproduct_view.xml2
-rw-r--r--addons/note/note_view.xml2
-rw-r--r--addons/point_of_sale/point_of_sale_view.xml6
-rw-r--r--addons/pos_discount/views/views.xml4
-rw-r--r--addons/pos_loyalty/views/views.xml4
-rw-r--r--addons/pos_reprint/views/views.xml2
-rw-r--r--addons/project_timesheet/project_timesheet_view.xml2
-rw-r--r--addons/report/views/views.xml2
-rw-r--r--addons/report_webkit/company_view.xml2
-rw-r--r--addons/report_webkit/ir_report_view.xml2
-rw-r--r--addons/sale/report/sale_report_view.xml2
-rw-r--r--addons/sale/sale_view.xml4
-rw-r--r--addons/sale_journal/sale_journal_view.xml4
-rw-r--r--addons/sale_layout/views/sale_layout_category_view.xml4
-rw-r--r--addons/sale_mrp/sale_mrp_view.xml2
-rw-r--r--addons/sale_stock/report/sale_report_view.xml2
-rw-r--r--addons/sale_stock/sale_stock_view.xml8
-rw-r--r--addons/sales_team/sales_team.xml2
-rw-r--r--addons/stock/stock_view.xml2
-rw-r--r--addons/stock_picking_wave/stock_picking_wave_view.xml2
-rw-r--r--addons/website/controllers/main.py12
-rw-r--r--addons/website/views/res_config.xml2
-rw-r--r--addons/website/views/website_views.xml2
-rw-r--r--addons/website_crm_score/views/sales.xml6
-rw-r--r--addons/website_livechat/views/res_config.xml2
-rw-r--r--addons/website_quote/views/website_quotation_backend.xml2
-rw-r--r--addons/website_slides/view/res_config.xml2
-rw-r--r--addons/website_version/models/ir_ui_view.py7
-rw-r--r--addons/website_version/views/website_version_views.xml2
-rw-r--r--openerp/addons/base/base.sql10
-rw-r--r--openerp/addons/base/ir/ir_actions.xml2
-rw-r--r--openerp/addons/base/ir/ir_attachment_view.xml2
-rw-r--r--openerp/addons/base/ir/ir_model.py7
-rw-r--r--openerp/addons/base/ir/ir_model_view.xml14
-rw-r--r--openerp/addons/base/ir/ir_translation.py345
-rw-r--r--openerp/addons/base/ir/ir_translation_view.xml3
-rw-r--r--openerp/addons/base/ir/ir_ui_view.py86
-rw-r--r--openerp/addons/base/ir/ir_ui_view_view.xml6
-rw-r--r--openerp/addons/base/res/res_company_view.xml4
-rw-r--r--openerp/addons/base/res/res_config.xml2
-rw-r--r--openerp/addons/base/res/res_users_view.xml2
-rw-r--r--openerp/addons/base/tests/test_translate.py117
-rw-r--r--openerp/addons/base/tests/test_views.py11
-rw-r--r--openerp/fields.py99
-rw-r--r--openerp/models.py147
-rw-r--r--openerp/osv/expression.py2
-rw-r--r--openerp/tools/mail.py2
-rw-r--r--openerp/tools/translate.py261
74 files changed, 880 insertions, 474 deletions
diff --git a/addons/account/views/account_invoice_view.xml b/addons/account/views/account_invoice_view.xml
index ca5143a5543..1259df6f472 100644
--- a/addons/account/views/account_invoice_view.xml
+++ b/addons/account/views/account_invoice_view.xml
@@ -519,7 +519,7 @@
<group expand="0" string="Group By">
<filter name="group_by_partner_id" string="Partner" context="{'group_by':'partner_id'}"/>
<filter string="Salesperson" context="{'group_by':'user_id'}"/>
- <filter string="Status" context="{'group_by':'state'}"/>
+ <filter name="status" string="Status" context="{'group_by':'state'}"/>
<separator/>
<filter string="Period" context="{'group_by':'date'}"/>
<filter string="Due Month" context="{'group_by':'date_due'}"/>
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index 2a8ea72872f..4291732688a 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -1625,7 +1625,7 @@
<form position="attributes">
<attribute name="string">Accounting Application Configuration</attribute>
</form>
- <group string="res_config_contents" position="replace">
+ <group name="res_config_contents" position="replace">
<field name="only_one_chart_template" invisible="1"/>
<field name="complete_tax_set" invisible="1"/>
<group col="1">
diff --git a/addons/account/views/product_view.xml b/addons/account/views/product_view.xml
index 91bab738f0a..a44b0b4b29b 100644
--- a/addons/account/views/product_view.xml
+++ b/addons/account/views/product_view.xml
@@ -7,7 +7,7 @@
<field name="priority">5</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
- <page string="Sales" position="after">
+ <page name="sales" position="after">
<page string="Accounting" groups="account.group_account_invoice">
<group>
<label for="categ_id" string="Internal Category"/>
diff --git a/addons/auth_oauth/res_users.xml b/addons/auth_oauth/res_users.xml
index feae1b16138..f9132d1eede 100644
--- a/addons/auth_oauth/res_users.xml
+++ b/addons/auth_oauth/res_users.xml
@@ -7,7 +7,7 @@
<field name="type">form</field>
<field name="inherit_id" ref="base.view_users_form"/>
<field name="arch" type="xml">
- <xpath expr="//page[@string='Access Rights']" position="after">
+ <xpath expr="//page[@name='access_rights']" position="after">
<page string="Oauth">
<group>
<field name="oauth_provider_id"/>
diff --git a/addons/base_gengo/ir_translation.py b/addons/base_gengo/ir_translation.py
index 9885c24df65..f1297f96e97 100644
--- a/addons/base_gengo/ir_translation.py
+++ b/addons/base_gengo/ir_translation.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from openerp.osv import fields, osv
+from openerp.osv import fields, osv, api
from openerp.tools.translate import _
from openerp.exceptions import UserError
@@ -84,3 +84,20 @@ class ir_translation(osv.Model):
"""
params += ('machine', 'standard', 'ultra', 'pro',)
return (query, params)
+
+ @api.model
+ def _get_terms_query(self, field, records):
+ query, params = super(ir_translation, self)._get_terms_query(field, records)
+ # order translations from worst to best
+ query += """
+ ORDER BY
+ CASE
+ WHEN gengo_translation=%s then 10
+ WHEN gengo_translation=%s then 20
+ WHEN gengo_translation=%s then 30
+ WHEN gengo_translation=%s then 40
+ ELSE 0
+ END ASC
+ """
+ params += ('machine', 'standard', 'ultra', 'pro')
+ return query, params
diff --git a/addons/base_gengo/ir_translation.xml b/addons/base_gengo/ir_translation.xml
index ba9667d979e..5195c65abe8 100644
--- a/addons/base_gengo/ir_translation.xml
+++ b/addons/base_gengo/ir_translation.xml
@@ -18,7 +18,7 @@
<field name="inherit_id" ref="base.view_translation_form"/>
<field name="model">ir.translation</field>
<field name="arch" type="xml">
- <xpath expr="//form[@string='Translations']/sheet" position="inside">
+ <xpath expr="//form/sheet" position="inside">
<group string="Gengo Translation Service" col="4" colspan="4">
<field name="gengo_translation" />
<label string="Note: If the translation state is 'In Progress', it means that the translation has to be approved to be uploaded in this system. You are supposed to do that directly by using your Gengo Account"/>
diff --git a/addons/base_setup/base_setup_views.xml b/addons/base_setup/base_setup_views.xml
index b1d0c3344e3..1883dcc3ea8 100644
--- a/addons/base_setup/base_setup_views.xml
+++ b/addons/base_setup/base_setup_views.xml
@@ -12,7 +12,7 @@
<form position="attributes">
<attribute name="string">Specify Your Terminology</attribute>
</form>
- <group string="res_config_contents" position="replace">
+ <group name="res_config_contents" position="replace">
<label string="You can use this wizard to change the terminologies for customers in the whole application."/>
<separator string="Specify Your Terminology"/>
<group colspan="4">
diff --git a/addons/base_setup/res_config_view.xml b/addons/base_setup/res_config_view.xml
index 6bd7155a50e..d9855c93a80 100644
--- a/addons/base_setup/res_config_view.xml
+++ b/addons/base_setup/res_config_view.xml
@@ -54,7 +54,7 @@
</div>
</div>
</group>
- <group>
+ <group name="authentication">
<label for="id" string="Authentication"/>
<div>
<div>
diff --git a/addons/crm/crm_lead_view.xml b/addons/crm/crm_lead_view.xml
index 28687e0af1d..bf0258bcffe 100644
--- a/addons/crm/crm_lead_view.xml
+++ b/addons/crm/crm_lead_view.xml
@@ -182,7 +182,7 @@
<page string="Internal Notes">
<field name="description"/>
</page>
- <page string="Extra Info">
+ <page name="extra" string="Extra Info">
<group>
<group string="Mailings">
<field name="opt_out"/>
@@ -196,7 +196,7 @@
<field name="medium_id"/>
<field name="source_id" />
</group>
- <group string="Misc">
+ <group name="misc" string="Misc">
<field name="probability" groups="base.group_no_one"/>
<field name="active"/>
<field name="referred"/>
@@ -373,7 +373,7 @@
<filter string="Salesperson" domain="[]" context="{'group_by':'user_id'}"/>
<filter string="Sales Team" domain="[]" context="{'group_by':'team_id'}" groups="base.group_multi_salesteams"/>
<filter string="Stage" domain="[]" context="{'group_by':'stage_id'}"/>
- <filter string="Customer" help="Partner" domain="[]" context="{'group_by':'partner_id'}"/>
+ <filter name="customer" string="Customer" help="Partner" domain="[]" context="{'group_by':'partner_id'}"/>
<filter string="Campaign" domain="[]" context="{'group_by':'campaign_id'}"/>
<filter string="Source" domain="[]" context="{'group_by':'source_id'}"/>
<filter string="Channel" domain="[]" context="{'group_by':'medium_id'}"/>
@@ -472,7 +472,7 @@
<page string="Internal Notes">
<field name="description"/>
</page>
- <page string="Lead">
+ <page name="lead" string="Lead">
<group>
<group>
<field name="partner_name"/>
@@ -512,7 +512,7 @@
<field name="referred"/>
<field name="type" invisible="1"/>
</group>
- <group string="References">
+ <group name="references" string="References">
<field name="ref"/>
<field name="ref2"/>
</group>
@@ -601,7 +601,7 @@
<filter string="Sales Team" context="{'group_by':'team_id'}" groups="base.group_multi_salesteams"/>
<filter string="Country" context="{'group_by':'country_id'}" />
<filter string="Company" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
- <filter string="Stage" context="{'group_by':'stage_id'}"/>
+ <filter name="stage" string="Stage" context="{'group_by':'stage_id'}"/>
<filter string="Campaign" domain="[]" context="{'group_by':'campaign_id'}"/>
<filter string="Channel" domain="[]" context="{'group_by':'medium_id'}"/>
<filter string="Source" domain="[]" context="{'group_by':'source_id'}"/>
diff --git a/addons/crm/sales_team_view.xml b/addons/crm/sales_team_view.xml
index 47b00ddfd43..2c1a343350b 100644
--- a/addons/crm/sales_team_view.xml
+++ b/addons/crm/sales_team_view.xml
@@ -88,7 +88,7 @@
<field name="use_leads"/><label for="use_leads" string="Leads"/>
<field name="use_opportunities" class="oe_inline"/><label for="use_opportunities"/>
</xpath>
- <xpath expr="//page[@string='Team Members']" position="after">
+ <xpath expr="//page[@name='members']" position="after">
<page string="Stages">
<separator string="Select Stages for this Sales Team"/>
<field name="stage_ids"/>
diff --git a/addons/crm_partner_assign/crm_lead_view.xml b/addons/crm_partner_assign/crm_lead_view.xml
index d14e7862a95..6acac6f76ab 100644
--- a/addons/crm_partner_assign/crm_lead_view.xml
+++ b/addons/crm_partner_assign/crm_lead_view.xml
@@ -7,7 +7,7 @@
<field name="inherit_id" ref="crm.crm_case_form_view_oppor"/>
<field name="arch" type="xml">
<data>
- <xpath expr="//notebook/page[@string='Lead']" position="after">
+ <xpath expr="//notebook/page[@name='lead']" position="after">
<page string="Assigned Partner" groups="base.group_sale_salesman">
<group name="partner_assign_group">
<group string="Partner Assignation" col="3" colspan="1">
@@ -59,7 +59,7 @@
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.view_crm_case_opportunities_filter"/>
<field name="arch" type="xml">
- <filter string="Stage" position="after">
+ <filter name="stage" position="after">
<filter string="Assigned Partner" domain="[]" context="{'group_by':'partner_assigned_id'}"/>
</filter>
<filter name="unassigned" position="after">
@@ -77,7 +77,7 @@
<field name="inherit_id" ref="crm.crm_case_form_view_leads"/>
<field name="arch" type="xml">
<data>
- <xpath expr="//notebook/page[@string='Extra Info']" position="after">
+ <xpath expr="//notebook/page[@name='extra']" position="after">
<page string="Assigned Partner" groups="base.group_sale_salesman">
<group name="partner_assign_group">
<group string="Partner Assignation" col="3">
@@ -128,7 +128,7 @@
<field name="model">crm.lead</field>
<field name="inherit_id" ref="crm.view_crm_case_leads_filter"/>
<field name="arch" type="xml">
- <filter string="Customer" position="after">
+ <filter name="customer" position="after">
<filter string="Assigned Partner" domain="[]" context="{'group_by':'partner_assigned_id'}"/>
</filter>
<filter name="unassigned" position="after">
diff --git a/addons/crm_partner_assign/res_partner_view.xml b/addons/crm_partner_assign/res_partner_view.xml
index 3493b12a5b4..4843c671149 100644
--- a/addons/crm_partner_assign/res_partner_view.xml
+++ b/addons/crm_partner_assign/res_partner_view.xml
@@ -98,7 +98,7 @@
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_res_partner_filter"/>
<field name="arch" type="xml">
- <filter string="Salesperson" position="after">
+ <filter name="salesperson" position="after">
<filter string="Activation" context="{'group_by' : 'activation'}" domain="[]" icon="terp-personal" />
</filter>
<field name="category_id" position="after">
diff --git a/addons/delivery/delivery_view.xml b/addons/delivery/delivery_view.xml
index 525a5227cfe..0f44a21d47b 100644
--- a/addons/delivery/delivery_view.xml
+++ b/addons/delivery/delivery_view.xml
@@ -230,11 +230,11 @@
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml">
- <data>
- <xpath expr="//page[@string='Additional Info']/group[last()]" position="after">
- <separator string="Carrier Information"/>
- <group>
- <group name='carrier_data'>
+ <data>
+ <xpath expr="//page[@name='extra']" position="inside">
+ <separator string="Carrier Information"/>
+ <group>
+ <group name='carrier_data'>
<field name="carrier_id"/>
<field name="delivery_type" attrs="{'invisible':True}"/>
<label for="carrier_tracking_ref"/>
@@ -243,27 +243,27 @@
<button type='object' class="fa fa-arrow-right oe_link" name="cancel_shipment" string="Cancel" attrs="{'invisible':['|','|','|',('carrier_tracking_ref','=',False),('delivery_type','=','grid'),('delivery_type','=',False),('state','not in',('done'))]}"/>
</div>
<field name="carrier_price"/>
- </group>
- <group>
- <label for="weight" string="Weight"/>
- <div>
- <field name="weight" class="oe_inline"/>
- <field name="weight_uom_id" nolabel="1" class="oe_inline"/>
- </div>
- <field name="weight_net" groups="base.group_no_one"/>
- <field name="number_of_packages"/>
- </group>
- </group>
- </xpath>
- <xpath expr="//h1" position="before">
- <div class="oe_right oe_button_box">
- <button type="object" name="open_website_url" class="oe_stat_button" icon='fa-truck' string="Tracking" attrs="{'invisible': ['|',('carrier_tracking_ref','=',False),('delivery_type','=','grid')]}" />
- </div>
- </xpath>
- <xpath expr="//form[@string='Transfer']/header/button[last()]" position="after">
+ </group>
+ <group>
+ <label for="weight" string="Weight"/>
+ <div>
+ <field name="weight" class="oe_inline"/>
+ <field name="weight_uom_id" nolabel="1" class="oe_inline"/>
+ </div>
+ <field name="weight_net" groups="base.group_no_one"/>
+ <field name="number_of_packages"/>
+ </group>
+ </group>
+ </xpath>
+ <xpath expr="//h1" position="before">
+ <div class="oe_right oe_button_box">
+ <button type="object" name="open_website_url" class="oe_stat_button" icon='fa-truck' string="Tracking" attrs="{'invisible': ['|',('carrier_tracking_ref','=',False),('delivery_type','=','grid')]}" />
+ </div>
+ </xpath>
+ <xpath expr="/form/header/button[last()]" position="after">
<button name="send_to_shipper" string="Send to Shipper" type="object" attrs="{'invisible':['|','|','|',('carrier_tracking_ref','!=',False),('delivery_type','=','grid'),('delivery_type','=',False),('state','not in',('done'))]}"/>
- </xpath>
- </data>
+ </xpath>
+ </data>
</field>
</record>
diff --git a/addons/document/document_view.xml b/addons/document/document_view.xml
index 7afaed118b7..8dafdc37be4 100644
--- a/addons/document/document_view.xml
+++ b/addons/document/document_view.xml
@@ -207,7 +207,7 @@
<field name="model">ir.attachment</field>
<field name="inherit_id" ref="base.view_attachment_search"/>
<field name="arch" type="xml">
- <filter string="Owner" position="replace">
+ <filter name="owner" position="replace">
<filter string="Owner" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}"/>
<filter string="Partner" icon="terp-partner" domain="[]" context="{'group_by':'partner_id'}"/>
<filter string="Directory" icon="terp-folder-green" domain="[]" context="{'group_by':'parent_id'}"/>
diff --git a/addons/document/wizard/document_configuration_view.xml b/addons/document/wizard/document_configuration_view.xml
index dad1a88052e..049c3a00974 100644
--- a/addons/document/wizard/document_configuration_view.xml
+++ b/addons/document/wizard/document_configuration_view.xml
@@ -10,7 +10,7 @@
<form position="attributes">
<attribute name="string">Knowledge Application Configuration</attribute>
</form>
- <group string="res_config_contents" position="replace">
+ <group name="res_config_contents" position="replace">
<separator string="Configure Directories"/>
<p>Odoo's Document Management System supports mapping virtual folders with documents. The virtual folder of a document can be used to manage the files attached to the document, or to print and download any report. This tool will create directories automatically according to modules installed.</p>
<p>When executing this wizard, it will configure your directories automatically according to modules installed.</p>
diff --git a/addons/event/event_view.xml b/addons/event/event_view.xml
index bd90a57919c..15ef033e83d 100644
--- a/addons/event/event_view.xml
+++ b/addons/event/event_view.xml
@@ -93,7 +93,7 @@
</group>
</group>
<notebook>
- <page string="Registrations">
+ <page name="registrations" string="Registrations">
<group>
<group>
<field name="auto_confirm" invisible="1"/>
diff --git a/addons/event_sale/views/event.xml b/addons/event_sale/views/event.xml
index f4d93ff22e0..c637b51e64f 100644
--- a/addons/event_sale/views/event.xml
+++ b/addons/event_sale/views/event.xml
@@ -49,7 +49,7 @@
<field name="inherit_id" ref="event.view_event_form"/>
<field name="arch" type="xml">
<data>
- <xpath expr="//page[@string='Registrations']" position="replace">
+ <xpath expr="//page[@name='registrations']" position="replace">
<page string="Tickets">
<group>
<group>
diff --git a/addons/hr_gamification/views/gamification.xml b/addons/hr_gamification/views/gamification.xml
index d305babc7de..ed4eb263e1e 100644
--- a/addons/hr_gamification/views/gamification.xml
+++ b/addons/hr_gamification/views/gamification.xml
@@ -21,7 +21,7 @@
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
- <xpath expr="//page[@string='Public Information']" position="after">
+ <xpath expr="//page[@name='public']" position="after">
<page string="Received Badges" attrs="{'invisible': [('user_id', '=', False)]}">
<field name="has_badges" invisible="1"/>
<button string="Grant a Badge" type="action" name="%(action_reward_wizard)d"/> to reward this employee for a good action
diff --git a/addons/hr_payroll/hr_payroll_view.xml b/addons/hr_payroll/hr_payroll_view.xml
index cc5e8726494..bc8b086fd40 100644
--- a/addons/hr_payroll/hr_payroll_view.xml
+++ b/addons/hr_payroll/hr_payroll_view.xml
@@ -612,7 +612,7 @@
<field name="register_id"/>
</group>
</page>
- <page string="Child Rules">
+ <page name="rules" string="Child Rules">
<field name="parent_rule_id"/>
<separator string="Children Definition"/>
<field name="child_ids"/>
diff --git a/addons/hr_payroll_account/hr_payroll_account_view.xml b/addons/hr_payroll_account/hr_payroll_account_view.xml
index f45d0745e37..618261b54f8 100644
--- a/addons/hr_payroll_account/hr_payroll_account_view.xml
+++ b/addons/hr_payroll_account/hr_payroll_account_view.xml
@@ -22,7 +22,7 @@
<field name="model">hr.salary.rule</field>
<field name="inherit_id" ref="hr_payroll.hr_salary_rule_form"/>
<field name="arch" type="xml">
- <xpath expr="/form/notebook/page[@string='Child Rules']" position="after">
+ <xpath expr="/form/notebook/page[@name='rules']" position="after">
<page string="Accounting">
<group colspan="4">
<field name="account_debit" />
diff --git a/addons/hr_timesheet/hr_timesheet_view.xml b/addons/hr_timesheet/hr_timesheet_view.xml
index 6c9114f872d..9e6450ef7aa 100644
--- a/addons/hr_timesheet/hr_timesheet_view.xml
+++ b/addons/hr_timesheet/hr_timesheet_view.xml
@@ -50,7 +50,7 @@
<field name="product_uom_id" class="oe_inline"/>
</div>
</group>
- <group string="Accounting">
+ <group name="accounting" string="Accounting">
<field domain="[('type','=','normal'),('state', '&lt;&gt;', 'close'),('parent_id','!=',False)]" name="account_id" select="1" options="{'no_open': True, 'no_create': True}"/>
<field name="amount"/>
<field name="general_account_id"/>
diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml
index 69837e9fe72..30766544e13 100644
--- a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml
+++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml
@@ -31,7 +31,7 @@
<field name="model">account.analytic.line</field>
<field name="inherit_id" ref="hr_timesheet.hr_timesheet_line_form"/>
<field name="arch" type="xml">
- <xpath expr="//group[@string='Accounting']" position='after'>
+ <xpath expr="//group[@name='accounting']" position='after'>
<group string="Invoicing">
<field name="to_invoice"/>
<field name="invoice_id" readonly="1"/>
diff --git a/addons/l10n_in_hr_payroll/l10n_in_hr_payroll_view.xml b/addons/l10n_in_hr_payroll/l10n_in_hr_payroll_view.xml
index 6c589799d5f..822a442631f 100644
--- a/addons/l10n_in_hr_payroll/l10n_in_hr_payroll_view.xml
+++ b/addons/l10n_in_hr_payroll/l10n_in_hr_payroll_view.xml
@@ -36,7 +36,7 @@
<newline/>
<field name="available_advice"/>
</xpath>
- <xpath expr="//button[@string='Set to Draft']" position="after">
+ <xpath expr="//button[@name='draft_payslip_run']" position="after">
<button name="create_advice" string="Create Advice" type="object" states="close" />
</xpath>
</data>
@@ -197,7 +197,7 @@
<field name="inherit_id" ref="base.view_company_form"/>
<field name="arch" type="xml">
<data>
- <page string="Configuration" position="inside">
+ <page name="configuration" position="inside">
<group name="rule" string="Payroll">
<field name="dearness_allowance"/>
</group>
diff --git a/addons/mail/views/res_config_views.xml b/addons/mail/views/res_config_views.xml
index 441f42b5a43..189a1f4b2ea 100644
--- a/addons/mail/views/res_config_views.xml
+++ b/addons/mail/views/res_config_views.xml
@@ -6,7 +6,7 @@
<field name="model">base.config.settings</field>
<field name="inherit_id" ref="base_setup.view_general_configuration"/>
<field name="arch" type="xml">
- <xpath expr="//button[@string='Configure outgoing email servers']" position='after'>
+ <xpath expr="//div[@name='email.outgoing_server']/button[1]" position='after'>
<button class="oe_inline oe_link" style="display: inline-block;" name= "%(action_view_mail_mail)d" type="action"
context="{'search_default_exception': 1, 'search_default_outgoing': 0}"
attrs="{'invisible': [('fail_counter','=',0)]}">
diff --git a/addons/mail/views/res_partner_views.xml b/addons/mail/views/res_partner_views.xml
index 97942b5561a..da3338a19fa 100644
--- a/addons/mail/views/res_partner_views.xml
+++ b/addons/mail/views/res_partner_views.xml
@@ -29,7 +29,7 @@
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_res_partner_filter"/>
<field name="arch" type="xml">
- <filter string="Suppliers" position="after">
+ <filter name="supplier" position="after">
<separator />
<filter string="Available for mass mailing"
name='not_opt_out' domain="[('opt_out', '=', False)]"
diff --git a/addons/mrp/mrp_view.xml b/addons/mrp/mrp_view.xml
index 83b8e51e002..0d53c8671b6 100644
--- a/addons/mrp/mrp_view.xml
+++ b/addons/mrp/mrp_view.xml
@@ -543,7 +543,7 @@
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_search_view"/>
<field name="arch" type="xml">
- <xpath expr="//filter[@string='Consumable']" position="after">
+ <xpath expr="//filter[@name='consumable']" position="after">
<separator/>
<filter string="Components" name="components" icon="terp-accessories-archiver" domain="[('bom_ids','not in',[])]"/>
</xpath>
diff --git a/addons/mrp_byproduct/mrp_byproduct_view.xml b/addons/mrp_byproduct/mrp_byproduct_view.xml
index 32e8a696369..0e6b6941d0a 100644
--- a/addons/mrp_byproduct/mrp_byproduct_view.xml
+++ b/addons/mrp_byproduct/mrp_byproduct_view.xml
@@ -7,7 +7,7 @@
<field name="inherit_id" ref="mrp.mrp_bom_form_view"/>
<field name="arch" type="xml">
<notebook position="inside">
- <page string="Byproducts" position="inside">
+ <page string="Byproducts">
<field name="sub_products">
<tree string="Byproducts" editable="top">
<field name="product_id" on_change="onchange_product_id(product_id)"/>
diff --git a/addons/note/note_view.xml b/addons/note/note_view.xml
index 9a9f5057418..a6a6876d3f7 100644
--- a/addons/note/note_view.xml
+++ b/addons/note/note_view.xml
@@ -146,7 +146,7 @@
<field name="model">base.config.settings</field>
<field name="inherit_id" ref="base_setup.view_general_configuration"/>
<field name="arch" type="xml">
- <xpath expr="//label[@string='Authentication']/.." position="after">
+ <xpath expr="//group[@name='authentication']" position="after">
<group>
<label for="id" string="Notes"/>
<div name="note">
diff --git a/addons/point_of_sale/point_of_sale_view.xml b/addons/point_of_sale/point_of_sale_view.xml
index db9a92d2f62..8118cdf1c9b 100644
--- a/addons/point_of_sale/point_of_sale_view.xml
+++ b/addons/point_of_sale/point_of_sale_view.xml
@@ -74,7 +74,7 @@
</tree>
</field>
</page>
- <page string="Extra Info">
+ <page name="extra" string="Extra Info">
<group string="General Information">
<field name="company_id" groups="base.group_multi_company"/>
<field name="location_id" widget="selection" groups="stock.group_locations"/>
@@ -692,7 +692,7 @@
<field name="type" />
</tree>
</field>
- <group string="Features" >
+ <group name="features" string="Features" >
<group>
<field name="iface_vkeyboard" />
<field name="iface_invoicing" />
@@ -716,7 +716,7 @@
<field name="iface_electronic_scale" />
<field name="iface_cashdrawer" />
</group>
- <group string="Receipt" >
+ <group name="receipt" string="Receipt" >
<field name="receipt_header" string="Header" placeholder="A custom receipt header message"/>
<field name="receipt_footer" string="Footer" placeholder="A custom receipt footer message"/>
</group>
diff --git a/addons/pos_discount/views/views.xml b/addons/pos_discount/views/views.xml
index 18a3f65cd26..fd75259e924 100644
--- a/addons/pos_discount/views/views.xml
+++ b/addons/pos_discount/views/views.xml
@@ -7,10 +7,10 @@
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.view_pos_config_form" />
<field name="arch" type="xml">
- <xpath expr="//group[@string='Features']/group" position="inside">
+ <xpath expr="//group[@name='features']/group[1]" position="inside">
<field name='iface_discount' />
</xpath>
- <xpath expr="//group[@string='Receipt']" position="after">
+ <xpath expr="//group[@name='receipt']" position="after">
<group string="Discounts" col="4" >
<field name='discount_pc' />
<field name="discount_product_id" />
diff --git a/addons/pos_loyalty/views/views.xml b/addons/pos_loyalty/views/views.xml
index 2aa4f110727..b4b7652f8cb 100644
--- a/addons/pos_loyalty/views/views.xml
+++ b/addons/pos_loyalty/views/views.xml
@@ -7,7 +7,7 @@
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.view_pos_config_form" />
<field name="arch" type="xml">
- <xpath expr="//group[@string='Receipt']" position="after">
+ <xpath expr="//group[@name='receipt']" position="after">
<group string="Loyalty Program" col="4" >
<field name="loyalty_id" />
</group>
@@ -33,7 +33,7 @@
<field name="model">pos.order</field>
<field name="inherit_id" ref="point_of_sale.view_pos_pos_form"/>
<field name="arch" type="xml">
- <xpath expr="//page[@string='Extra Info']" position="inside">
+ <xpath expr="//page[@name='extra']" position="inside">
<group name="loyalty" string="Loyalty Programs">
<field name="loyalty_points" />
</group>
diff --git a/addons/pos_reprint/views/views.xml b/addons/pos_reprint/views/views.xml
index 44117dc74a8..8bcd70e57b7 100644
--- a/addons/pos_reprint/views/views.xml
+++ b/addons/pos_reprint/views/views.xml
@@ -7,7 +7,7 @@
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.view_pos_config_form" />
<field name="arch" type="xml">
- <xpath expr="//group[@string='Receipt']" position="inside">
+ <xpath expr="//group[@name='receipt']" position="inside">
<field string="Reprinting" name='iface_reprint' />
</xpath>
</field>
diff --git a/addons/project_timesheet/project_timesheet_view.xml b/addons/project_timesheet/project_timesheet_view.xml
index 0d1d4cb7f99..aa574d233ba 100644
--- a/addons/project_timesheet/project_timesheet_view.xml
+++ b/addons/project_timesheet/project_timesheet_view.xml
@@ -55,7 +55,7 @@
<field name="model">project.project</field>
<field name="inherit_id" ref="project.view_project_project_filter"/>
<field name="arch" type="xml">
- <xpath expr='//filter[@string="Template"]' position='after'>
+ <xpath expr='//filter[@name="Template"]' position='after'>
<filter string="Billable" domain="[('to_invoice','!=', False)]"/>
</xpath>
</field>
diff --git a/addons/report/views/views.xml b/addons/report/views/views.xml
index 283e288621c..cec184febfd 100644
--- a/addons/report/views/views.xml
+++ b/addons/report/views/views.xml
@@ -99,7 +99,7 @@
<field name="model">res.company</field>
<field name="arch" type="xml">
<data>
- <xpath expr="//page[@string='Report Configuration']/group[@string='Configuration']" position="inside">
+ <xpath expr="//page[@name='report']/group[1]" position="inside">
<field name="paperformat_id" />
</xpath>
</data>
diff --git a/addons/report_webkit/company_view.xml b/addons/report_webkit/company_view.xml
index 566cf29e5a8..c6f9d07ebfd 100644
--- a/addons/report_webkit/company_view.xml
+++ b/addons/report_webkit/company_view.xml
@@ -5,7 +5,7 @@
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form"/>
<field name="arch" type="xml">
- <xpath expr="//page[@string='Report Configuration']/group[@string='Configuration']" position="after">
+ <xpath expr="//page[@name='report']/group[1]" position="after">
<group string="Configuration (Webkit)" col="2">
<field name="header_image"/>
<field name="header_webkit"/>
diff --git a/addons/report_webkit/ir_report_view.xml b/addons/report_webkit/ir_report_view.xml
index 37260628396..b8efd3d9b8a 100644
--- a/addons/report_webkit/ir_report_view.xml
+++ b/addons/report_webkit/ir_report_view.xml
@@ -5,7 +5,7 @@
<field name="model">ir.actions.report.xml</field>
<field name="inherit_id" ref="base.act_report_xml_view"/>
<field name="arch" type="xml">
- <xpath expr="//page[@string='Security']" position="before" >
+ <xpath expr="//page[@name='security']" position="before" >
<page string="Webkit" attrs="{'invisible':[('report_type','!=','webkit')]}">
<group>
<field name="webkit_header"/>
diff --git a/addons/sale/report/sale_report_view.xml b/addons/sale/report/sale_report_view.xml
index 00092070969..aaf3f8bd282 100644
--- a/addons/sale/report/sale_report_view.xml
+++ b/addons/sale/report/sale_report_view.xml
@@ -76,7 +76,7 @@
<filter string="Partner's Country" name="country_id" context="{'group_by':'country_id'}"/>
<filter string="Customer" name="Customer" context="{'group_by':'partner_id'}"/>
<filter string="Product Category" name="Category" context="{'group_by':'categ_id'}"/>
- <filter string="Status" context="{'group_by':'state'}"/>
+ <filter name="status" string="Status" context="{'group_by':'state'}"/>
<filter string="Company" groups="base.group_multi_company" context="{'group_by':'company_id'}"/>
<separator/>
<filter string="Order Month" context="{'group_by':'date:month'}" help="Ordered date of the sales order"/>
diff --git a/addons/sale/sale_view.xml b/addons/sale/sale_view.xml
index 03c687d4f91..0058f1e055b 100644
--- a/addons/sale/sale_view.xml
+++ b/addons/sale/sale_view.xml
@@ -354,7 +354,7 @@
<filter string="New Mail" name="message_unread" domain="[('message_unread','=',True)]"/>
<group expand="0" string="Group By">
<filter string="Salesperson" domain="[]" context="{'group_by':'user_id'}"/>
- <filter string="Customer" domain="[]" context="{'group_by':'partner_id'}"/>
+ <filter name="customer" string="Customer" domain="[]" context="{'group_by':'partner_id'}"/>
<filter string="Order Month" domain="[]" context="{'group_by':'date_order'}"/>
</group>
</search>
@@ -640,7 +640,7 @@
<xpath expr="//field[@name='user_id']" position="after">
<field name="team_id" groups="base.group_multi_salesteams"/>
</xpath>
- <xpath expr="//group/filter[@string='Status']" position="after">
+ <xpath expr="//group/filter[@name='status']" position="after">
<filter string="Sales Team" domain="[]" context="{'group_by':'team_id'}" groups="base.group_multi_salesteams"/>
</xpath>
</field>
diff --git a/addons/sale_journal/sale_journal_view.xml b/addons/sale_journal/sale_journal_view.xml
index 03c8c57bdc8..a0f4e9b72f0 100644
--- a/addons/sale_journal/sale_journal_view.xml
+++ b/addons/sale_journal/sale_journal_view.xml
@@ -101,7 +101,7 @@
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_sales_order_filter"/>
<field name="arch" type="xml">
- <xpath expr="//group/filter[@string='Customer']" position="after">
+ <xpath expr="//group/filter[@name='customer']" position="after">
<filter string="Invoice Type" domain="[]" context="{'group_by':'invoice_type_id'}"/>
</xpath>
</field>
@@ -114,7 +114,7 @@
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_internal_search"/>
<field name="arch" type="xml">
- <xpath expr="//group/filter[@string='Picking Type']" position="after">
+ <xpath expr="//group/filter[@name='picking_type']" position="after">
<filter string="Invoice Type" domain="[]" context="{'group_by':'invoice_type_id'}"/>
</xpath>
</field>
diff --git a/addons/sale_layout/views/sale_layout_category_view.xml b/addons/sale_layout/views/sale_layout_category_view.xml
index 942c5128fea..90621502221 100644
--- a/addons/sale_layout/views/sale_layout_category_view.xml
+++ b/addons/sale_layout/views/sale_layout_category_view.xml
@@ -8,7 +8,7 @@
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<data>
- <xpath expr="//notebook/page/field[@name='order_line']/tree[@string='Sales Order Lines']/field[@name='product_id']" position="after">
+ <xpath expr="//field[@name='order_line']/tree/field[@name='product_id']" position="after">
<field name="sale_layout_cat_id"/>
</xpath>
<xpath expr="//field[@name='order_line']/form/group/group/field[@name='product_id']" position="after">
@@ -25,7 +25,7 @@
<field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml">
<data>
- <xpath expr="//page[@string='Invoice Lines']/field[@name='invoice_line_ids']/tree/field[@name='product_id']" position="after">
+ <xpath expr="//page/field[@name='invoice_line_ids']/tree/field[@name='product_id']" position="after">
<field name="sale_layout_cat_id"/>
</xpath>
diff --git a/addons/sale_mrp/sale_mrp_view.xml b/addons/sale_mrp/sale_mrp_view.xml
index 99fd238d0c2..002a5641fc5 100644
--- a/addons/sale_mrp/sale_mrp_view.xml
+++ b/addons/sale_mrp/sale_mrp_view.xml
@@ -21,7 +21,7 @@
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
- <xpath expr="//page[@string='Order Lines']/field[@name='order_line']/form[@string='Sales Order Lines']/group/group/field[@name='th_weight']" position="after">
+ <xpath expr="//page/field[@name='order_line']/form/group/group/field[@name='th_weight']" position="after">
<field name="property_ids" widget="many2many_tags"
groups="sale.group_mrp_properties"/>
</xpath>
diff --git a/addons/sale_stock/report/sale_report_view.xml b/addons/sale_stock/report/sale_report_view.xml
index 9628af9d8d1..34b3ae5d8c8 100644
--- a/addons/sale_stock/report/sale_report_view.xml
+++ b/addons/sale_stock/report/sale_report_view.xml
@@ -11,7 +11,7 @@
<separator/>
<filter icon="terp-accessories-archiver" string="Picked" domain="[('shipped','=',True)]"/>
</filter>
- <xpath expr="//group/filter[@string='Status']" position="after">
+ <xpath expr="//group/filter[@name='status']" position="after">
<filter string="Warehouse" context="{'group_by':'warehouse_id'}"/>
</xpath>
</field>
diff --git a/addons/sale_stock/sale_stock_view.xml b/addons/sale_stock/sale_stock_view.xml
index fb4e9e8383e..ed081b95c20 100644
--- a/addons/sale_stock/sale_stock_view.xml
+++ b/addons/sale_stock/sale_stock_view.xml
@@ -62,20 +62,20 @@
<xpath expr="//group[@name='technical']" position="inside">
<field name="shipped" groups="base.group_no_one"/>
</xpath>
- <xpath expr="//page[@string='Order Lines']/field[@name='order_line']/form[@string='Sales Order Lines']/group/group/field[@name='tax_id']" position="after">
+ <xpath expr="//page/field[@name='order_line']/form/group/group/field[@name='tax_id']" position="after">
<label for="delay"/>
<div>
<field name="delay" class="oe_inline"/> days
</div>
</xpath>
- <xpath expr="//page[@string='Order Lines']/field[@name='order_line']/form[@string='Sales Order Lines']/group/group/field[@name='tax_id']" position="before">
+ <xpath expr="//page/field[@name='order_line']/form/group/group/field[@name='tax_id']" position="before">
<field name="product_tmpl_id" invisible="1"/>
<field name="product_packaging" context="{'default_product_tmpl_id': product_tmpl_id, 'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}" on_change="product_packaging_change(parent.pricelist_id, product_id, product_uom_qty, product_uom, parent.partner_id, product_packaging, True, context)" domain="[('product_tmpl_id','=',product_tmpl_id)]" groups="product.group_stock_packaging" />
</xpath>
- <xpath expr="//page[@string='Order Lines']/field[@name='order_line']/tree[@string='Sales Order Lines']/field[@name='sequence']" position="after">
+ <xpath expr="//page/field[@name='order_line']/tree/field[@name='sequence']" position="after">
<field name="delay" invisible="1"/>
</xpath>
- <xpath expr="//page[@string='Order Lines']/field[@name='order_line']/tree[@string='Sales Order Lines']/field[@name='th_weight']" position="after">
+ <xpath expr="//page/field[@name='order_line']/tree/field[@name='th_weight']" position="after">
<field name="product_packaging" invisible="1"/>
</xpath>
<xpath expr="//group[@name='sale_pay']" position="inside">
diff --git a/addons/sales_team/sales_team.xml b/addons/sales_team/sales_team.xml
index f8c563f1417..d0f438f52ca 100644
--- a/addons/sales_team/sales_team.xml
+++ b/addons/sales_team/sales_team.xml
@@ -70,7 +70,7 @@
</group>
</group>
<notebook colspan="4">
- <page string="Team Members">
+ <page name="members" string="Team Members">
<field name="member_ids" widget="many2many_kanban">
<kanban quick_create="false" create="true" delete="true">
<field name="name"/>
diff --git a/addons/stock/stock_view.xml b/addons/stock/stock_view.xml
index da1234ed48c..ae88eb9d88c 100644
--- a/addons/stock/stock_view.xml
+++ b/addons/stock/stock_view.xml
@@ -856,7 +856,7 @@
<filter string="Order Date" icon="terp-go-month" domain="[]" context="{'group_by':'date'}"/>
<filter string="Expected Date" icon="terp-go-month" domain="[]" context="{'group_by':'min_date'}"/>
<filter string="Origin" domain="[]" context="{'group_by':'origin'}"/>
- <filter string="Picking Type" domain="[]" context="{'group_by':'picking_type_id'}"/>
+ <filter name="picking_type" string="Picking Type" domain="[]" context="{'group_by':'picking_type_id'}"/>
<filter string="Procurement Group" domain="[]" context="{'group_by':'group_id'}"/>
</group>
</search>
diff --git a/addons/stock_picking_wave/stock_picking_wave_view.xml b/addons/stock_picking_wave/stock_picking_wave_view.xml
index e79bf4b4c8f..0db76a5aa67 100644
--- a/addons/stock_picking_wave/stock_picking_wave_view.xml
+++ b/addons/stock_picking_wave/stock_picking_wave_view.xml
@@ -120,7 +120,7 @@
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_internal_search"/>
<field name="arch" type="xml">
- <xpath expr="//filter[@string='Picking Type']" position="after">
+ <xpath expr="//filter[@name='picking_type']" position="after">
<filter string="Wave" domain="[]" context="{'group_by':'wave_id'}"/>
</xpath>
</field>
diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py
index f300487e7b8..9f01f71b2a0 100644
--- a/addons/website/controllers/main.py
+++ b/addons/website/controllers/main.py
@@ -253,7 +253,8 @@ class Website(openerp.addons.web.controllers.main.Home):
old_trans = irt.search_read(
request.cr, request.uid,
[
- ('type', '=', 'view'),
+ ('type', '=', 'model'),
+ ('name', '=', 'ir.ui.view,arch_db'),
('res_id', '=', view_id),
('lang', '=', lang),
('src', '=', initial_content),
@@ -261,15 +262,16 @@ class Website(openerp.addons.web.controllers.main.Home):
if old_trans:
tid = old_trans[0]['id']
if tid:
- vals = {'value': new_content}
+ vals = {'value': new_content, 'state': 'translated'}
irt.write(request.cr, request.uid, [tid], vals)
else:
new_trans = {
- 'name': 'website',
+ 'type': 'model',
+ 'name': 'ir.ui.view,arch_db',
+ 'state': 'translated',
'res_id': view_id,
'lang': lang,
- 'type': 'view',
- 'source': initial_content,
+ 'src': initial_content,
'value': new_content,
}
if t.get('gengo_translation'):
diff --git a/addons/website/views/res_config.xml b/addons/website/views/res_config.xml
index b0002e61b29..04e67dd7ed1 100644
--- a/addons/website/views/res_config.xml
+++ b/addons/website/views/res_config.xml
@@ -25,7 +25,7 @@
</div>
</div>
</group>
- <group string="Social Media">
+ <group name="social" string="Social Media">
<label for="social_twitter"/>
<div name="social_twitter">
<div class="oe_inline">
diff --git a/addons/website/views/website_views.xml b/addons/website/views/website_views.xml
index ebb1728bf46..58c13347619 100644
--- a/addons/website/views/website_views.xml
+++ b/addons/website/views/website_views.xml
@@ -23,7 +23,7 @@
<button string="Cancel" type="object" name="noop" class="oe_link"/>
</header>
<div name="domain">
- <separator string="Domain"/>
+ <separator name="domain" string="Domain"/>
<group name="domain">
<field name="name"/>
<field name="domain"/>
diff --git a/addons/website_crm_score/views/sales.xml b/addons/website_crm_score/views/sales.xml
index 08045f9e38e..b4518eec4ef 100644
--- a/addons/website_crm_score/views/sales.xml
+++ b/addons/website_crm_score/views/sales.xml
@@ -51,7 +51,7 @@
<field name="priority" position="after">
<field name="score" />
</field>
- <xpath expr="//group[@string='Misc']" position="after">
+ <xpath expr="//group[@name='misc']" position="after">
<group string="Scoring">
<field name="score_ids"
widget="many2many_tags"
@@ -84,7 +84,7 @@
<field name="user_id" position="after">
<field name="score" />
</field>
- <xpath expr="//group[@string='References']" position="after">
+ <xpath expr="//group[@name='references']" position="after">
<group string="Scoring">
<field name="score_ids"
widget="many2many_tags"
@@ -169,7 +169,7 @@
</div>
<field name="min_for_assign" class="oe_inline"/>
</xpath>
- <xpath expr="//page[@string='Team Members']" position="replace">
+ <xpath expr="//page[@name='members']" position="replace">
<page string="Assignation">
<field name="team_user_ids" widget="one2many" mode="kanban" context="{'default_team_id':active_id, 'ro_team':True}">
<kanban>
diff --git a/addons/website_livechat/views/res_config.xml b/addons/website_livechat/views/res_config.xml
index ac69929ca63..178cb20ae27 100644
--- a/addons/website_livechat/views/res_config.xml
+++ b/addons/website_livechat/views/res_config.xml
@@ -7,7 +7,7 @@
<field name="model">website.config.settings</field>
<field name="inherit_id" ref="website.view_website_config_settings"/>
<field name="arch" type="xml">
- <xpath expr="//group[@string='Social Media']" position="inside">
+ <xpath expr="//group[@name='social']" position="inside">
<label for="channel_id"/>
<div name="channel_id">
<div class="oe_inline">
diff --git a/addons/website_quote/views/website_quotation_backend.xml b/addons/website_quote/views/website_quotation_backend.xml
index a1cb0b8112e..fc8449a90a6 100644
--- a/addons/website_quote/views/website_quotation_backend.xml
+++ b/addons/website_quote/views/website_quotation_backend.xml
@@ -17,7 +17,7 @@
<xpath expr="//header/button[@name='print_quotation'][1]" position="replace">
<button name="print_quotation" string="Print" type="object" states="draft" groups="base.group_user"/>
</xpath>
- <xpath expr="//page[@string='Order Lines']" position="after">
+ <xpath expr="//page/field[@name='order_line']/.." position="after">
<page string="Suggested Products">
<label string="Optional Products &amp; Services" for="options"/>
<field name="quote_viewed" invisible="1"/>
diff --git a/addons/website_slides/view/res_config.xml b/addons/website_slides/view/res_config.xml
index 152041c86ce..0c9be892a59 100644
--- a/addons/website_slides/view/res_config.xml
+++ b/addons/website_slides/view/res_config.xml
@@ -7,7 +7,7 @@
<field name="model">website.config.settings</field>
<field name="inherit_id" ref="website.view_website_config_settings"/>
<field name="arch" type="xml">
- <xpath expr="//group[@string='Social Media']" position="after">
+ <xpath expr="//group[@name='social']" position="after">
<group string="Website Slides">
<field name="website_slide_google_app_key" />
</group>
diff --git a/addons/website_version/models/ir_ui_view.py b/addons/website_version/models/ir_ui_view.py
index 94fa6cab45e..4a3ded70f43 100644
--- a/addons/website_version/models/ir_ui_view.py
+++ b/addons/website_version/models/ir_ui_view.py
@@ -170,7 +170,12 @@ class view(models.Model):
for v in view_list:
views = self.customize_template_get(v.id, full=True)
views_ids += [v.get('id') for v in views if v.get('active')]
- domain = [('type', '=', 'view'), ('res_id', 'in', views_ids), ('lang', '=', lang)]
+ domain = [
+ ('type', '=', 'model'),
+ ('name', '=', 'ir.ui.view,arch_db'),
+ ('res_id', 'in', views_ids),
+ ('lang', '=', lang),
+ ]
element_list = irt.search_read(self.env.cr, self.env.uid,
domain, field, context=self.env.context)
for element in element_list:
diff --git a/addons/website_version/views/website_version_views.xml b/addons/website_version/views/website_version_views.xml
index 5ad886dd71e..d20682d3c87 100644
--- a/addons/website_version/views/website_version_views.xml
+++ b/addons/website_version/views/website_version_views.xml
@@ -62,7 +62,7 @@
<field name="company_id"/>
<field name="user_id" context="{'active_test': False}"/>
</field>
- <separator string="Domain" position="replace">
+ <separator name="domain" position="replace">
<separator string="Statistics"/>
</separator>
diff --git a/openerp/addons/base/base.sql b/openerp/addons/base/base.sql
index 7084148885b..c0fd67ea56c 100644
--- a/openerp/addons/base/base.sql
+++ b/openerp/addons/base/base.sql
@@ -28,12 +28,16 @@ CREATE TABLE ir_model_fields (
model varchar NOT NULL,
model_id integer references ir_model on delete cascade,
name varchar NOT NULL,
- relation varchar,
- select_level varchar,
+ state varchar default 'base',
field_description varchar,
+ help varchar,
ttype varchar,
- state varchar default 'base',
+ relation varchar,
relation_field varchar,
+ select_level varchar,
+ readonly boolean default False,
+ required boolean default False,
+ selectable boolean default False,
translate boolean default False,
serialization_field_id integer references ir_model_fields on delete cascade,
primary key(id)
diff --git a/openerp/addons/base/ir/ir_actions.xml b/openerp/addons/base/ir/ir_actions.xml
index 214f85a9772..f13efa48e9d 100644
--- a/openerp/addons/base/ir/ir_actions.xml
+++ b/openerp/addons/base/ir/ir_actions.xml
@@ -77,7 +77,7 @@
</group>
</group>
<notebook>
- <page string="Security">
+ <page name="security" string="Security">
<field name="groups_id"/>
</page>
<page string="RML Configuration" attrs="{'invisible':[('report_type','not in',['pdf','sxw'])]}">
diff --git a/openerp/addons/base/ir/ir_attachment_view.xml b/openerp/addons/base/ir/ir_attachment_view.xml
index 27afbcafa9e..68b8c37234f 100644
--- a/openerp/addons/base/ir/ir_attachment_view.xml
+++ b/openerp/addons/base/ir/ir_attachment_view.xml
@@ -79,7 +79,7 @@
<field name="create_uid"/>
<field name="type"/>
<group expand="0" string="Group By">
- <filter string="Owner" icon="terp-personal" domain="[]" context="{'group_by':'create_uid'}"/>
+ <filter name="owner" string="Owner" icon="terp-personal" domain="[]" context="{'group_by':'create_uid'}"/>
<filter string="Type" icon="terp-stock_symbol-selection" domain="[]" context="{'group_by':'type'}" groups="base.group_no_one"/>
<filter string="Company" icon="terp-gtk-home" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
<filter string="Creation Month" icon="terp-go-month" domain="[]" context="{'group_by':'create_date'}"/>
diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py
index d035e30b09d..c3ba59f4f71 100644
--- a/openerp/addons/base/ir/ir_model.py
+++ b/openerp/addons/base/ir/ir_model.py
@@ -220,7 +220,8 @@ class ir_model_fields(osv.osv):
help="For one2many fields, the field on the target model that implement the opposite many2one relationship"),
'model_id': fields.many2one('ir.model', 'Model', required=True, select=True, ondelete='cascade',
help="The model this field belongs to"),
- 'field_description': fields.char('Field Label', required=True),
+ 'field_description': fields.char('Field Label', required=True, translate=True),
+ 'help': fields.text('Field Help', translate=True),
'ttype': fields.selection(_get_fields_type, 'Field Type', required=True),
'selection': fields.char('Selection Options', help="List of options for a selection field, "
"specified as a Python expression defining a list of (key, label) pairs. "
@@ -394,6 +395,7 @@ class ir_model_fields(osv.osv):
# static table of properties
model_props = [ # (our-name, fields.prop, set_fn)
('field_description', 'string', tools.ustr),
+ ('help', 'help', lambda s: s and tools.ustr(s) or None),
('required', 'required', bool),
('readonly', 'readonly', bool),
('domain', 'domain', eval),
@@ -403,6 +405,9 @@ class ir_model_fields(osv.osv):
('select_level', 'index', lambda x: bool(int(x))),
('selection', 'selection', eval),
]
+ if context.get('lang') not in (None, 'en_US'):
+ # do not patch registry with translations of field string and help
+ model_props = model_props[2:]
if vals and ids:
checked_selection = False # need only check it once, so defer
diff --git a/openerp/addons/base/ir/ir_model_view.xml b/openerp/addons/base/ir/ir_model_view.xml
index 1b6c46083f4..a62b8518eda 100644
--- a/openerp/addons/base/ir/ir_model_view.xml
+++ b/openerp/addons/base/ir/ir_model_view.xml
@@ -53,10 +53,15 @@
<field name="state"/>
</tree>
<form string="Fields Description">
- <group col="4">
- <field name="name"/>
- <field name="state"/>
- <field name="field_description"/>
+ <group>
+ <group>
+ <field name="name"/>
+ <field name="state"/>
+ </group>
+ <group>
+ <field name="field_description"/>
+ <field name="help"/>
+ </group>
</group>
<group string="Properties">
<group>
@@ -158,6 +163,7 @@
<group>
<field name="name" string="Field Name"/>
<field name="field_description"/>
+ <field name="help"/>
</group>
<group>
<field name="state"/>
diff --git a/openerp/addons/base/ir/ir_translation.py b/openerp/addons/base/ir/ir_translation.py
index 3eefef97902..37e69fafaf3 100644
--- a/openerp/addons/base/ir/ir_translation.py
+++ b/openerp/addons/base/ir/ir_translation.py
@@ -1,28 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from difflib import get_close_matches
import logging
-from openerp import tools
+from openerp import api, tools
import openerp.modules
from openerp.osv import fields, osv
from openerp.tools.translate import _
-from openerp.exceptions import UserError
+from openerp.exceptions import AccessError, UserError
_logger = logging.getLogger(__name__)
TRANSLATION_TYPE = [
- ('field', 'Field'),
+ ('field', 'Field'), # deprecated
('model', 'Object'),
- ('rml', 'RML (deprecated - use Report)'), # Pending deprecation - to be replaced by report!
('report', 'Report/Template'),
('selection', 'Selection'),
- ('view', 'View'),
- ('wizard_button', 'Wizard Button'),
- ('wizard_field', 'Wizard Field'),
- ('wizard_view', 'Wizard View'),
- ('xsl', 'XSL'),
- ('help', 'Help'),
+ ('view', 'View'), # deprecated
+ ('help', 'Help'), # deprecated
('code', 'Code'),
('constraint', 'Constraint'),
('sql_constraint', 'SQL Constraint')
@@ -66,10 +62,33 @@ class ir_translation_import_cursor(object):
# ugly hack for QWeb views - pending refactoring of translations in master
if params['imd_model'] == 'website':
params['imd_model'] = "ir.ui.view"
- # non-QWeb views do not need a matching res_id -> force to 0 to avoid dropping them
- elif params['res_id'] is None:
+ # non-QWeb views do not need a matching res_id in case they do not
+ # have an xml id -> force to 0 to avoid dropping them
+ elif params['res_id'] is None and not params['imd_name']:
+ # maybe we should insert this translation for all views of the
+ # given model?
params['res_id'] = 0
+ # backward compatibility: convert 'field', 'help', 'view' into 'model'
+ if params['type'] == 'field':
+ model, field = params['name'].split(',')
+ params['type'] = 'model'
+ params['name'] = 'ir.model.fields,field_description'
+ params['imd_model'] = 'ir.model.fields'
+ params['imd_name'] = 'field_%s_%s' % (model.replace('.', '_'), field)
+
+ elif params['type'] == 'help':
+ model, field = params['name'].split(',')
+ params['type'] = 'model'
+ params['name'] = 'ir.model.fields,help'
+ params['imd_model'] = 'ir.model.fields'
+ params['imd_name'] = 'field_%s_%s' % (model.replace('.', '_'), field)
+
+ elif params['type'] == 'view':
+ params['type'] = 'model'
+ params['name'] = 'ir.ui.view,arch_db'
+ params['imd_model'] = "ir.ui.view"
+
self._cr.execute("""INSERT INTO %s (name, lang, res_id, src, type, imd_model, module, imd_name, value, state, comments)
VALUES (%%(name)s, %%(lang)s, %%(res_id)s, %%(src)s, %%(type)s, %%(imd_model)s, %%(module)s,
%%(imd_name)s, %%(value)s, %%(state)s, %%(comments)s)""" % self._table_name,
@@ -162,16 +181,18 @@ class ir_translation(osv.osv):
context = {}
res = dict.fromkeys(ids, False)
for record in self.browse(cr, uid, ids, context=context):
- if record.type != 'model':
- res[record.id] = record.src
- else:
- model_name, field = record.name.split(',')
+ res[record.id] = record.src
+ if record.type == 'model':
+ model_name, field_name = record.name.split(',')
model = self.pool.get(model_name)
- if model is not None:
+ if model is None:
+ continue
+ field = model._fields[field_name]
+ if not callable(field.translate):
# Pass context without lang, need to read real stored field, not translation
context_no_lang = dict(context, lang=None)
- result = model.read(cr, uid, [record.res_id], [field], context=context_no_lang)
- res[record.id] = result[0][field] if result else False
+ result = model.read(cr, uid, [record.res_id], [field_name], context=context_no_lang)
+ res[record.id] = result[0][field_name] if result else False
return res
def _set_src(self, cr, uid, id, name, value, args, context=None):
@@ -181,15 +202,18 @@ class ir_translation(osv.osv):
if context is None:
context = {}
record = self.browse(cr, uid, id, context=context)
- if record.type == 'model':
- model_name, field = record.name.split(',')
+ if record.type == 'model':
+ model_name, field_name = record.name.split(',')
model = self.pool.get(model_name)
- #We need to take the context without the language information, because we want to write on the
- #value store in db and not on the one associate with current language.
- #Also not removing lang from context trigger an error when lang is different
- context_wo_lang = context.copy()
- context_wo_lang.pop('lang', None)
- model.write(cr, uid, [record.res_id], {field: value}, context=context_wo_lang)
+ field = model._fields[field_name]
+ if not callable(field.translate):
+ # Make a context without language information, because we want
+ # to write on the value stored in db and not on the one
+ # associated with the current language. Also not removing lang
+ # from context trigger an error when lang is different.
+ context_wo_lang = context.copy()
+ context_wo_lang.pop('lang', None)
+ model.write(cr, uid, [record.res_id], {field_name: value}, context=context_wo_lang)
return self.write(cr, uid, id, {'src': value}, context=context)
_columns = {
@@ -267,8 +291,7 @@ class ir_translation(osv.osv):
return translations
def _set_ids(self, cr, uid, name, tt, lang, ids, value, src=None):
- self._get_ids.clear_cache(self)
- self.__get_source.clear_cache(self)
+ self.clear_caches()
cr.execute('delete from ir_translation '
'where lang=%s '
@@ -354,28 +377,135 @@ class ir_translation(osv.osv):
res_id = tuple(res_id)
return self.__get_source(cr, uid, name, types, lang, source, res_id)
- def create(self, cr, uid, vals, context=None):
- if context is None:
- context = {}
- ids = super(ir_translation, self).create(cr, uid, vals, context=context)
- self.__get_source.clear_cache(self)
- self._get_ids.clear_cache(self)
- self.pool['ir.ui.view'].clear_cache()
- return ids
+ @api.model
+ def _get_terms_query(self, field, records):
+ """ Utility function that makes the query for field terms. """
+ query = """ SELECT id, res_id, src, value FROM ir_translation
+ WHERE lang=%s AND type=%s AND name=%s AND res_id IN %s """
+ name = "%s,%s" % (field.model_name, field.name)
+ params = (records.env.lang, 'model', name, tuple(records.ids))
+ return query, params
+
+ @api.model
+ def _get_terms_mapping(self, field, records):
+ """ Return a function mapping a (trans_id, source, value) to a value.
+ This method is called before querying the database for translations.
+ """
+ if self._context.get('edit_translations'):
+ # TODO: this part should be moved to module web_editor
+ self.insert_missing(field, records)
+ return lambda tid, src, value: '<span data-oe-translation-id="%s">%s</span>' % (tid, value)
+ else:
+ return lambda tid, src, value: value
- def write(self, cursor, user, ids, vals, context=None):
- if context is None:
- context = {}
- if isinstance(ids, (int, long)):
- ids = [ids]
- if vals.get('src') or ('value' in vals and not(vals.get('value'))):
- vals.update({'state':'to_translate'})
+ @api.model
+ def _get_terms_translations(self, field, records):
+ """ Return the terms and translations of a given `field` on `records`.
+
+ :return: {record_id: {source: value}}
+ """
+ result = {rid: {} for rid in records.ids}
+ if records:
+ map_trans = self._get_terms_mapping(field, records)
+ query, params = self._get_terms_query(field, records)
+ self._cr.execute(query, params)
+ for trans_id, res_id, src, value in self._cr.fetchall():
+ result[res_id][src] = map_trans(trans_id, src, value)
+ return result
+
+ @api.model
+ def _sync_terms_translations(self, field, records):
+ """ Synchronize the translations to the terms to translate, after the
+ English value of a field is modified. The algorithm tries to match
+ existing translations to the terms to translate, provided the distance
+ between modified strings is not too large. It allows to not retranslate
+ data where a typo has been fixed in the English value.
+ """
+ if not callable(getattr(field, 'translate', None)):
+ return
+
+ trans = self.env['ir.translation']
+ outdated = trans
+ discarded = trans
+
+ for record in records:
+ # get field value and terms to translate
+ value = record[field.name]
+ terms = set(field.get_trans_terms(value))
+ record_trans = trans.search([
+ ('type', '=', 'model'),
+ ('name', '=', "%s,%s" % (field.model_name, field.name)),
+ ('res_id', '=', record.id),
+ ])
+
+ if not terms:
+ # discard all translations for that field
+ discarded += record_trans
+ continue
+
+ # remap existing translations on terms when possible
+ for trans in record_trans:
+ if trans.src not in terms:
+ matches = get_close_matches(trans.src, terms, 1, 0.9)
+ if matches:
+ trans.write({'src': matches[0], 'state': trans.state})
+ else:
+ outdated += trans
+
+ # process outdated and discarded translations
+ outdated.write({'state': 'to_translate'})
+ discarded.unlink()
+
+ @api.model
+ @tools.ormcache_context('model_name', keys=('lang',))
+ def get_field_string(self, model_name):
+ """ Return the translation of fields strings in the context's language.
+ Note that the result contains the available translations only.
+
+ :param model_name: the name of a model
+ :return: the model's fields' strings as a dictionary `{field_name: field_string}`
+ """
+ fields = self.env['ir.model.fields'].search([('model', '=', model_name)])
+ return {field.name: field.field_description for field in fields}
+
+ @api.model
+ @tools.ormcache_context('model_name', keys=('lang',))
+ def get_field_help(self, model_name):
+ """ Return the translation of fields help in the context's language.
+ Note that the result contains the available translations only.
+
+ :param model_name: the name of a model
+ :return: the model's fields' help as a dictionary `{field_name: field_help}`
+ """
+ fields = self.env['ir.model.fields'].search([('model', '=', model_name)])
+ return {field.name: field.help for field in fields}
+
+ @api.model
+ def create(self, vals):
+ if vals.get('type') == 'model' and vals.get('value'):
+ # check and sanitize value
+ mname, fname = vals['name'].split(',')
+ field = self.env[mname]._fields[fname]
+ vals['value'] = field.check_trans_value(vals['value'])
+ record = super(ir_translation, self).create(vals)
+ self.clear_caches()
+ return record
+
+ @api.multi
+ def write(self, vals):
if vals.get('value'):
- vals.update({'state':'translated'})
- result = super(ir_translation, self).write(cursor, user, ids, vals, context=context)
- self.__get_source.clear_cache(self)
- self._get_ids.clear_cache(self)
- self.pool['ir.ui.view'].clear_cache()
+ vals.setdefault('state', 'translated')
+ ttype = vals.get('type') or self[:1].type
+ if ttype == 'model':
+ # check and sanitize value
+ name = vals.get('name') or self[:1].name
+ mname, fname = name.split(',')
+ field = self.env[mname]._fields[fname]
+ vals['value'] = field.check_trans_value(vals['value'])
+ elif vals.get('src') or not vals.get('value', True):
+ vals.setdefault('state', 'to_translate')
+ result = super(ir_translation, self).write(vals)
+ self.clear_caches()
return result
def unlink(self, cursor, user, ids, context=None):
@@ -384,43 +514,85 @@ class ir_translation(osv.osv):
if isinstance(ids, (int, long)):
ids = [ids]
- self.__get_source.clear_cache(self)
- self._get_ids.clear_cache(self)
+ self.clear_caches()
result = super(ir_translation, self).unlink(cursor, user, ids, context=context)
return result
- def translate_fields(self, cr, uid, model, id, field=None, context=None):
- trans_model = self.pool[model]
- domain = ['&', ('res_id', '=', id), ('name', '=like', model + ',%')]
- langs_ids = self.pool.get('res.lang').search(cr, uid, [('code', '!=', 'en_US')], context=context)
- if not langs_ids:
- raise UserError(_("Translation features are unavailable until you install an extra Odoo translation."))
- langs = [lg.code for lg in self.pool.get('res.lang').browse(cr, uid, langs_ids, context=context)]
+ @api.model
+ def insert_missing(self, field, records):
+ """ Insert missing translations for `field` on `records`. """
+ records = records.with_context(lang=None)
+ if callable(field.translate):
+ # insert missing translations for each term in src
+ query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value)
+ SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s
+ FROM res_lang l
+ WHERE l.code != 'en_US' AND NOT EXISTS (
+ SELECT 1 FROM ir_translation
+ WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s AND src=%(src)s
+ );
+ """
+ for record in records:
+ src = record[field.name] or None
+ for term in set(field.get_trans_terms(src)):
+ self._cr.execute(query, {
+ 'name': "%s,%s" % (field.model_name, field.name),
+ 'res_id': record.id,
+ 'src': term,
+ })
+ else:
+ # insert missing translations for src
+ query = """ INSERT INTO ir_translation (lang, type, name, res_id, src, value)
+ SELECT l.code, 'model', %(name)s, %(res_id)s, %(src)s, %(src)s
+ FROM res_lang l
+ WHERE l.code != 'en_US' AND NOT EXISTS (
+ SELECT 1 FROM ir_translation
+ WHERE lang=l.code AND type='model' AND name=%(name)s AND res_id=%(res_id)s
+ );
+ UPDATE ir_translation SET src=%(src)s
+ WHERE type='model' AND name=%(name)s AND res_id=%(res_id)s;
+ """
+ for record in records:
+ self._cr.execute(query, {
+ 'name': "%s,%s" % (field.model_name, field.name),
+ 'res_id': record.id,
+ 'src': record[field.name] or None,
+ })
+ self.clear_caches()
+
+ @api.model
+ def translate_fields(self, model, id, field=None):
+ """ Open a view for translating the field(s) of the record (model, id). """
main_lang = 'en_US'
- translatable_fields = []
- for k, f in trans_model._fields.items():
- if getattr(f, 'translate', False):
- if f.inherited:
- parent_id = trans_model.read(cr, uid, [id], [f.related[0]], context=context)[0][f.related[0]][0]
- translatable_fields.append({'name': k, 'id': parent_id, 'model': f.base_field.model_name})
- domain.insert(0, '|')
- domain.extend(['&', ('res_id', '=', parent_id), ('name', '=', "%s,%s" % (f.base_field.model_name, k))])
- else:
- translatable_fields.append({'name': k, 'id': id, 'model': model })
- if len(langs):
- fields = [f.get('name') for f in translatable_fields]
- record = trans_model.read(cr, uid, [id], fields, context={ 'lang': main_lang })[0]
- for lg in langs:
- for f in translatable_fields:
- # Check if record exists, else create it (at once)
- sql = """INSERT INTO ir_translation (lang, src, name, type, res_id, value)
- SELECT %s, %s, %s, 'model', %s, %s WHERE NOT EXISTS
- (SELECT 1 FROM ir_translation WHERE lang=%s AND name=%s AND res_id=%s AND type='model');
- UPDATE ir_translation SET src = %s WHERE lang=%s AND name=%s AND res_id=%s AND type='model';
- """
- src = record[f['name']] or None
- name = "%s,%s" % (f['model'], f['name'])
- cr.execute(sql, (lg, src , name, f['id'], src, lg, name, f['id'], src, lg, name, id))
+ if not self.env['res.lang'].search_count([('code', '!=', main_lang)]):
+ raise UserError(_("Translation features are unavailable until you install an extra translation."))
+
+ # determine domain for selecting translations
+ record = self.env[model].with_context(lang=main_lang).browse(id)
+ domain = ['&', ('res_id', '=', id), ('name', '=like', model + ',%')]
+
+ def make_domain(fld, rec):
+ name = "%s,%s" % (fld.model_name, fld.name)
+ return ['&', ('res_id', '=', rec.id), ('name', '=', name)]
+
+ # insert missing translations, and extend domain for related fields
+ for name, fld in record._fields.items():
+ if not getattr(fld, 'translate', False):
+ continue
+
+ rec = record
+ if fld.related:
+ try:
+ # traverse related fields up to their data source
+ while fld.related:
+ rec, fld = fld.traverse_related(rec)
+ if rec:
+ domain = ['|'] + domain + make_domain(fld, rec)
+ except AccessError:
+ continue
+
+ assert fld.translate and rec._name == fld.model_name
+ self.insert_missing(fld, rec)
action = {
'name': 'Translate',
@@ -431,10 +603,11 @@ class ir_translation(osv.osv):
'domain': domain,
}
if field:
- f = trans_model._fields[field]
- action['context'] = {
- 'search_default_name': "%s,%s" % (f.base_field.model_name, field)
- }
+ fld = record._fields[field]
+ if not fld.related:
+ action['context'] = {
+ 'search_default_name': "%s,%s" % (fld.model_name, fld.name),
+ }
return action
def _get_import_cursor(self, cr, uid, context=None):
diff --git a/openerp/addons/base/ir/ir_translation_view.xml b/openerp/addons/base/ir/ir_translation_view.xml
index 3fd80f90da9..6bf3d783040 100644
--- a/openerp/addons/base/ir/ir_translation_view.xml
+++ b/openerp/addons/base/ir/ir_translation_view.xml
@@ -8,7 +8,7 @@
<search string="Translations">
<filter icon="terp-gdu-smart-failing"
string="Untranslated"
- domain="['|',('value', '=', False),('value','=','')]"/>
+ domain="['|','|',('state','=','to_translate'),('value', '=', False),('value','=','')]"/>
<filter name="openerp-web"
string="Web-only translations"
domain="[('comments', 'like', 'openerp-web')]"/>
@@ -61,6 +61,7 @@
<field name="name"/>
<field name="lang"/>
<field name="type"/>
+ <field name="state"/>
</tree>
</field>
</record>
diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py
index 25101e8d6ff..58f3b12a412 100644
--- a/openerp/addons/base/ir/ir_ui_view.py
+++ b/openerp/addons/base/ir/ir_ui_view.py
@@ -3,11 +3,11 @@
import collections
import copy
import datetime
-import dateutil
from dateutil.relativedelta import relativedelta
import fnmatch
import logging
import os
+import re
import time
from operator import itemgetter
@@ -18,6 +18,7 @@ from lxml import etree
import openerp
from openerp import tools, api
+from openerp.exceptions import ValidationError
from openerp.http import request
from openerp.modules.module import get_resource_path, get_resource_from_path
from openerp.osv import fields, osv, orm
@@ -27,7 +28,7 @@ from openerp.tools.parse_version import parse_version
from openerp.tools.safe_eval import safe_eval as eval
from openerp.tools.view_validation import valid_view
from openerp.tools import misc
-from openerp.tools.translate import _
+from openerp.tools.translate import TRANSLATED_ATTRS, encode, xml_translate, _
_logger = logging.getLogger(__name__)
@@ -118,6 +119,22 @@ def get_view_arch_from_file(filename, xmlid):
xpath_utils = etree.FunctionNamespace(None)
xpath_utils['hasclass'] = _hasclass
+TRANSLATED_ATTRS_RE = re.compile(r"@(%s)\b" % "|".join(TRANSLATED_ATTRS))
+
+def valid_inheritance(arch):
+ """ Check whether view inheritance is based on translated attribute. """
+ for node in arch.xpath('//*[@position]'):
+ # inheritance may not use a translated attribute as selector
+ if node.tag == 'xpath':
+ match = TRANSLATED_ATTRS_RE.search(node.get('expr', ''))
+ if match:
+ raise ValidationError("View inheritance may not use attribute %r as a selector." % match.group(1))
+ else:
+ for attr in TRANSLATED_ATTRS:
+ if node.get(attr):
+ raise ValidationError("View inheritance may not use attribute %r as a selector." % attr)
+ return True
+
class view(osv.osv):
_name = 'ir.ui.view'
@@ -135,7 +152,6 @@ class view(osv.osv):
def _arch_get(self, cr, uid, ids, name, arg, context=None):
result = {}
-
for view in self.browse(cr, uid, ids, context=context):
arch_fs = None
if config['dev_mode'] and view.arch_fs and view.xml_id:
@@ -164,6 +180,16 @@ class view(osv.osv):
return True
+ @api.multi
+ def _arch_base_get(self, name, arg):
+ """ Return the field 'arch' without translation. """
+ return self.with_context(lang=None)._arch_get(name, arg)
+
+ @api.multi
+ def _arch_base_set(self, name, value, arg):
+ """ Assign the field 'arch' without translation. """
+ return self.with_context(lang=None)._arch_set(name, value, arg)
+
_columns = {
'name': fields.char('View Name', required=True),
'model': fields.char('Object', select=True),
@@ -182,7 +208,8 @@ class view(osv.osv):
('search','Search'),
('qweb', 'QWeb')], string='View Type'),
'arch': fields.function(_arch_get, fnct_inv=_arch_set, string='View Architecture', type="text", nodrop=True),
- 'arch_db': fields.text('Arch Blob', oldname='arch'),
+ 'arch_base': fields.function(_arch_base_get, fnct_inv=_arch_base_set, string='View Architecture', type="text"),
+ 'arch_db': fields.text('Arch Blob', translate=xml_translate, oldname='arch'),
'arch_fs': fields.char('Arch Filename'),
'inherit_id': fields.many2one('ir.ui.view', 'Inherited View', ondelete='restrict', select=True),
'inherit_children_ids': fields.one2many('ir.ui.view','inherit_id', 'Inherit Views'),
@@ -249,6 +276,9 @@ class view(osv.osv):
# Sanity checks: the view should not break anything upon rendering!
# Any exception raised below will cause a transaction rollback.
for view in self.browse(cr, uid, ids, context):
+ view_arch = etree.fromstring(encode(view.arch))
+ if not valid_inheritance(view_arch):
+ return False
view_def = self.read_combined(cr, uid, view.id, None, context=context)
view_arch_utf8 = view_def['arch']
if view.type != 'qweb':
@@ -278,7 +308,7 @@ class view(osv.osv):
" extend an other view"),
]
_constraints = [
- (_check_xml, 'Invalid view definition', ['arch']),
+ (_check_xml, 'Invalid view definition', ['arch', 'arch_base']),
]
def _auto_init(self, cr, context=None):
@@ -462,7 +492,6 @@ class view(osv.osv):
self.inherit_branding(node, view_id, root_id)
else:
node.set('data-oe-id', str(view_id))
- node.set('data-oe-source-id', str(root_id))
node.set('data-oe-xpath', xpath)
node.set('data-oe-model', 'ir.ui.view')
node.set('data-oe-field', 'arch')
@@ -735,46 +764,6 @@ class view(osv.osv):
# Get the attrs before they are (possibly) deleted by check_group below
orm.transfer_node_to_modifiers(node, modifiers, context, in_tree_view)
- # TODO remove attrs counterpart in modifiers when invisible is true ?
-
- # translate view
- if 'lang' in context:
- Translations = self.pool['ir.translation']
- if node.text and node.text.strip():
- term = node.text.strip()
- trans = Translations._get_source(cr, user, model, 'view', context['lang'], term)
- if trans:
- node.text = node.text.replace(term, trans)
- if node.tail and node.tail.strip():
- term = node.tail.strip()
- trans = Translations._get_source(cr, user, model, 'view', context['lang'], term)
- if trans:
- node.tail = node.tail.replace(term, trans)
-
- if node.get('string') and node.get('string').strip() and not result:
- term = node.get('string').strip()
- trans = Translations._get_source(cr, user, model, 'view', context['lang'], term)
- if trans == term:
- if 'base_model_name' in context:
- # If translation is same as source, perhaps we'd have more luck with the alternative model name
- # (in case we are in a mixed situation, such as an inherited view where parent_view.model != model
- trans = Translations._get_source(cr, user, context['base_model_name'], 'view', context['lang'], term)
- else:
- inherit_model = self.browse(cr, user, view_id, context=context).inherit_id.model or model
- if inherit_model != model:
- # parent view has a different model, if the terms belongs to the parent view, the translation
- # should be checked on the parent model as well
- trans = Translations._get_source(cr, user, inherit_model, 'view', context['lang'], term)
- if trans:
- node.set('string', trans)
-
- for attr_name in ('confirm', 'sum', 'avg', 'help', 'placeholder'):
- attr_value = node.get(attr_name)
- if attr_value and attr_value.strip():
- trans = Translations._get_source(cr, user, model, 'view', context['lang'], attr_value.strip())
- if trans:
- node.set(attr_name, trans)
-
for f in node:
if children or (node.tag == 'field' and f.tag in ('filter','separator')):
fields.update(self.postprocess(cr, user, model, f, view_id, in_tree_view, model_fields, context))
@@ -1104,6 +1093,11 @@ class view(osv.osv):
# Misc
#------------------------------------------------------
+ @api.multi
+ def open_translations(self):
+ """ Open a view for editing the translations of field 'arch_db'. """
+ return self.env['ir.translation'].translate_fields('ir.ui.view', self.id, 'arch_db')
+
def graph_get(self, cr, uid, id, model, node_obj, conn_obj, src_node, des_node, label, scale, context=None):
nodes=[]
nodes_name=[]
diff --git a/openerp/addons/base/ir/ir_ui_view_view.xml b/openerp/addons/base/ir/ir_ui_view_view.xml
index 171d04d5651..02c0f69e83e 100644
--- a/openerp/addons/base/ir/ir_ui_view_view.xml
+++ b/openerp/addons/base/ir/ir_ui_view_view.xml
@@ -6,7 +6,7 @@
<field name="model">ir.ui.view</field>
<field name="arch" type="xml">
<form string="Views">
- <sheet>
+ <sheet>
<group>
<group>
<field name="name"/>
@@ -25,7 +25,9 @@
</group>
<notebook>
<page string="Architecture">
- <field name="arch"/>
+ <button type="object" name="open_translations"
+ string="Edit Translations" class="oe_link oe_right"/>
+ <field name="arch_base"/>
</page>
<page string="Groups">
<field name="groups_id"/>
diff --git a/openerp/addons/base/res/res_company_view.xml b/openerp/addons/base/res/res_company_view.xml
index 8f653359b74..4b0612e0900 100644
--- a/openerp/addons/base/res/res_company_view.xml
+++ b/openerp/addons/base/res/res_company_view.xml
@@ -77,14 +77,14 @@
</field>
</group>
</page>
- <page string="Configuration">
+ <page name="configuration" string="Configuration">
<group>
<group name="account_grp" string="Accounting">
<field name="currency_id"/>
</group>
</group>
</page>
- <page string="Report Configuration">
+ <page name="report" string="Report Configuration">
<group string="Configuration">
<field name="custom_footer" on_change="onchange_footer(custom_footer, phone, fax, email, website, vat, company_registry, bank_ids)"/>
<field name="rml_footer" attrs="{'invisible': [('custom_footer','=',False)]}"/>
diff --git a/openerp/addons/base/res/res_config.xml b/openerp/addons/base/res/res_config.xml
index 67e476d8a8b..cd4cfc6a6ef 100644
--- a/openerp/addons/base/res/res_config.xml
+++ b/openerp/addons/base/res/res_config.xml
@@ -7,7 +7,7 @@
<field name="model">res.config</field>
<field name="arch" type="xml">
<form string="Configuration">
- <group string="res_config_contents"/>
+ <group name="res_config_contents"/>
<footer>
<button name="action_next" type="object" string="Apply" class="oe_highlight"/>
or
diff --git a/openerp/addons/base/res/res_users_view.xml b/openerp/addons/base/res/res_users_view.xml
index 062dcc6f31e..71ea27df5e4 100644
--- a/openerp/addons/base/res/res_users_view.xml
+++ b/openerp/addons/base/res/res_users_view.xml
@@ -185,7 +185,7 @@
</group>
</div>
<notebook colspan="4">
- <page string="Access Rights">
+ <page name="access_rights" string="Access Rights">
<group string="Allowed Companies" groups="base.group_multi_company">
<field name="company_ids" nolabel="1" widget="many2many_tags"/>
</group>
diff --git a/openerp/addons/base/tests/test_translate.py b/openerp/addons/base/tests/test_translate.py
index 4607cd64b9e..35518bbfb11 100644
--- a/openerp/addons/base/tests/test_translate.py
+++ b/openerp/addons/base/tests/test_translate.py
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import unittest
-from openerp.tools.translate import quote, unquote
+from openerp.tools.translate import quote, unquote, xml_translate
class TranslationToolsTestCase(unittest.TestCase):
@@ -27,3 +27,118 @@ class TranslationToolsTestCase(unittest.TestCase):
self.assertRaises(AssertionError, quote, """test \nall kinds\n\no\r
\\\\nope\n\n"
""")
+
+ def test_translate_xml_base(self):
+ """ Test xml_translate() without formatting elements. """
+ terms = []
+ source = """<form string="Form stuff">
+ <h1>Blah blah blah</h1>
+ Put some more text here
+ <field name="foo"/>
+ </form>"""
+ result = xml_translate(terms.append, source)
+ self.assertEquals(result, source)
+ self.assertItemsEqual(terms,
+ ['Form stuff', 'Blah blah blah', 'Put some more text here'])
+
+ def test_translate_xml_inline1(self):
+ """ Test xml_translate() with formatting elements. """
+ terms = []
+ source = """<form string="Form stuff">
+ <h1>Blah <i>blah</i> blah</h1>
+ Put some <b>more text</b> here
+ <field name="foo"/>
+ </form>"""
+ result = xml_translate(terms.append, source)
+ self.assertEquals(result, source)
+ self.assertItemsEqual(terms,
+ ['Form stuff', 'Blah <i>blah</i> blah', 'Put some <b>more text</b> here'])
+
+ def test_translate_xml_inline2(self):
+ """ Test xml_translate() with formatting elements embedding other elements. """
+ terms = []
+ source = """<form string="Form stuff">
+ <b><h1>Blah <i>blah</i> blah</h1></b>
+ Put <em>some <b>more text</b></em> here
+ <field name="foo"/>
+ </form>"""
+ result = xml_translate(terms.append, source)
+ self.assertEquals(result, source)
+ self.assertItemsEqual(terms,
+ ['Form stuff', 'Blah <i>blah</i> blah', 'Put <em>some <b>more text</b></em> here'])
+
+ def test_translate_xml_inline3(self):
+ """ Test xml_translate() with formatting elements without actual text. """
+ terms = []
+ source = """<form string="Form stuff">
+ <div>
+ <span class="before"/>
+ <h1>Blah blah blah</h1>
+ <span class="after">
+ <i class="hack"/>
+ </span>
+ </div>
+ </form>"""
+ result = xml_translate(terms.append, source)
+ self.assertEquals(result, source)
+ self.assertItemsEqual(terms,
+ ['Form stuff', 'Blah blah blah'])
+
+ def test_translate_xml_t(self):
+ """ Test xml_translate() with t-* attributes. """
+ terms = []
+ source = """<t t-name="stuff">
+ stuff before
+ <span t-field="o.name"/>
+ stuff after
+ </t>"""
+ result = xml_translate(terms.append, source)
+ self.assertEquals(result, source)
+ self.assertItemsEqual(terms,
+ ['stuff before', 'stuff after'])
+
+ def test_translate_xml_off(self):
+ """ Test xml_translate() with attribute translate="off". """
+ terms = []
+ source = """<div>
+ stuff before
+ <div translation="off">Do not translate this</div>
+ stuff after
+ </div>"""
+ result = xml_translate(terms.append, source)
+ self.assertEquals(result, source)
+ self.assertItemsEqual(terms,
+ ['stuff before', 'stuff after'])
+
+ def test_translate_xml_attribute(self):
+ """ Test xml_translate() with <attribute> elements. """
+ terms = []
+ source = """<field name="foo" position="attributes">
+ <attribute name="string">Translate this</attribute>
+ <attribute name="option">Do not translate this</attribute>
+ </field>"""
+ result = xml_translate(terms.append, source)
+ self.assertEquals(result, source)
+ self.assertItemsEqual(terms,
+ ['Translate this'])
+
+ def test_translate_xml_a(self):
+ """ Test xml_translate() with <a> elements. """
+ terms = []
+ source = """<t t-name="stuff">
+ <ul class="nav navbar-nav">
+ <li>
+ <a class="oe_menu_leaf" href="/web#menu_id=42&amp;action=54">
+ <span class="oe_menu_text">Blah</span>
+ </a>
+ </li>
+ <li class="dropdown" id="menu_more_container" style="display: none;">
+ <a class="dropdown-toggle" data-toggle="dropdown" href="#">More <b class="caret"/></a>
+ <ul class="dropdown-menu" id="menu_more"/>
+ </li>
+ </ul>
+ </t>"""
+ result = xml_translate(terms.append, source)
+ self.assertEquals(result, source)
+ self.assertItemsEqual(terms,
+ ['<span class="oe_menu_text">Blah</span>', 'More <b class="caret"/>'])
diff --git a/openerp/addons/base/tests/test_views.py b/openerp/addons/base/tests/test_views.py
index 7b41d2968c4..fb8867b1ac4 100644
--- a/openerp/addons/base/tests/test_views.py
+++ b/openerp/addons/base/tests/test_views.py
@@ -544,7 +544,6 @@ class TestTemplating(ViewCase):
'data-oe-id': str(id2),
'data-oe-field': 'arch',
'data-oe-xpath': '/xpath/item/content[1]',
- 'data-oe-source-id': str(id)
}), {
'order': '2',
}),
@@ -553,7 +552,7 @@ class TestTemplating(ViewCase):
'data-oe-model': 'ir.ui.view',
'data-oe-id': str(id),
'data-oe-field': 'arch',
- 'data-oe-xpath': '/root[1]/item[1]'
+ 'data-oe-xpath': '/root[1]/item[1]',
})
)
)
@@ -707,7 +706,7 @@ class test_views(ViewCase):
'model': 'ir.ui.view',
'arch': """
<form string="Base title" version="7.0">
- <separator string="separator" colspan="4"/>
+ <separator name="separator" string="Separator" colspan="4"/>
<footer>
<button name="action_next" type="object" string="Next button"/>
or
@@ -730,7 +729,7 @@ class test_views(ViewCase):
<button name="action_next" type="object" string="New button"/>
</footer>
</footer>
- <separator string="separator" position="replace">
+ <separator name="separator" position="replace">
<p>Replacement data</p>
</separator>
</data>
@@ -775,7 +774,7 @@ class test_views(ViewCase):
'model': 'ir.ui.view.custom',
'arch': """
<form string="Base title" version="7.0">
- <separator string="separator" colspan="4"/>
+ <separator name="separator" string="Separator" colspan="4"/>
<footer>
<button name="action_next" type="object" string="Next button"/>
or
@@ -798,7 +797,7 @@ class test_views(ViewCase):
<button name="action_next" type="object" string="New button"/>
</footer>
</footer>
- <separator string="separator" position="replace">
+ <separator name="separator" position="replace">
<p>Replacement data</p>
</separator>
</data>
diff --git a/openerp/fields.py b/openerp/fields.py
index 56d177d9c9e..fc66e49cd24 100644
--- a/openerp/fields.py
+++ b/openerp/fields.py
@@ -514,28 +514,28 @@ class Field(object):
if self.inherited and field.required:
self.required = True
+ def traverse_related(self, record):
+ """ Traverse the fields of the related field `self` except for the last
+ one, and return it as a pair `(last_record, last_field)`. """
+ for name in self.related[:-1]:
+ record = record[name][:1]
+ return record, self.related_field
+
def _compute_related(self, records):
""" Compute the related field `self` on `records`. """
# when related_sudo, bypass access rights checks when reading values
others = records.sudo() if self.related_sudo else records
for record, other in zip(records, others):
- if not record.id:
- # draft record, do not switch to another environment
- other = record
- # traverse the intermediate fields; follow the first record at each step
- for name in self.related[:-1]:
- other = other[name][:1]
- record[self.name] = other[self.related[-1]]
+ # do not switch to another environment if record is a draft one
+ other, field = self.traverse_related(other if record.id else record)
+ record[self.name] = other[field.name]
def _inverse_related(self, records):
""" Inverse the related field `self` on `records`. """
for record in records:
- other = record
- # traverse the intermediate fields, and keep at most one record
- for name in self.related[:-1]:
- other = other[name][:1]
+ other, field = self.traverse_related(record)
if other:
- other[self.related[-1]] = record[self.name]
+ other[field.name] = record[self.name]
def _search_related(self, records, operator, value):
""" Determine the domain to search on field `self`. """
@@ -651,17 +651,16 @@ class Field(object):
def _description_string(self, env):
if self.string and env.lang:
- field = self.base_field
- name = "%s,%s" % (field.model_name, field.name)
- trans = env['ir.translation']._get_source(name, 'field', env.lang)
- return trans or self.string
+ model_name = self.base_field.model_name
+ field_string = env['ir.translation'].get_field_string(model_name)
+ return field_string.get(self.name) or self.string
return self.string
def _description_help(self, env):
if self.help and env.lang:
- name = "%s,%s" % (self.model_name, self.name)
- trans = env['ir.translation']._get_source(name, 'help', env.lang)
- return trans or self.help
+ model_name = self.base_field.model_name
+ field_help = env['ir.translation'].get_field_help(model_name)
+ return field_help.get(self.name) or self.help
return self.help
############################################################################
@@ -1111,15 +1110,59 @@ class _String(Field):
_column_translate = property(attrgetter('translate'))
_related_translate = property(attrgetter('translate'))
- _description_translate = property(attrgetter('translate'))
+
+ def _description_translate(self, env):
+ return bool(self.translate)
+
+ def get_trans_terms(self, value):
+ """ Return the sequence of terms to translate found in `value`. """
+ if not callable(self.translate):
+ return [value] if value else []
+ terms = []
+ self.translate(terms.append, value)
+ return terms
+
+ def get_trans_func(self, records):
+ """ Return a translation function `translate` for `self` on the given
+ records; the function call `translate(record_id, value)` translates the
+ field value to the language given by the environment of `records`.
+ """
+ if callable(self.translate):
+ rec_src_trans = records.env['ir.translation']._get_terms_translations(self, records)
+
+ def translate(record_id, value):
+ src_trans = rec_src_trans[record_id]
+ return self.translate(src_trans.get, value)
+
+ else:
+ rec_trans = records.env['ir.translation']._get_ids(
+ '%s,%s' % (self.model_name, self.name), 'model', records.env.lang, records.ids)
+
+ def translate(record_id, value):
+ return rec_trans.get(record_id) or value
+
+ return translate
+
+ def check_trans_value(self, value):
+ """ Check and possibly sanitize the translated term `value`. """
+ if callable(self.translate):
+ # do a "no-translation" to sanitize the value
+ callback = lambda term: None
+ return self.translate(callback, value)
+ else:
+ return value
class Char(_String):
""" Basic string field, can be length-limited, usually displayed as a
- single-line string in clients
+ single-line string in clients.
:param int size: the maximum size of values stored for that field
- :param bool translate: whether the values of this field can be translated
+
+ :param translate: enable the translation of the field's values; use
+ `translate=True` to translate field values as a whole; `translate` may
+ also be a callable such that `translate(callback, value)` translates
+ `value` by using `callback(term)` to retrieve the translation of terms.
"""
type = 'char'
_slots = {
@@ -1144,7 +1187,10 @@ class Text(_String):
""" Very similar to :class:`~.Char` but used for longer contents, does not
have a size and usually displayed as a multiline text box.
- :param translate: whether the value of this field can be translated
+ :param translate: enable the translation of the field's values; use
+ `translate=True` to translate field values as a whole; `translate` may
+ also be a callable such that `translate(callback, value)` translates
+ `value` by using `callback(term)` to retrieve the translation of terms.
"""
type = 'text'
@@ -1702,11 +1748,8 @@ class _RelationalMulti(_Relational):
def _compute_related(self, records):
""" Compute the related field `self` on `records`. """
for record in records:
- value = record
- # traverse the intermediate fields, and keep at most one record
- for name in self.related[:-1]:
- value = value[name][:1]
- record[self.name] = value[self.related[-1]]
+ other, field = self.traverse_related(record)
+ record[self.name] = other[field.name]
class One2many(_RelationalMulti):
diff --git a/openerp/models.py b/openerp/models.py
index c601d309512..6955ff46207 100644
--- a/openerp/models.py
+++ b/openerp/models.py
@@ -380,14 +380,15 @@ class BaseModel(object):
'model': self._name,
'name': k,
'field_description': f.string,
+ 'help': f.help or False,
'ttype': f.type,
'relation': f.comodel_name or '',
'select_level': tools.ustr(int(f.index)),
- 'readonly': (f.readonly and 1) or 0,
- 'required': (f.required and 1) or 0,
- 'selectable': (f.search or f.store and 1) or 0,
- 'translate': (f.translate if hasattr(f,'translate') else False and 1) or 0,
- 'relation_field': f.inverse_name if hasattr(f, 'inverse_name') else '',
+ 'readonly': bool(f.readonly),
+ 'required': bool(f.required),
+ 'selectable': bool(f.search or f.store),
+ 'translate': bool(getattr(f, 'translate', False)),
+ 'relation_field': getattr(f, 'inverse_name', ''),
'serialization_field_id': None,
}
if getattr(f, 'serialization_field', None):
@@ -409,16 +410,11 @@ class BaseModel(object):
cr.execute('select nextval(%s)', ('ir_model_fields_id_seq',))
id = cr.fetchone()[0]
vals['id'] = id
- cr.execute("""INSERT INTO ir_model_fields (
- id, model_id, model, name, field_description, ttype,
- relation,state,select_level,relation_field, translate, serialization_field_id
- ) VALUES (
- %s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s
- )""", (
- id, vals['model_id'], vals['model'], vals['name'], vals['field_description'], vals['ttype'],
- vals['relation'], 'base',
- vals['select_level'], vals['relation_field'], bool(vals['translate']), vals['serialization_field_id']
- ))
+ query = "INSERT INTO ir_model_fields (%s) VALUES (%s)" % (
+ ",".join(vals),
+ ",".join("%%(%s)s" % name for name in vals),
+ )
+ cr.execute(query, vals)
if 'module' in context:
name1 = 'field_' + self._table + '_' + k
cr.execute("select name from ir_model_data where name=%s", (name1,))
@@ -430,16 +426,11 @@ class BaseModel(object):
else:
for key, val in vals.items():
if cols[k][key] != vals[key]:
- cr.execute('update ir_model_fields set field_description=%s where model=%s and name=%s', (vals['field_description'], vals['model'], vals['name']))
- cr.execute("""UPDATE ir_model_fields SET
- model_id=%s, field_description=%s, ttype=%s, relation=%s,
- select_level=%s, readonly=%s ,required=%s, selectable=%s, relation_field=%s, translate=%s, serialization_field_id=%s
- WHERE
- model=%s AND name=%s""", (
- vals['model_id'], vals['field_description'], vals['ttype'],
- vals['relation'],
- vals['select_level'], bool(vals['readonly']), bool(vals['required']), bool(vals['selectable']), vals['relation_field'], bool(vals['translate']), vals['serialization_field_id'], vals['model'], vals['name']
- ))
+ names = set(vals) - set(['model', 'name'])
+ query = "UPDATE ir_model_fields SET %s WHERE model=%%(model)s and name=%%(name)s" % (
+ ",".join("%s=%%(%s)s" % (name, name) for name in names),
+ )
+ cr.execute(query, vals)
break
self.invalidate_cache(cr, SUPERUSER_ID)
@@ -1166,11 +1157,11 @@ class BaseModel(object):
Converter = self.pool['ir.fields.converter']
Translation = self.pool['ir.translation']
fields = dict(self._fields)
- field_names = dict(
- (f, (Translation._get_source(cr, uid, self._name + ',' + f, 'field',
- context.get('lang'))
- or field.string))
- for f, field in fields.iteritems())
+ field_names = {name: field.string for name, field in fields.iteritems()}
+ if context.get('lang'):
+ field_names.update(
+ Translation.get_field_string(cr, uid, self._name, context=context)
+ )
convert = Converter.for_model(cr, uid, self, context=context)
@@ -1714,46 +1705,6 @@ class BaseModel(object):
res = self.name_get(cr, access_rights_uid, ids, context)
return res
- def read_string(self, cr, uid, id, langs, fields=None, context=None):
- res = {}
- res2 = {}
- self.pool.get('ir.translation').check_access_rights(cr, uid, 'read')
- if not fields:
- fields = self._columns.keys() + self._inherit_fields.keys()
- #FIXME: collect all calls to _get_source into one SQL call.
- for lang in langs:
- res[lang] = {'code': lang}
- for f in fields:
- if f in self._columns:
- res_trans = self.pool.get('ir.translation')._get_source(cr, uid, self._name+','+f, 'field', lang)
- if res_trans:
- res[lang][f] = res_trans
- else:
- res[lang][f] = self._columns[f].string
- for table in self._inherits:
- cols = intersect(self._inherit_fields.keys(), fields)
- res2 = self.pool[table].read_string(cr, uid, id, langs, cols, context)
- for lang in res2:
- if lang in res:
- res[lang]['code'] = lang
- for f in res2[lang]:
- res[lang][f] = res2[lang][f]
- return res
-
- def write_string(self, cr, uid, id, langs, vals, context=None):
- self.pool.get('ir.translation').check_access_rights(cr, uid, 'write')
- #FIXME: try to only call the translation in one SQL
- for lang in langs:
- for field in vals:
- if field in self._columns:
- src = self._columns[field].string
- self.pool.get('ir.translation')._set_ids(cr, uid, self._name+','+field, 'field', lang, [0], vals[field], src)
- for table in self._inherits:
- cols = intersect(self._inherit_fields.keys(), vals)
- if cols:
- self.pool[table].write_string(cr, uid, id, langs, vals, context)
- return True
-
def _add_missing_default_values(self, cr, uid, values, context=None):
# avoid overriding inherited values when parent is set
avoid_tables = []
@@ -2184,7 +2135,8 @@ class BaseModel(object):
)
model, alias = parent_model, parent_alias
# handle the case where the field is translated
- if model._columns[field].translate:
+ translate = model._columns[field].translate
+ if translate and not callable(translate):
return model._generate_translated_field(alias, field, query)
else:
return '"%s"."%s"' % (alias, field)
@@ -3271,6 +3223,7 @@ class BaseModel(object):
field
for field in fields
if field.base_field.column._classic_write
+ if not (field.inherited and callable(field.base_field.column.translate))
]
# the query may involve several tables: we need fully-qualified names
@@ -3303,8 +3256,18 @@ class BaseModel(object):
result.extend(cr.dictfetchall())
ids = [vals['id'] for vals in result]
+ fetched = self.browse(ids)
if ids:
+ # translate the fields if necessary
+ if context.get('lang'):
+ for field in fields_pre:
+ if not field.inherited and callable(field.column.translate):
+ f = field.name
+ translate = field.get_trans_func(fetched)
+ for vals in result:
+ vals[f] = translate(vals['id'], vals[f])
+
# apply the symbol_get functions of the fields we just read
for field in fields_pre:
symbol_get = field.base_field.column._symbol_get
@@ -3361,7 +3324,6 @@ class BaseModel(object):
record._cache.update(record._convert_to_cache(vals, validate=False))
# store failed values in cache for the records that could not be read
- fetched = self.browse(ids)
missing = self - fetched
if missing:
extras = fetched - self
@@ -3844,7 +3806,7 @@ class BaseModel(object):
upd_todo = []
updend = []
direct = []
- totranslate = context.get('lang', False) and (context['lang'] != 'en_US')
+ totranslate = context.get('lang') and context['lang'] != 'en_US'
for field in vals:
ffield = self._fields.get(field)
if ffield and ffield.deprecated:
@@ -3854,7 +3816,12 @@ class BaseModel(object):
if hasattr(column, 'selection') and vals[field]:
self._check_selection_field_value(cr, user, field, vals[field], context=context)
if column._classic_write and not hasattr(column, '_fnct_inv'):
- if (not totranslate) or not column.translate:
+ if totranslate and column.translate:
+ # vals[field] is a translation: do not update the table
+ if callable(column.translate):
+ flabel = ffield.get_description(recs.env)['string']
+ raise UserError(_("You cannot update the translated value of field '%s'") % flabel)
+ else:
updates.append((field, '%s', column._symbol_set[1](vals[field])))
direct.append(field)
else:
@@ -3879,17 +3846,25 @@ class BaseModel(object):
if cr.rowcount != len(sub_ids):
raise MissingError(_('One of the records you are trying to modify has already been deleted (Document type: %s).') % self._description)
- if totranslate:
- # TODO: optimize
- for f in direct:
- if self._columns[f].translate:
- src_trans = self.pool[self._name].read(cr, user, ids, [f])[0][f]
- if not src_trans:
- src_trans = vals[f]
- # Inserting value to DB
- context_wo_lang = dict(context, lang=None)
- self.write(cr, user, ids, {f: vals[f]}, context=context_wo_lang)
- self.pool.get('ir.translation')._set_ids(cr, user, self._name+','+f, 'model', context['lang'], ids, vals[f], src_trans)
+ # TODO: optimize
+ for f in direct:
+ column = self._columns[f]
+ if callable(column.translate):
+ # The English value of a field has been modified,
+ # synchronize translated terms when possible.
+ assert not totranslate
+ self.pool['ir.translation']._sync_terms_translations(
+ cr, user, self._fields[f], recs, context=context)
+
+ elif column.translate and totranslate:
+ # The translated value of a field has been modified.
+ src_trans = self.pool[self._name].read(cr, user, ids, [f])[0][f]
+ if not src_trans:
+ # Insert value to DB
+ src_trans = vals[f]
+ context_wo_lang = dict(context, lang=None)
+ self.write(cr, user, ids, {f: vals[f]}, context=context_wo_lang)
+ self.pool['ir.translation']._set_ids(cr, user, self._name+','+f, 'model', context['lang'], ids, vals[f], src_trans)
# invalidate and mark new-style fields to recompute; do this before
# setting other fields, because it can require the value of computed
@@ -4604,7 +4579,7 @@ class BaseModel(object):
elif order_field in self._columns:
order_column = self._columns[order_field]
if order_column._classic_read:
- if order_column.translate:
+ if order_column.translate and not callable(order_column.translate):
inner_clause = self._generate_translated_field(self._table, order_field, query)
else:
inner_clause = '"%s"."%s"' % (self._table, order_field)
diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py
index 171ff9e4047..bbc373e7f0e 100644
--- a/openerp/osv/expression.py
+++ b/openerp/osv/expression.py
@@ -1026,7 +1026,7 @@ class expression(object):
right += ' 00:00:00'
push(create_substitution_leaf(leaf, (left, operator, right), model))
- elif column.translate and right:
+ elif column.translate and not callable(column.translate) and right:
need_wildcard = operator in ('like', 'ilike', 'not like', 'not ilike')
sql_operator = {'=like': 'like', '=ilike': 'ilike'}.get(operator, operator)
if need_wildcard:
diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py
index 43d51705349..697a0301a06 100644
--- a/openerp/tools/mail.py
+++ b/openerp/tools/mail.py
@@ -31,7 +31,7 @@ tags_to_remove = ['html', 'body']
allowed_tags = clean.defs.tags | frozenset('article section header footer hgroup nav aside figure main'.split() + [etree.Comment])
safe_attrs = clean.defs.safe_attrs | frozenset(
['style',
- 'data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-type', 'data-oe-expression', 'data-oe-translate', 'data-oe-nodeid',
+ 'data-oe-model', 'data-oe-id', 'data-oe-field', 'data-oe-type', 'data-oe-expression', 'data-oe-translation-id', 'data-oe-nodeid',
'data-publish', 'data-id', 'data-res_id', 'data-member_id', 'data-view-id'
])
diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py
index 2f8d779215a..67885926e61 100644
--- a/openerp/tools/translate.py
+++ b/openerp/tools/translate.py
@@ -18,6 +18,7 @@ from collections import defaultdict
from datetime import datetime
from lxml import etree
from os.path import join
+from xml.sax.saxutils import escape
import config
import misc
@@ -128,6 +129,154 @@ class UNIX_LINE_TERMINATOR(csv.excel):
csv.register_dialect("UNIX", UNIX_LINE_TERMINATOR)
+
+#
+# Helper functions for translating fields
+#
+def encode(s):
+ if isinstance(s, unicode):
+ return s.encode('utf8')
+ return s
+
+# which elements are translated inline
+TRANSLATED_ELEMENTS = {
+ 'abbr', 'audio', 'b', 'bdi', 'bdo', 'br', 'canvas', 'cite', 'code',
+ 'data', 'datalist', 'del', 'dfn', 'em', 'embed', 'font', 'i', 'iframe',
+ 'ins', 'kbd', 'keygen', 'map', 'mark', 'math', 'meter', 'object', 'output',
+ 'progress', 'q', 'ruby', 's', 'samp', 'select', 'small', 'span', 'strong',
+ 'sub', 'sup', 'svg', 'template', 'textarea', 'time', 'u', 'var', 'video',
+ 'wbr', 'text',
+}
+
+# which attributes must be translated
+TRANSLATED_ATTRS = {
+ 'string', 'help', 'sum', 'avg', 'confirm', 'placeholder', 'alt', 'title',
+}
+
+def serialize(tag, attrib, content):
+ """ Return a serialized element with the given `tag`, attributes
+ `attrib`, and already-serialized `content`.
+ """
+ elem = etree.tostring(etree.Element(tag, attrib))
+ assert elem.endswith("/>")
+ return "%s>%s</%s>" % (elem[:-2], content, tag) if content else elem
+
+class XMLTranslator(object):
+ """ A sequence of serialized xml items, with some of them to translate
+ (todo) and others already translated (done). The purpose of this object
+ is to simplify the handling of phrasing elements (like <b>) that must be
+ translated together with their surrounding text.
+
+ For instance, the content of the "div" element below will be translated
+ as a whole (without surrounding spaces):
+
+ <div>
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit,
+ <b>sed</b> do eiusmod tempor incididunt ut labore et dolore
+ magna aliqua. <span class="more">Ut enim ad minim veniam,
+ <em>quis nostrud exercitation</em> ullamco laboris nisi ut
+ aliquip ex ea commodo consequat.</span>
+ </div>
+
+ """
+ def __init__(self, callback):
+ self.callback = callback # callback function to translate terms
+ self._done = [] # translated strings
+ self._todo = [] # todo strings that come after _done
+ self.needs_trans = False # whether todo needs translation
+
+ def todo(self, text, needs_trans=True):
+ self._todo.append(text)
+ if needs_trans and text.strip():
+ self.needs_trans = True
+
+ def all_todo(self):
+ return not self._done
+
+ def get_todo(self):
+ return "".join(self._todo)
+
+ def flush(self):
+ if self._todo:
+ todo = "".join(self._todo)
+ done = self.process_text(todo) if self.needs_trans else todo
+ self._done.append(done)
+ del self._todo[:]
+ self.needs_trans = False
+
+ def done(self, text):
+ self.flush()
+ self._done.append(text)
+
+ def get_done(self):
+ """ Complete the translations and return the result. """
+ self.flush()
+ return "".join(self._done)
+
+ def process_text(self, text):
+ """ Translate text.strip(), but keep the surrounding spaces from text. """
+ term = text.strip()
+ trans = term and self.callback(term)
+ return text.replace(term, trans) if trans else text
+
+ def process(self, node):
+ """ Process the given xml `node`: collect `todo` and `done` items. """
+ if (
+ isinstance(node, SKIPPED_ELEMENT_TYPES) or
+ node.tag in SKIPPED_ELEMENTS or
+ node.get("translation", "").strip() == "off" or
+ node.tag == "attribute" and node.get("name") not in TRANSLATED_ATTRS
+ ):
+ # do not translate the contents of the node
+ tail, node.tail = node.tail, None
+ self.done(etree.tostring(node))
+ self.todo(escape(tail or ""))
+ return
+
+ # process children nodes locally in child_trans
+ child_trans = XMLTranslator(self.callback)
+ child_trans.todo(escape(node.text or ""))
+ for child in node:
+ child_trans.process(child)
+
+ if (child_trans.all_todo() and
+ node.tag in TRANSLATED_ELEMENTS and
+ not any(attr.startswith("t-") for attr in node.attrib)):
+ # serialize the node element as todo
+ self.todo(serialize(node.tag, node.attrib, child_trans.get_todo()),
+ child_trans.needs_trans)
+ else:
+ # complete translations and serialize result as done
+ for attr in TRANSLATED_ATTRS:
+ if node.get(attr):
+ node.set(attr, self.process_text(node.get(attr)))
+ self.done(serialize(node.tag, node.attrib, child_trans.get_done()))
+
+ # add node tail as todo
+ self.todo(escape(node.tail or ""))
+
+
+def xml_translate(callback, value):
+ """ Translate an XML value (string), using `callback` for translating text
+ appearing in `value`. If `value` is not XML valid, it is parsed with an HTML
+ parser instead, and some element wrapping is done to make the parsing work.
+ """
+ if not value:
+ return value
+
+ trans = XMLTranslator(callback)
+ try:
+ root = etree.fromstring(encode(value))
+ trans.process(root)
+ return trans.get_done()
+ except etree.ParseError:
+ wrapped = "<div>%s</div>" % value
+ root = etree.fromstring(wrapped, etree.HTMLParser())
+ # html > body > div
+ trans.process(root[0][0])
+ return trans.get_done()[5:-6]
+
+
#
# Warning: better use self.pool.get('ir.translation')._get_source if you can
#
@@ -508,28 +657,6 @@ def trans_export(lang, modules, buffer, format, cr):
_process(format, modules, translations, buffer, lang)
del translations
-def trans_parse_xsl(de):
- return list(set(trans_parse_xsl_aux(de, False)))
-
-def trans_parse_xsl_aux(de, t):
- res = []
-
- for n in de:
- t = t or n.get("t")
- if t:
- if isinstance(n, SKIPPED_ELEMENT_TYPES) or n.tag.startswith('{http://www.w3.org/1999/XSL/Transform}'):
- continue
- if n.text:
- l = n.text.strip().replace('\n',' ')
- if len(l):
- res.append(l.encode("utf8"))
- if n.tail:
- l = n.tail.strip().replace('\n',' ')
- if len(l):
- res.append(l.encode("utf8"))
- res.extend(trans_parse_xsl_aux(n, t))
- return res
-
def trans_parse_rml(de):
res = []
for n in de:
@@ -550,28 +677,6 @@ def _push(callback, term, source_line):
if len(term) > 8 or any(x.isalpha() for x in term):
callback(term, source_line)
-def trans_parse_view(element, callback):
- """ Helper method to recursively walk an etree document representing a
- regular view and call ``callback(term)`` for each translatable term
- that is found in the document.
-
- :param ElementTree element: root of etree document to extract terms from
- :param callable callback: a callable in the form ``f(term, source_line)``,
- that will be called for each extracted term.
- """
- for el in element.iter():
- if (not isinstance(el, SKIPPED_ELEMENT_TYPES)
- and el.tag.lower() not in SKIPPED_ELEMENTS
- and el.get("translation", '').strip() != "off"
- and el.text):
- _push(callback, el.text, el.sourceline)
- if el.tail:
- _push(callback, el.tail, el.sourceline)
- for attr in ('string', 'help', 'sum', 'confirm', 'placeholder'):
- value = el.get(attr)
- if value:
- _push(callback, value, el.sourceline)
-
# tests whether an object is in a list of modules
def in_modules(object_name, modules):
if 'all' in modules:
@@ -668,11 +773,6 @@ def trans_generate(lang, modules, cr):
tnx = (module, source, name, id, type, tuple(comments or ()))
_to_translate.add(tnx)
- def encode(s):
- if isinstance(s, unicode):
- return s.encode('utf8')
- return s
-
def push(mod, type, name, res_id, term):
term = (term or '').strip()
if len(term) > 2 or term in ENGLISH_SMALL_WORDS:
@@ -705,51 +805,20 @@ def trans_generate(lang, modules, cr):
_logger.warning("Unable to find object %r with id %d", model, res_id)
continue
- if model=='ir.ui.view':
- d = etree.XML(encode(obj.arch))
- if obj.type == 'qweb':
- view_id = get_root_view(xml_name)
- push_qweb = lambda t,l: push(module, 'view', 'website', view_id, t)
- _extract_translatable_qweb_terms(d, push_qweb)
- else:
- push_view = lambda t,l: push(module, 'view', obj.model, xml_name, t)
- trans_parse_view(d, push_view)
- elif model=='ir.actions.wizard':
- pass # TODO Can model really be 'ir.actions.wizard' ?
-
- elif model=='ir.model.fields':
+ if model=='ir.model.fields':
try:
field_name = encode(obj.name)
except AttributeError, exc:
_logger.error("name error in %s: %s", xml_name, str(exc))
continue
- objmodel = registry.get(obj.model)
- if (objmodel is None or field_name not in objmodel._columns
- or not objmodel._translate):
+ field_model = registry.get(obj.model)
+ if (field_model is None or not field_model._translate or
+ field_name not in field_model._fields):
continue
- field_def = objmodel._columns[field_name]
-
- name = "%s,%s" % (encode(obj.model), field_name)
- push_translation(module, 'field', name, 0, encode(field_def.string))
-
- if field_def.help:
- push_translation(module, 'help', name, 0, encode(field_def.help))
-
- if field_def.translate:
- ids = objmodel.search(cr, uid, [])
- obj_values = objmodel.read(cr, uid, ids, [field_name])
- for obj_value in obj_values:
- res_id = obj_value['id']
- if obj.name in ('ir.model', 'ir.ui.menu'):
- res_id = 0
- model_data_ids = model_data_obj.search(cr, uid, [
- ('model', '=', model),
- ('res_id', '=', res_id),
- ])
- if not model_data_ids:
- push_translation(module, 'model', name, 0, encode(obj_value[field_name]))
+ field_def = field_model._fields[field_name]
if hasattr(field_def, 'selection') and isinstance(field_def.selection, (list, tuple)):
+ name = "%s,%s" % (encode(obj.model), field_name)
for dummy, val in field_def.selection:
push_translation(module, 'selection', name, 0, encode(val))
@@ -761,9 +830,7 @@ def trans_generate(lang, modules, cr):
parse_func = trans_parse_rml
report_type = "report"
elif obj.report_xsl:
- fname = obj.report_xsl
- parse_func = trans_parse_xsl
- report_type = "xsl"
+ continue
if fname and obj.report_type in ('pdf', 'xsl'):
try:
report_file = misc.file_open(fname)
@@ -776,17 +843,15 @@ def trans_generate(lang, modules, cr):
except (IOError, etree.XMLSyntaxError):
_logger.exception("couldn't export translation for report %s %s %s", name, report_type, fname)
- for field_name, field_def in obj._columns.items():
- if model == 'ir.model' and field_name == 'name' and obj.name == obj.model:
- # ignore model name if it is the technical one, nothing to translate
- continue
- if field_def.translate:
+ for field_name, field_def in obj._fields.iteritems():
+ if getattr(field_def, 'translate', None):
name = model + "," + field_name
try:
- term = obj[field_name] or ''
- except:
- term = ''
- push_translation(module, 'model', name, xml_name, encode(term))
+ value = obj[field_name] or ''
+ except Exception:
+ continue
+ for term in set(field_def.get_trans_terms(value)):
+ push_translation(module, 'model', name, xml_name, encode(term))
# End of data for ir.model.data query results