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:
@@ -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'}"/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</group>
|
||||
<group>
|
||||
<group name="authentication">
|
||||
<label for="id" string="Authentication"/>
|
||||
<div>
|
||||
<div>
|
||||
|
||||
@@ -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'}"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Registrations">
|
||||
<page name="registrations" string="Registrations">
|
||||
<group>
|
||||
<group>
|
||||
<field name="auto_confirm" invisible="1"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<field name="product_uom_id" class="oe_inline"/>
|
||||
</div>
|
||||
</group>
|
||||
<group string="Accounting">
|
||||
<group name="accounting" string="Accounting">
|
||||
<field domain="[('type','=','normal'),('state', '<>', 'close'),('parent_id','!=',False)]" name="account_id" select="1" options="{'no_open': True, 'no_create': True}"/>
|
||||
<field name="amount"/>
|
||||
<field name="general_account_id"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)]}">
|
||||
|
||||
@@ -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)]"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)"/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'):
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 & Services" for="options"/>
|
||||
<field name="quote_viewed" invisible="1"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'}"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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=[]
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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)]}"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import unittest
|
||||
from openerp.tools.translate import quote, unquote
|
||||
from openerp.tools.translate import quote, unquote, xml_translate
|
||||
|
||||
class TranslationToolsTestCase(unittest.TestCase):
|
||||
|
||||
@@ -27,3 +27,118 @@ class TranslationToolsTestCase(unittest.TestCase):
|
||||
self.assertRaises(AssertionError, quote, """test \nall kinds\n\no\r
|
||||
\\\\nope\n\n"
|
||||
""")
|
||||
|
||||
def test_translate_xml_base(self):
|
||||
""" Test xml_translate() without formatting elements. """
|
||||
terms = []
|
||||
source = """<form string="Form stuff">
|
||||
<h1>Blah blah blah</h1>
|
||||
Put some more text here
|
||||
<field name="foo"/>
|
||||
</form>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['Form stuff', 'Blah blah blah', 'Put some more text here'])
|
||||
|
||||
def test_translate_xml_inline1(self):
|
||||
""" Test xml_translate() with formatting elements. """
|
||||
terms = []
|
||||
source = """<form string="Form stuff">
|
||||
<h1>Blah <i>blah</i> blah</h1>
|
||||
Put some <b>more text</b> here
|
||||
<field name="foo"/>
|
||||
</form>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['Form stuff', 'Blah <i>blah</i> blah', 'Put some <b>more text</b> here'])
|
||||
|
||||
def test_translate_xml_inline2(self):
|
||||
""" Test xml_translate() with formatting elements embedding other elements. """
|
||||
terms = []
|
||||
source = """<form string="Form stuff">
|
||||
<b><h1>Blah <i>blah</i> blah</h1></b>
|
||||
Put <em>some <b>more text</b></em> here
|
||||
<field name="foo"/>
|
||||
</form>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['Form stuff', 'Blah <i>blah</i> blah', 'Put <em>some <b>more text</b></em> here'])
|
||||
|
||||
def test_translate_xml_inline3(self):
|
||||
""" Test xml_translate() with formatting elements without actual text. """
|
||||
terms = []
|
||||
source = """<form string="Form stuff">
|
||||
<div>
|
||||
<span class="before"/>
|
||||
<h1>Blah blah blah</h1>
|
||||
<span class="after">
|
||||
<i class="hack"/>
|
||||
</span>
|
||||
</div>
|
||||
</form>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['Form stuff', 'Blah blah blah'])
|
||||
|
||||
def test_translate_xml_t(self):
|
||||
""" Test xml_translate() with t-* attributes. """
|
||||
terms = []
|
||||
source = """<t t-name="stuff">
|
||||
stuff before
|
||||
<span t-field="o.name"/>
|
||||
stuff after
|
||||
</t>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['stuff before', 'stuff after'])
|
||||
|
||||
def test_translate_xml_off(self):
|
||||
""" Test xml_translate() with attribute translate="off". """
|
||||
terms = []
|
||||
source = """<div>
|
||||
stuff before
|
||||
<div translation="off">Do not translate this</div>
|
||||
stuff after
|
||||
</div>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['stuff before', 'stuff after'])
|
||||
|
||||
def test_translate_xml_attribute(self):
|
||||
""" Test xml_translate() with <attribute> elements. """
|
||||
terms = []
|
||||
source = """<field name="foo" position="attributes">
|
||||
<attribute name="string">Translate this</attribute>
|
||||
<attribute name="option">Do not translate this</attribute>
|
||||
</field>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['Translate this'])
|
||||
|
||||
def test_translate_xml_a(self):
|
||||
""" Test xml_translate() with <a> elements. """
|
||||
terms = []
|
||||
source = """<t t-name="stuff">
|
||||
<ul class="nav navbar-nav">
|
||||
<li>
|
||||
<a class="oe_menu_leaf" href="/web#menu_id=42&action=54">
|
||||
<span class="oe_menu_text">Blah</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="dropdown" id="menu_more_container" style="display: none;">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">More <b class="caret"/></a>
|
||||
<ul class="dropdown-menu" id="menu_more"/>
|
||||
</li>
|
||||
</ul>
|
||||
</t>"""
|
||||
result = xml_translate(terms.append, source)
|
||||
self.assertEquals(result, source)
|
||||
self.assertItemsEqual(terms,
|
||||
['<span class="oe_menu_text">Blah</span>', 'More <b class="caret"/>'])
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user