first push message
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
'name': "odoo_project_sync",
|
||||
|
||||
'summary': "Short (1 phrase/line) summary of the module's purpose",
|
||||
|
||||
'description': """
|
||||
Long description of module's purpose
|
||||
""",
|
||||
|
||||
'author': "My Company",
|
||||
'website': "https://www.yourcompany.com",
|
||||
|
||||
# Categories can be used to filter modules in modules listing
|
||||
# Check https://github.com/odoo/odoo/blob/15.0/odoo/addons/base/data/ir_module_category_data.xml
|
||||
# for the full list
|
||||
'category': 'Uncategorized',
|
||||
'version': '0.1',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
'depends': ['project'],
|
||||
|
||||
# always loaded
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/project_sync_config_views.xml',
|
||||
'views/remote_project_views.xml',
|
||||
],
|
||||
# only loaded in demonstration mode
|
||||
'demo': [
|
||||
'demo/demo.xml',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from . import controllers
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,21 @@
|
||||
# from odoo import http
|
||||
|
||||
|
||||
# class OdooProjectSync(http.Controller):
|
||||
# @http.route('/odoo_project_sync/odoo_project_sync', auth='public')
|
||||
# def index(self, **kw):
|
||||
# return "Hello, world"
|
||||
|
||||
# @http.route('/odoo_project_sync/odoo_project_sync/objects', auth='public')
|
||||
# def list(self, **kw):
|
||||
# return http.request.render('odoo_project_sync.listing', {
|
||||
# 'root': '/odoo_project_sync/odoo_project_sync',
|
||||
# 'objects': http.request.env['odoo_project_sync.odoo_project_sync'].search([]),
|
||||
# })
|
||||
|
||||
# @http.route('/odoo_project_sync/odoo_project_sync/objects/<model("odoo_project_sync.odoo_project_sync"):obj>', auth='public')
|
||||
# def object(self, obj, **kw):
|
||||
# return http.request.render('odoo_project_sync.object', {
|
||||
# 'object': obj
|
||||
# })
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<!--
|
||||
<record id="object0" model="odoo_project_sync.odoo_project_sync">
|
||||
<field name="name">Object 0</field>
|
||||
<field name="value">0</field>
|
||||
</record>
|
||||
|
||||
<record id="object1" model="odoo_project_sync.odoo_project_sync">
|
||||
<field name="name">Object 1</field>
|
||||
<field name="value">10</field>
|
||||
</record>
|
||||
|
||||
<record id="object2" model="odoo_project_sync.odoo_project_sync">
|
||||
<field name="name">Object 2</field>
|
||||
<field name="value">20</field>
|
||||
</record>
|
||||
|
||||
<record id="object3" model="odoo_project_sync.odoo_project_sync">
|
||||
<field name="name">Object 3</field>
|
||||
<field name="value">30</field>
|
||||
</record>
|
||||
|
||||
<record id="object4" model="odoo_project_sync.odoo_project_sync">
|
||||
<field name="name">Object 4</field>
|
||||
<field name="value">40</field>
|
||||
</record>
|
||||
-->
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,3 @@
|
||||
from . import remote_project
|
||||
from . import project_sync_config
|
||||
from . import project_task
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,299 @@
|
||||
|
||||
import xmlrpc.client
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ProjectSyncConfig(models.Model):
|
||||
_name = 'project.sync.config'
|
||||
_description = 'Project Server Synchronization Configuration'
|
||||
|
||||
name = fields.Char(string='Connection Name', required=True)
|
||||
remote_url = fields.Char(string='Remote URL', required=True, help='e.g., https://remote-odoo.com')
|
||||
remote_db = fields.Char(string='Remote Database', required=True)
|
||||
remote_user = fields.Char(string='Remote Username', required=True)
|
||||
remote_password = fields.Char(string='Remote Password', required=True)
|
||||
|
||||
remote_project_ids = fields.Many2many(
|
||||
'project.sync.remote.project',
|
||||
relation='project_sync_config_all_projects_rel',
|
||||
column1='config_id',
|
||||
column2='project_id',
|
||||
string='All Remote Projects',
|
||||
readonly=True
|
||||
)
|
||||
|
||||
selected_project_ids = fields.Many2many(
|
||||
'project.sync.remote.project',
|
||||
relation='project_sync_config_selected_rel',
|
||||
column1='config_id',
|
||||
column2='selected_project_id',
|
||||
string='Projects to Sync',
|
||||
domain="[('sync_config_id', '=', id)]"
|
||||
)
|
||||
|
||||
sync_direction = fields.Selection([
|
||||
('remote_to_local', 'Remote to Local'),
|
||||
('local_to_remote', 'Local to Remote')
|
||||
], string='Direction', default='remote_to_local')
|
||||
|
||||
last_sync = fields.Datetime(string='Last Sync', readonly=True)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
def _get_connection(self):
|
||||
"""Test connection and return XML-RPC proxies"""
|
||||
self.ensure_one()
|
||||
try:
|
||||
url = self.remote_url.rstrip('/')
|
||||
|
||||
# Test common endpoint
|
||||
common = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/common')
|
||||
|
||||
# Authenticate
|
||||
uid = common.authenticate(self.remote_db, self.remote_user, self.remote_password, {})
|
||||
if not uid:
|
||||
raise UserError(
|
||||
f"Authentication failed for user '{self.remote_user}' on database '{self.remote_db}'. Check credentials and user access rights.")
|
||||
|
||||
# Get models proxy
|
||||
models_proxy = xmlrpc.client.ServerProxy(f'{url}/xmlrpc/2/object')
|
||||
|
||||
# Verify project module exists on remote
|
||||
modules = models_proxy.execute_kw(
|
||||
self.remote_db, uid, self.remote_password,
|
||||
'ir.module.module', 'search_read',
|
||||
[[('name', '=', 'project'), ('state', '=', 'installed')]],
|
||||
{'fields': ['name', 'state']}
|
||||
)
|
||||
if not modules:
|
||||
raise UserError("Remote server does not have the 'project' module installed!")
|
||||
|
||||
_logger.info(f"Connected to remote server {url} as UID {uid}")
|
||||
return uid, models_proxy
|
||||
|
||||
except xmlrpc.client.Fault as e:
|
||||
raise UserError(f"XML-RPC Fault: {e.faultString}")
|
||||
except Exception as e:
|
||||
raise UserError(f"Connection failed: {str(e)}\nURL: {self.remote_url}\nDB: {self.remote_db}")
|
||||
|
||||
def action_fetch_remote_projects(self):
|
||||
"""Fetch all projects from remote server"""
|
||||
self.ensure_one()
|
||||
try:
|
||||
uid, models_proxy = self._get_connection()
|
||||
|
||||
# Fetch projects
|
||||
remote_projects = models_proxy.execute_kw(
|
||||
self.remote_db, uid, self.remote_password,
|
||||
'project.project', 'search_read',
|
||||
[[]],
|
||||
{'fields': ['id', 'name', 'active']}
|
||||
)
|
||||
|
||||
_logger.info(f"Fetched {len(remote_projects)} projects from remote")
|
||||
|
||||
# Clear old mappings
|
||||
old_mappings = self.env['project.sync.remote.project'].search([('sync_config_id', '=', self.id)])
|
||||
old_mappings.unlink()
|
||||
|
||||
# Create new mappings
|
||||
vals_list = []
|
||||
for p in remote_projects:
|
||||
vals_list.append({
|
||||
'sync_config_id': self.id,
|
||||
'remote_id': p['id'],
|
||||
'remote_name': p['name'],
|
||||
'sync_status': 'pending',
|
||||
'error_message': False,
|
||||
})
|
||||
|
||||
if vals_list:
|
||||
self.env['project.sync.remote.project'].create(vals_list)
|
||||
|
||||
# Refresh cache
|
||||
self.invalidate_recordset(['remote_project_ids', 'selected_project_ids'])
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Success',
|
||||
'message': f'Loaded {len(remote_projects)} remote projects.',
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(f"Fetch failed: {str(e)}", exc_info=True)
|
||||
raise UserError(f"Failed to fetch projects: {str(e)}")
|
||||
|
||||
def action_sync(self):
|
||||
"""Sync selected projects from remote to local"""
|
||||
if not self.selected_project_ids:
|
||||
raise UserError("Please select at least one project to sync. Go to 'Remote Projects' tab first.")
|
||||
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for mapping in self.selected_project_ids:
|
||||
try:
|
||||
tasks_synced = self._sync_single_project(mapping)
|
||||
mapping.write({
|
||||
'sync_status': 'synced',
|
||||
'error_message': False,
|
||||
'last_sync_attempt': fields.Datetime.now(),
|
||||
'tasks_synced_count': tasks_synced
|
||||
})
|
||||
success_count += 1
|
||||
_logger.info(f"Synced project '{mapping.remote_name}': {tasks_synced} tasks")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
mapping.write({
|
||||
'sync_status': 'error',
|
||||
'error_message': error_msg,
|
||||
'last_sync_attempt': fields.Datetime.now()
|
||||
})
|
||||
error_count += 1
|
||||
_logger.error(f"Sync failed for {mapping.remote_name}: {error_msg}", exc_info=True)
|
||||
|
||||
self.last_sync = fields.Datetime.now()
|
||||
|
||||
# Show result
|
||||
msg = f"Sync completed: {success_count} succeeded, {error_count} failed."
|
||||
if error_count > 0:
|
||||
msg += " Check error details on each project."
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Sync Result',
|
||||
'message': msg,
|
||||
'type': 'danger' if error_count > 0 else 'success',
|
||||
'sticky': error_count > 0,
|
||||
}
|
||||
}
|
||||
|
||||
def _sync_single_project(self, mapping):
|
||||
"""Sync tasks from one remote project"""
|
||||
self.ensure_one()
|
||||
uid, models_proxy = self._get_connection()
|
||||
|
||||
# Check if local project is mapped
|
||||
local_project_id = mapping.local_project_id.id if mapping.local_project_id else False
|
||||
|
||||
# Fetch remote tasks with all needed fields
|
||||
domain = [('project_id', '=', mapping.remote_id)]
|
||||
|
||||
# Get available fields on remote task
|
||||
task_fields = models_proxy.execute_kw(
|
||||
self.remote_db, uid, self.remote_password,
|
||||
'project.task', 'fields_get', [],
|
||||
{'attributes': ['string']}
|
||||
)
|
||||
|
||||
# Build field list dynamically
|
||||
fields_to_fetch = ['id', 'name', 'description']
|
||||
if 'stage_id' in task_fields:
|
||||
fields_to_fetch.append('stage_id')
|
||||
if 'user_ids' in task_fields:
|
||||
fields_to_fetch.append('user_ids')
|
||||
if 'tag_ids' in task_fields:
|
||||
fields_to_fetch.append('tag_ids')
|
||||
if 'priority' in task_fields:
|
||||
fields_to_fetch.append('priority')
|
||||
if 'date_deadline' in task_fields:
|
||||
fields_to_fetch.append('date_deadline')
|
||||
|
||||
remote_tasks = models_proxy.execute_kw(
|
||||
self.remote_db, uid, self.remote_password,
|
||||
'project.task', 'search_read',
|
||||
[domain],
|
||||
{'fields': fields_to_fetch, 'limit': 1000}
|
||||
)
|
||||
|
||||
_logger.info(f"Found {len(remote_tasks)} tasks in remote project '{mapping.remote_name}'")
|
||||
|
||||
tasks_synced = 0
|
||||
for rt in remote_tasks:
|
||||
try:
|
||||
# Prepare task values
|
||||
task_vals = {
|
||||
'name': rt.get('name', 'Untitled Task'),
|
||||
'description': rt.get('description', ''),
|
||||
'project_id': local_project_id,
|
||||
'remote_task_id': rt['id'],
|
||||
}
|
||||
|
||||
# Map stage if exists
|
||||
if 'stage_id' in rt and rt['stage_id']:
|
||||
# Try to find matching stage locally by name
|
||||
stage_id = rt['stage_id'][0]
|
||||
stage_name = rt['stage_id'][1]
|
||||
local_stage = self.env['project.task.type'].search([('name', '=', stage_name)], limit=1)
|
||||
if local_stage:
|
||||
task_vals['stage_id'] = local_stage.id
|
||||
|
||||
# Map assignees if exists
|
||||
if 'user_ids' in rt and rt['user_ids']:
|
||||
# Get remote user IDs
|
||||
remote_user_ids = [u[0] for u in rt['user_ids']]
|
||||
# For now, skip assignee mapping (requires user mapping table)
|
||||
_logger.debug(f"Remote task {rt['id']} has assignees: {remote_user_ids}")
|
||||
|
||||
# Map tags if exists
|
||||
if 'tag_ids' in rt and rt['tag_ids']:
|
||||
remote_tag_ids = [t[0] for t in rt['tag_ids']]
|
||||
# For now, skip tag mapping (requires tag mapping table)
|
||||
|
||||
# Map priority
|
||||
if 'priority' in rt:
|
||||
task_vals['priority'] = rt.get('priority', '0')
|
||||
|
||||
# Map deadline
|
||||
if 'date_deadline' in rt and rt.get('date_deadline'):
|
||||
task_vals['date_deadline'] = rt['date_deadline']
|
||||
|
||||
# Check if task already exists
|
||||
existing_task = self.env['project.task'].search([
|
||||
('remote_task_id', '=', rt['id']),
|
||||
('project_id', '=', local_project_id)
|
||||
], limit=1)
|
||||
|
||||
if existing_task:
|
||||
# Update existing
|
||||
existing_task.write(task_vals)
|
||||
_logger.debug(f"Updated local task {existing_task.id}")
|
||||
else:
|
||||
# Create new
|
||||
new_task = self.env['project.task'].create(task_vals)
|
||||
_logger.debug(f"Created local task {new_task.id}")
|
||||
|
||||
tasks_synced += 1
|
||||
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to sync task {rt.get('id')}: {str(e)}", exc_info=True)
|
||||
continue
|
||||
|
||||
return tasks_synced
|
||||
|
||||
def action_clear_errors(self):
|
||||
"""Reset error status for all selected projects"""
|
||||
for mapping in self.selected_project_ids:
|
||||
mapping.write({
|
||||
'sync_status': 'pending',
|
||||
'error_message': False
|
||||
})
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Cleared',
|
||||
'message': 'Error status cleared. You can retry sync.',
|
||||
'type': 'success'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
from odoo import models, fields
|
||||
|
||||
class ProjectTask(models.Model):
|
||||
_inherit = 'project.task'
|
||||
|
||||
remote_id = fields.Integer(
|
||||
string='Remote Server Task ID',
|
||||
copy=False,
|
||||
help="Stores the ID of the corresponding task on the remote Odoo server."
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class RemoteProjectMapping(models.Model):
|
||||
_name = 'project.sync.remote.project'
|
||||
_description = 'Remote Project Mapping'
|
||||
_order = 'sync_status, remote_name'
|
||||
|
||||
sync_config_id = fields.Many2one('project.sync.config', required=True, ondelete='cascade')
|
||||
remote_id = fields.Integer(string='Remote Project ID', required=True)
|
||||
remote_name = fields.Char(string='Remote Project Name', required=True)
|
||||
local_project_id = fields.Many2one('project.project', string='Local Target Project')
|
||||
sync_status = fields.Selection([
|
||||
('pending', 'Pending'),
|
||||
('synced', 'Synced'),
|
||||
('error', 'Error')
|
||||
], default='pending', readonly=True)
|
||||
|
||||
# ✅ NEW: Store error details
|
||||
error_message = fields.Text(string='Error Details', readonly=True)
|
||||
last_sync_attempt = fields.Datetime(string='Last Sync Attempt', readonly=True)
|
||||
tasks_synced_count = fields.Integer(string='Tasks Synced', default=0, readonly=True)
|
||||
@@ -0,0 +1,5 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sync_config_user,project.sync.config.user,model_project_sync_config,project.group_project_user,1,0,0,0
|
||||
access_sync_config_manager,project.sync.config.manager,model_project_sync_config,project.group_project_manager,1,1,1,1
|
||||
access_remote_project_user,project.sync.remote.project.user,model_project_sync_remote_project,project.group_project_user,1,0,0,0
|
||||
access_remote_project_manager,project.sync.remote.project.manager,model_project_sync_remote_project,project.group_project_manager,1,1,1,1
|
||||
|
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_project_sync_config_form" model="ir.ui.view">
|
||||
<field name="name">project.sync.config.form</field>
|
||||
<field name="model">project.sync.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="action_fetch_remote_projects" string="Fetch Remote Projects"
|
||||
type="object" class="oe_highlight"/>
|
||||
<button name="action_sync" string="Sync Selected Projects"
|
||||
type="object" class="oe_highlight"
|
||||
invisible="not selected_project_ids"/>
|
||||
<button name="action_clear_errors" string="Clear All Errors"
|
||||
type="object"
|
||||
invisible="not selected_project_ids"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="active"/>
|
||||
</group>
|
||||
<group string="Remote Server Connection">
|
||||
<field name="remote_url"/>
|
||||
<field name="remote_db"/>
|
||||
<field name="remote_user"/>
|
||||
<field name="remote_password" password="True"/>
|
||||
</group>
|
||||
<group string="Sync Settings">
|
||||
<field name="sync_direction"/>
|
||||
<field name="last_sync"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Remote Projects (Multi-Select)">
|
||||
<field name="remote_project_ids" readonly="1">
|
||||
<list>
|
||||
<field name="remote_id"/>
|
||||
<field name="remote_name"/>
|
||||
<field name="sync_status" widget="badge"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Selected for Sync">
|
||||
<field name="selected_project_ids" widget="many2many_tags"
|
||||
options="{'no_create': True, 'no_open': True, 'color_field': 'sync_status'}"/>
|
||||
<group>
|
||||
<p class="text-muted">
|
||||
Select projects from the list above, then map them to local projects.
|
||||
Click "Sync Selected Projects" to start.
|
||||
</p>
|
||||
</group>
|
||||
</page>
|
||||
<page string="Synced Tasks (Local Filter)">
|
||||
<p>After syncing, go to <b>Project → Tasks</b> and filter by project to view synced tasks.</p>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_project_sync_config" model="ir.actions.act_window">
|
||||
<field name="name">Project Sync Configurations</field>
|
||||
<field name="res_model">project.sync.config</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a connection to start syncing projects between Odoo instances.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_project_sync_root" name="Server Sync" parent="project.menu_project_config" sequence="100"/>
|
||||
<menuitem id="menu_project_sync_config" name="Sync Configurations" parent="menu_project_sync_root" action="action_project_sync_config" sequence="10"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,59 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_remote_project_list" model="ir.ui.view">
|
||||
<field name="name">project.sync.remote.project.list</field>
|
||||
<field name="model">project.sync.remote.project</field>
|
||||
<field name="arch" type="xml">
|
||||
<list editable="bottom" create="false" delete="false">
|
||||
<field name="remote_id" readonly="1"/>
|
||||
<field name="remote_name" readonly="1"/>
|
||||
<field name="local_project_id"/>
|
||||
<field name="tasks_synced_count" readonly="1"/>
|
||||
<field name="last_sync_attempt" readonly="1"/>
|
||||
<field name="sync_status" widget="badge"
|
||||
decoration-success="sync_status=='synced'"
|
||||
decoration-warning="sync_status=='pending'"
|
||||
decoration-danger="sync_status=='error'"/>
|
||||
<field name="error_message" readonly="1" optional="hide"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_remote_project_form" model="ir.ui.view">
|
||||
<field name="name">project.sync.remote.project.form</field>
|
||||
<field name="model">project.sync.remote.project</field>
|
||||
<field name="arch" type="xml">
|
||||
<form create="false" delete="false">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="remote_id"/>
|
||||
<field name="remote_name"/>
|
||||
<field name="sync_config_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="local_project_id"/>
|
||||
<field name="sync_status"/>
|
||||
<field name="tasks_synced_count"/>
|
||||
<field name="last_sync_attempt"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Error Details" invisible="not error_message">
|
||||
<field name="error_message" nolabel="1" readonly="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="action_project_sync_remote" model="ir.actions.act_window">
|
||||
<field name="name">Project Sync Configurations</field>
|
||||
<field name="res_model">project.sync.remote.project</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a connection to start syncing projects between Odoo instances.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<menuitem id="menu_project_sync_remote" name="Sync Remote" parent="menu_project_sync_root" action="action_project_sync_remote" sequence="11"/>
|
||||
</odoo>
|
||||
@@ -0,0 +1,24 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<!--
|
||||
<template id="listing">
|
||||
<ul>
|
||||
<li t-foreach="objects" t-as="object">
|
||||
<a t-attf-href="#{ root }/objects/#{ object.id }">
|
||||
<t t-esc="object.display_name"/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template id="object">
|
||||
<h1><t t-esc="object.display_name"/></h1>
|
||||
<dl>
|
||||
<t t-foreach="object._fields" t-as="field">
|
||||
<dt><t t-esc="field"/></dt>
|
||||
<dd><t t-esc="object[field]"/></dd>
|
||||
</t>
|
||||
</dl>
|
||||
</template>
|
||||
-->
|
||||
</data>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user