diff options
| author | Raphael Collet <rco@openerp.com> | 2015-07-06 17:01:33 +0200 |
|---|---|---|
| committer | Raphael Collet <rco@openerp.com> | 2015-07-06 17:01:33 +0200 |
| commit | b2176210d9f0749bbdcc9e8c4cd1476e8f2ebda9 (patch) | |
| tree | 39708e89570536de313e49487c0ebfe339fc370d | |
| parent | f03b3e18392088bf745065294734f190f5185f9d (diff) | |
| parent | 9bd60ca93510e410a0136b8b433f596330900593 (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`.
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', '<>', '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 & 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&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 |
