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`.
This commit is contained in:
Raphael Collet
2015-07-06 17:01:33 +02:00
74 changed files with 881 additions and 475 deletions
@@ -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'}"/>
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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"/>
+1 -1
View File
@@ -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"/>
+18 -1
View File
@@ -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
+1 -1
View File
@@ -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"/>
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -54,7 +54,7 @@
</div>
</div>
</group>
<group>
<group name="authentication">
<label for="id" string="Authentication"/>
<div>
<div>
+6 -6
View File
@@ -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'}"/>
+1 -1
View File
@@ -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"/>
+4 -4
View File
@@ -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">
@@ -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">
+25 -25
View File
@@ -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>
+1 -1
View File
@@ -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'}"/>
@@ -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>
+1 -1
View File
@@ -93,7 +93,7 @@
</group>
</group>
<notebook>
<page string="Registrations">
<page name="registrations" string="Registrations">
<group>
<group>
<field name="auto_confirm" invisible="1"/>
+1 -1
View File
@@ -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>
@@ -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
+1 -1
View File
@@ -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"/>
@@ -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" />
+1 -1
View File
@@ -50,7 +50,7 @@
<field name="product_uom_id" class="oe_inline"/>
</div>
</group>
<group string="Accounting">
<group name="accounting" string="Accounting">
<field domain="[('type','=','normal'),('state', '&lt;&gt;', 'close'),('parent_id','!=',False)]" name="account_id" select="1" options="{'no_open': True, 'no_create': True}"/>
<field name="amount"/>
<field name="general_account_id"/>
@@ -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"/>
@@ -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>
+1 -1
View File
@@ -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)]}">
+1 -1
View File
@@ -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)]"
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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)"/>
+1 -1
View File
@@ -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">
+3 -3
View File
@@ -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>
+2 -2
View File
@@ -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" />
+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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>
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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"/>
+1 -1
View File
@@ -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"/>
+1 -1
View File
@@ -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"/>
+2 -2
View File
@@ -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>
+2 -2
View File
@@ -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>
@@ -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>
+1 -1
View File
@@ -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>
@@ -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>
+4 -4
View File
@@ -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">
+1 -1
View File
@@ -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"/>
+1 -1
View File
@@ -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>
@@ -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>
+7 -5
View File
@@ -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'):
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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"/>
+3 -3
View File
@@ -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>
+1 -1
View File
@@ -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">
@@ -17,7 +17,7 @@
<xpath expr="//header/button[@name='print_quotation'][1]" position="replace">
<button name="print_quotation" string="Print" type="object" states="draft" groups="base.group_user"/>
</xpath>
<xpath expr="//page[@string='Order Lines']" position="after">
<xpath expr="//page/field[@name='order_line']/.." position="after">
<page string="Suggested Products">
<label string="Optional Products &amp; Services" for="options"/>
<field name="quote_viewed" invisible="1"/>
+1 -1
View File
@@ -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>
+6 -1
View File
@@ -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:
@@ -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>
+8 -4
View File
@@ -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,
field_description varchar,
ttype varchar,
state varchar default 'base',
field_description varchar,
help varchar,
ttype varchar,
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)
+1 -1
View File
@@ -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'])]}">
@@ -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'}"/>
+6 -1
View File
@@ -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
+10 -4
View File
@@ -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"/>
+259 -86
View File
@@ -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
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_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
@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):
@@ -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>
+40 -46
View File
@@ -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=[]
+4 -2
View File
@@ -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"/>
+2 -2
View File
@@ -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)]}"/>
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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>
+116 -1
View File
@@ -2,7 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import unittest
from openerp.tools.translate import quote, unquote
from openerp.tools.translate import quote, unquote, xml_translate
class TranslationToolsTestCase(unittest.TestCase):
@@ -27,3 +27,118 @@ class TranslationToolsTestCase(unittest.TestCase):
self.assertRaises(AssertionError, quote, """test \nall kinds\n\no\r
\\\\nope\n\n"
""")
def test_translate_xml_base(self):
""" Test xml_translate() without formatting elements. """
terms = []
source = """<form string="Form stuff">
<h1>Blah blah blah</h1>
Put some more text here
<field name="foo"/>
</form>"""
result = xml_translate(terms.append, source)
self.assertEquals(result, source)
self.assertItemsEqual(terms,
['Form stuff', 'Blah blah blah', 'Put some more text here'])
def test_translate_xml_inline1(self):
""" Test xml_translate() with formatting elements. """
terms = []
source = """<form string="Form stuff">
<h1>Blah <i>blah</i> blah</h1>
Put some <b>more text</b> here
<field name="foo"/>
</form>"""
result = xml_translate(terms.append, source)
self.assertEquals(result, source)
self.assertItemsEqual(terms,
['Form stuff', 'Blah <i>blah</i> blah', 'Put some <b>more text</b> here'])
def test_translate_xml_inline2(self):
""" Test xml_translate() with formatting elements embedding other elements. """
terms = []
source = """<form string="Form stuff">
<b><h1>Blah <i>blah</i> blah</h1></b>
Put <em>some <b>more text</b></em> here
<field name="foo"/>
</form>"""
result = xml_translate(terms.append, source)
self.assertEquals(result, source)
self.assertItemsEqual(terms,
['Form stuff', 'Blah <i>blah</i> blah', 'Put <em>some <b>more text</b></em> here'])
def test_translate_xml_inline3(self):
""" Test xml_translate() with formatting elements without actual text. """
terms = []
source = """<form string="Form stuff">
<div>
<span class="before"/>
<h1>Blah blah blah</h1>
<span class="after">
<i class="hack"/>
</span>
</div>
</form>"""
result = xml_translate(terms.append, source)
self.assertEquals(result, source)
self.assertItemsEqual(terms,
['Form stuff', 'Blah blah blah'])
def test_translate_xml_t(self):
""" Test xml_translate() with t-* attributes. """
terms = []
source = """<t t-name="stuff">
stuff before
<span t-field="o.name"/>
stuff after
</t>"""
result = xml_translate(terms.append, source)
self.assertEquals(result, source)
self.assertItemsEqual(terms,
['stuff before', 'stuff after'])
def test_translate_xml_off(self):
""" Test xml_translate() with attribute translate="off". """
terms = []
source = """<div>
stuff before
<div translation="off">Do not translate this</div>
stuff after
</div>"""
result = xml_translate(terms.append, source)
self.assertEquals(result, source)
self.assertItemsEqual(terms,
['stuff before', 'stuff after'])
def test_translate_xml_attribute(self):
""" Test xml_translate() with <attribute> elements. """
terms = []
source = """<field name="foo" position="attributes">
<attribute name="string">Translate this</attribute>
<attribute name="option">Do not translate this</attribute>
</field>"""
result = xml_translate(terms.append, source)
self.assertEquals(result, source)
self.assertItemsEqual(terms,
['Translate this'])
def test_translate_xml_a(self):
""" Test xml_translate() with <a> elements. """
terms = []
source = """<t t-name="stuff">
<ul class="nav navbar-nav">
<li>
<a class="oe_menu_leaf" href="/web#menu_id=42&amp;action=54">
<span class="oe_menu_text">Blah</span>
</a>
</li>
<li class="dropdown" id="menu_more_container" style="display: none;">
<a class="dropdown-toggle" data-toggle="dropdown" href="#">More <b class="caret"/></a>
<ul class="dropdown-menu" id="menu_more"/>
</li>
</ul>
</t>"""
result = xml_translate(terms.append, source)
self.assertEquals(result, source)
self.assertItemsEqual(terms,
['<span class="oe_menu_text">Blah</span>', 'More <b class="caret"/>'])
+5 -6
View File
@@ -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>
+71 -28
View File
@@ -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):
+61 -86
View File
@@ -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)
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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'
])
+163 -98
View File
@@ -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