first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
from . import controllers
from . import models
+33
View File
@@ -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
@@ -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
# })
+30
View File
@@ -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>
+3
View File
@@ -0,0 +1,3 @@
from . import remote_project
from . import project_sync_config
from . import project_task
@@ -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'
}
}
+10
View File
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sync_config_user project.sync.config.user model_project_sync_config project.group_project_user 1 0 0 0
3 access_sync_config_manager project.sync.config.manager model_project_sync_config project.group_project_manager 1 1 1 1
4 access_remote_project_user project.sync.remote.project.user model_project_sync_remote_project project.group_project_user 1 0 0 0
5 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>
+24
View File
@@ -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>