Public release from ruodoo-project: 19.0 - 2026-07-26 21:17:35 UTC
This commit is contained in:
11
tier_communications/models/__init__.py
Normal file
11
tier_communications/models/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
from . import tier_channel_mixin
|
||||
from . import tier_channel_email
|
||||
from . import tier_channel_tag
|
||||
from . import tier_mailbox
|
||||
from . import tier_mailbox_tag
|
||||
from . import tier_routing_channel
|
||||
from . import tier_routing_mailbox
|
||||
from . import tier_router
|
||||
from . import mail_message
|
||||
from . import mail_thread
|
||||
from . import res_users
|
||||
61
tier_communications/models/mail_message.py
Normal file
61
tier_communications/models/mail_message.py
Normal file
@ -0,0 +1,61 @@
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.addons.mail.tools.discuss import Store
|
||||
|
||||
|
||||
class MailMessage(models.Model):
|
||||
_inherit = 'mail.message'
|
||||
|
||||
channel_tag_ids = fields.Many2many(
|
||||
'tier.channel.tag',
|
||||
string='Delivery Channels',
|
||||
)
|
||||
mailbox_tag_ids = fields.One2many(
|
||||
'tier.mailbox.tag',
|
||||
'message_id',
|
||||
string='Mailbox Tags',
|
||||
)
|
||||
|
||||
def _to_store_defaults(self, target):
|
||||
"""Include channel_tag_ids as plain list of dicts for the frontend."""
|
||||
defaults = super()._to_store_defaults(target)
|
||||
defaults.append(
|
||||
Store.Attr(
|
||||
'channel_tag_ids',
|
||||
value=lambda m: [
|
||||
{'id': tag.id, 'name': tag.name, 'code': tag.code}
|
||||
for tag in m.channel_tag_ids
|
||||
]
|
||||
)
|
||||
)
|
||||
return defaults
|
||||
|
||||
@api.model
|
||||
def get_last_channel_code(self, thread_model, thread_id):
|
||||
"""Return the channel code of the last message in the thread.
|
||||
|
||||
Used by the composer to auto-select the channel based on the last
|
||||
incoming message. Returns None if no channel tag found.
|
||||
"""
|
||||
last_msg = self.search([
|
||||
('model', '=', thread_model),
|
||||
('res_id', '=', thread_id),
|
||||
('channel_tag_ids', '!=', False),
|
||||
], order='id desc', limit=1)
|
||||
if last_msg and last_msg.channel_tag_ids:
|
||||
return last_msg.channel_tag_ids[0].code
|
||||
return None
|
||||
|
||||
def action_move_to_mailbox(self):
|
||||
"""Open wizard to manually move this message to a mailbox."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Переместить в ящик'),
|
||||
'res_model': 'tier.move.to.mailbox.wizard',
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
'context': {
|
||||
'default_message_id': self.id,
|
||||
'default_current_mailbox_ids': self.mailbox_tag_ids.mapped('mailbox_id').ids,
|
||||
},
|
||||
}
|
||||
253
tier_communications/models/mail_thread.py
Normal file
253
tier_communications/models/mail_thread.py
Normal file
@ -0,0 +1,253 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from odoo import models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MailThread(models.AbstractModel):
|
||||
_inherit = 'mail.thread'
|
||||
|
||||
def _get_allowed_message_params(self):
|
||||
"""Extend allowed params so channel_tag_ids passes through the controller."""
|
||||
return super()._get_allowed_message_params() | {'channel_tag_ids'}
|
||||
|
||||
def _get_message_create_valid_field_names(self):
|
||||
"""Add channel_tag_ids to the whitelist for _message_create."""
|
||||
return super()._get_message_create_valid_field_names() | {'channel_tag_ids'}
|
||||
|
||||
def assign_mailbox(self, mailbox_id):
|
||||
"""Assign all messages of this thread to the given tier.mailbox.
|
||||
|
||||
Called from the chatter UI when user clicks "Входящие лиды" / "Входящие тикеты".
|
||||
Removes existing tags for this mailbox first to avoid duplicates.
|
||||
"""
|
||||
self.ensure_one()
|
||||
mailbox = self.env['tier.mailbox'].browse(mailbox_id)
|
||||
if not mailbox.exists():
|
||||
return False
|
||||
|
||||
# Get all messages of this thread
|
||||
thread_messages = self.env['mail.message'].search([
|
||||
('model', '=', self._name),
|
||||
('res_id', '=', self.id),
|
||||
])
|
||||
|
||||
for message in thread_messages:
|
||||
# Remove existing tag for this mailbox if any
|
||||
existing_tag = message.mailbox_tag_ids.filtered(
|
||||
lambda tag: tag.mailbox_id.id == mailbox_id
|
||||
)
|
||||
if existing_tag:
|
||||
existing_tag.unlink()
|
||||
# Create new tag
|
||||
self.env['tier.mailbox.tag'].create({
|
||||
'mailbox_id': mailbox_id,
|
||||
'message_id': message.id,
|
||||
})
|
||||
|
||||
print(
|
||||
f"[tier_communications] assign_mailbox: "
|
||||
f"thread {self._name}#{self.id} → mailbox '{mailbox.name}' "
|
||||
f"({len(thread_messages)} messages tagged)"
|
||||
)
|
||||
return True
|
||||
|
||||
# =========================================================================
|
||||
# ОТПРАВКА ИСХОДЯЩИХ СООБЩЕНИЙ
|
||||
# =========================================================================
|
||||
|
||||
def message_post(self, *, body='', **kwargs):
|
||||
"""Override: создаём сообщение в Odoo. Внешняя отправка через канал
|
||||
выполняется отдельным RPC-вызовом из JS (/tier_communications/send_via_channel).
|
||||
"""
|
||||
message = super().message_post(body=body, **kwargs)
|
||||
if not message:
|
||||
return message
|
||||
|
||||
if not message.channel_tag_ids:
|
||||
print(
|
||||
f"[tier_communications] message_post: msg_id={message.id} — "
|
||||
f"канал не выбран, сообщение только в Odoo"
|
||||
)
|
||||
|
||||
return message
|
||||
|
||||
# =========================================================================
|
||||
# ! ОТПРАВКА СООБЩЕНИЯ НА EMAIL
|
||||
# =========================================================================
|
||||
|
||||
def _tier_send_email(self, message):
|
||||
"""Отправить сообщение на email партнёра треда через стандартный mail.mail."""
|
||||
recipient_email = None
|
||||
|
||||
# 1. Thread has partner_id (business models: sale.order, crm.lead, etc.)
|
||||
if self._name not in ('mail.thread', 'discuss.channel') and hasattr(self, 'partner_id'):
|
||||
partner = self.partner_id
|
||||
if partner and partner.email:
|
||||
recipient_email = partner.email
|
||||
|
||||
# 2. Message has explicit partner_ids
|
||||
if not recipient_email and message.partner_ids:
|
||||
for partner in message.partner_ids:
|
||||
partner_email = partner.email if hasattr(partner, 'email') else None
|
||||
if partner_email and isinstance(partner_email, str) and '@' in partner_email:
|
||||
recipient_email = partner_email
|
||||
break
|
||||
|
||||
# 3. Last incoming message in this thread — use its email_from
|
||||
if not recipient_email and message.model and message.res_id:
|
||||
last_incoming = self.env['mail.message'].search([
|
||||
('model', '=', message.model),
|
||||
('res_id', '=', message.res_id),
|
||||
('message_type', 'in', ['email', 'comment']),
|
||||
('email_from', '!=', False),
|
||||
('author_id', '!=', self.env.user.partner_id.id),
|
||||
], order='id desc', limit=1)
|
||||
if last_incoming and last_incoming.email_from:
|
||||
email_match = re.search(r'<([^>]+)>', last_incoming.email_from)
|
||||
recipient_email = email_match.group(1) if email_match else last_incoming.email_from
|
||||
|
||||
# 4. For discuss.channel — find the other member's email
|
||||
if not recipient_email and self._name == 'discuss.channel':
|
||||
other_members = self.channel_member_ids.filtered(
|
||||
lambda m: m.partner_id and m.partner_id != self.env.user.partner_id
|
||||
)
|
||||
for member in other_members:
|
||||
if member.partner_id.email:
|
||||
recipient_email = member.partner_id.email
|
||||
break
|
||||
|
||||
if not recipient_email:
|
||||
print(
|
||||
f"[tier_communications] ! ОТПРАВКА СООБЩЕНИЯ НА EMAIL: "
|
||||
f"msg_id={message.id} — У пользователя нет email адреса получателя"
|
||||
)
|
||||
return
|
||||
|
||||
print(
|
||||
f"[tier_communications] ! ОТПРАВКА СООБЩЕНИЯ НА EMAIL: "
|
||||
f"msg_id={message.id}, email_to={recipient_email}"
|
||||
)
|
||||
|
||||
try:
|
||||
mail_values = {
|
||||
'subject': message.subject or 'Сообщение из Odoo',
|
||||
'body_html': message.body or '',
|
||||
'email_to': recipient_email,
|
||||
'email_from': self.env.company.email or self.env.user.email or '',
|
||||
'auto_delete': True,
|
||||
}
|
||||
outgoing_mail = self.env['mail.mail'].sudo().create(mail_values)
|
||||
outgoing_mail.send()
|
||||
print(
|
||||
f"[tier_communications] ! ОТПРАВКА СООБЩЕНИЯ НА EMAIL: "
|
||||
f"msg_id={message.id} — SUCCESS, mail.mail id={outgoing_mail.id}"
|
||||
)
|
||||
except Exception as send_error:
|
||||
print(
|
||||
f"[tier_communications] ! ОТПРАВКА СООБЩЕНИЯ НА EMAIL: "
|
||||
f"msg_id={message.id} — ОШИБКА: {send_error}"
|
||||
)
|
||||
_logger.exception(
|
||||
'tier_communications: ошибка отправки email, msg_id=%s', message.id
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX
|
||||
# =========================================================================
|
||||
|
||||
def _tier_send_matrix(self, message):
|
||||
"""Отправить сообщение в Matrix комнату текущего пользователя."""
|
||||
current_user = self.env.user
|
||||
|
||||
# Matrix fields are defined in tier_communications_matrix module
|
||||
# If not installed — fields don't exist
|
||||
if not hasattr(current_user, 'tier_matrix_homeserver'):
|
||||
print(
|
||||
f"[tier_communications] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id} — модуль tier_communications_matrix не установлен"
|
||||
)
|
||||
return
|
||||
|
||||
homeserver_url = current_user.tier_matrix_homeserver
|
||||
access_token = current_user.tier_matrix_access_token
|
||||
room_id = current_user.tier_matrix_room_id
|
||||
|
||||
if not homeserver_url:
|
||||
print(
|
||||
f"[tier_communications] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id} — У пользователя нет homeserver URL (tier_matrix_homeserver)"
|
||||
)
|
||||
return
|
||||
if not access_token:
|
||||
print(
|
||||
f"[tier_communications] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id} — У пользователя нет access token (tier_matrix_access_token)"
|
||||
)
|
||||
return
|
||||
if not room_id:
|
||||
print(
|
||||
f"[tier_communications] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id} — У пользователя нет room ID (tier_matrix_room_id)"
|
||||
)
|
||||
return
|
||||
|
||||
print(
|
||||
f"[tier_communications] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id}, room={room_id}, homeserver={homeserver_url}"
|
||||
)
|
||||
|
||||
if 'tier.channel.matrix.helper' in self.env:
|
||||
try:
|
||||
self.env['tier.channel.matrix.helper'].channel_send_message(
|
||||
message, user=current_user
|
||||
)
|
||||
except Exception as send_error:
|
||||
print(
|
||||
f"[tier_communications] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id} — ОШИБКА: {send_error}"
|
||||
)
|
||||
_logger.exception(
|
||||
'tier_communications: ошибка отправки matrix, msg_id=%s', message.id
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"[tier_communications] ! ОТПРАВКА СООБЩЕНИЯ НА MATRIX: "
|
||||
f"msg_id={message.id} — модуль tier_communications_matrix не установлен"
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
# ПРИЁМ ВХОДЯЩИХ СООБЩЕНИЙ (fetchmail / стандартный механизм Odoo)
|
||||
# =========================================================================
|
||||
|
||||
def message_process(self, model, message, custom_values=None, save_original=False,
|
||||
strip_attachments=False, thread_id=None):
|
||||
"""Override: после стандартной обработки входящего письма — помечаем тегом email."""
|
||||
result = super().message_process(
|
||||
model, message,
|
||||
custom_values=custom_values,
|
||||
save_original=save_original,
|
||||
strip_attachments=strip_attachments,
|
||||
thread_id=thread_id,
|
||||
)
|
||||
if result:
|
||||
incoming_message = self.env['mail.message'].search(
|
||||
[('model', '=', model), ('res_id', '=', result)],
|
||||
order='id desc',
|
||||
limit=1,
|
||||
)
|
||||
if incoming_message:
|
||||
email_tag = self.env['tier.channel.tag'].search(
|
||||
[('code', '=', 'email')], limit=1
|
||||
)
|
||||
if email_tag and not incoming_message.channel_tag_ids:
|
||||
incoming_message.channel_tag_ids = [(4, email_tag.id)]
|
||||
print(
|
||||
f"[tier_communications] message_process: "
|
||||
f"входящее письмо msg_id={incoming_message.id} помечено тегом 'email'"
|
||||
)
|
||||
from .tier_router import TierRouter
|
||||
TierRouter(self.env).route_incoming(incoming_message)
|
||||
return result
|
||||
12
tier_communications/models/res_users.py
Normal file
12
tier_communications/models/res_users.py
Normal file
@ -0,0 +1,12 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
tier_preferred_channel_id = fields.Many2one(
|
||||
'tier.channel.tag',
|
||||
string='Предпочтительный канал связи',
|
||||
domain=[('active', '=', True)],
|
||||
help='Канал связи, который будет использоваться по умолчанию при отправке сообщений.',
|
||||
)
|
||||
51
tier_communications/models/tier_channel_email.py
Normal file
51
tier_communications/models/tier_channel_email.py
Normal file
@ -0,0 +1,51 @@
|
||||
from odoo import models
|
||||
|
||||
|
||||
class TierChannelEmail(models.Model):
|
||||
"""Concrete Email channel implementation.
|
||||
|
||||
Thin wrapper around Odoo's standard mail.mail mechanism.
|
||||
Requirements: 3.1, 3.2
|
||||
"""
|
||||
_name = 'tier.channel.email'
|
||||
_inherit = ['tier.channel.mixin']
|
||||
_description = 'Email Channel'
|
||||
|
||||
def channel_authenticate(self):
|
||||
"""Email requires no external authentication — always returns True."""
|
||||
return True
|
||||
|
||||
def channel_send_message(self, message):
|
||||
"""Send a mail.message via Odoo's standard mail.mail mechanism."""
|
||||
email_to = message.email_from
|
||||
if not email_to and message.author_id:
|
||||
email_to = message.author_id.email
|
||||
|
||||
mail_mail = self.env['mail.mail'].create({
|
||||
'subject': message.subject or '(no subject)',
|
||||
'body_html': message.body,
|
||||
'email_to': email_to,
|
||||
'email_from': self.env.company.email,
|
||||
})
|
||||
mail_mail.send()
|
||||
|
||||
def channel_receive_message(self, payload):
|
||||
"""Create a mail.message from an incoming payload dict.
|
||||
|
||||
Expected payload keys:
|
||||
body (str): message body HTML
|
||||
subject (str, optional): message subject
|
||||
email_from (str, optional): sender email address
|
||||
author_id (int, optional): res.partner id of the author
|
||||
"""
|
||||
vals = {
|
||||
'body': payload.get('body', ''),
|
||||
'subject': payload.get('subject', False),
|
||||
'email_from': payload.get('email_from', False),
|
||||
'message_type': 'email',
|
||||
}
|
||||
author_id = payload.get('author_id')
|
||||
if author_id:
|
||||
vals['author_id'] = author_id
|
||||
|
||||
return self.env['mail.message'].create(vals)
|
||||
44
tier_communications/models/tier_channel_mixin.py
Normal file
44
tier_communications/models/tier_channel_mixin.py
Normal file
@ -0,0 +1,44 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class TierChannelMixin(models.AbstractModel):
|
||||
_name = 'tier.channel.mixin'
|
||||
_description = 'Tier Channel Mixin'
|
||||
_abstract = True
|
||||
|
||||
credential_scope = fields.Selection(
|
||||
selection=[('company', 'Company'), ('user', 'User')],
|
||||
string='Credential Scope',
|
||||
default='company',
|
||||
required=True,
|
||||
)
|
||||
|
||||
def channel_authenticate(self):
|
||||
"""Authenticate with the channel. Returns True by default (e.g. Email needs no auth)."""
|
||||
return True
|
||||
|
||||
def channel_send_message(self, message):
|
||||
"""Send a message through this channel. Must be overridden by subclasses."""
|
||||
raise NotImplementedError(
|
||||
f"channel_send_message() is not implemented for {self._name}"
|
||||
)
|
||||
|
||||
def channel_receive_message(self, payload):
|
||||
"""Receive a message from this channel. Must be overridden by subclasses."""
|
||||
raise NotImplementedError(
|
||||
f"channel_receive_message() is not implemented for {self._name}"
|
||||
)
|
||||
|
||||
def _get_credentials(self):
|
||||
"""Return credentials based on credential_scope."""
|
||||
if self.credential_scope == 'user':
|
||||
return self._get_user_credentials(self.env.user)
|
||||
return self._get_company_credentials(self.env.company)
|
||||
|
||||
def _get_user_credentials(self, user):
|
||||
"""Return user-level credentials. Override in subclasses."""
|
||||
return {}
|
||||
|
||||
def _get_company_credentials(self, company):
|
||||
"""Return company-level credentials. Override in subclasses."""
|
||||
return {}
|
||||
27
tier_communications/models/tier_channel_tag.py
Normal file
27
tier_communications/models/tier_channel_tag.py
Normal file
@ -0,0 +1,27 @@
|
||||
from odoo import fields, models
|
||||
from odoo.addons.mail.tools.discuss import Store
|
||||
|
||||
|
||||
class TierChannelTag(models.Model):
|
||||
_name = 'tier.channel.tag'
|
||||
_description = 'Tier Channel Tag'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char(string='Name', required=True)
|
||||
code = fields.Char(string='Code', required=True)
|
||||
active = fields.Boolean(string='Active', default=True)
|
||||
description = fields.Text(string='Description')
|
||||
is_company_reply = fields.Boolean(
|
||||
string='Company Reply',
|
||||
default=False,
|
||||
help='If enabled, this channel is only available to users in the '
|
||||
'"Отправитель от имени компании" group.',
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
('code_unique', 'UNIQUE(code)', 'The channel tag code must be unique.'),
|
||||
]
|
||||
|
||||
def _to_store_defaults(self, target):
|
||||
"""Expose id and name to the JS store."""
|
||||
return ['id', 'name', 'code', 'is_company_reply']
|
||||
98
tier_communications/models/tier_mailbox.py
Normal file
98
tier_communications/models/tier_mailbox.py
Normal file
@ -0,0 +1,98 @@
|
||||
import re
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class TierMailbox(models.Model):
|
||||
_name = 'tier.mailbox'
|
||||
_description = 'Tier Mailbox'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char(string='Name', required=True)
|
||||
code = fields.Char(string='Code', required=True)
|
||||
description = fields.Text(string='Description')
|
||||
user_ids = fields.Many2many(
|
||||
'res.users',
|
||||
string='Allowed Users',
|
||||
help='Users who have access to this mailbox. If empty, all users can access it.',
|
||||
)
|
||||
is_lead_inbox = fields.Boolean(string='Lead Inbox', default=False)
|
||||
is_ticket_inbox = fields.Boolean(string='Ticket Inbox', default=False)
|
||||
|
||||
_sql_constraints = [
|
||||
('code_unique', 'UNIQUE(code)', 'The mailbox code must be unique.'),
|
||||
]
|
||||
|
||||
@api.model
|
||||
def get_accessible_mailboxes(self):
|
||||
"""Return mailboxes accessible to the current user.
|
||||
|
||||
A mailbox is accessible if the current user is in user_ids,
|
||||
or if user_ids is empty (open to all).
|
||||
"""
|
||||
uid = self.env.uid
|
||||
domain = [
|
||||
'|',
|
||||
('user_ids', '=', False),
|
||||
('user_ids', 'in', [uid]),
|
||||
]
|
||||
mailboxes = self.search(domain)
|
||||
return mailboxes.read(['id', 'name', 'code', 'is_lead_inbox', 'is_ticket_inbox'])
|
||||
|
||||
@api.model
|
||||
def get_mailbox_conversations(self, mailbox_id, limit=50):
|
||||
"""Return messages in a mailbox grouped by author (partner).
|
||||
|
||||
Returns a list of conversations:
|
||||
[{
|
||||
'partner_id': int,
|
||||
'partner_name': str,
|
||||
'partner_avatar_url': str,
|
||||
'last_message_preview': str, # first 80 chars of body text
|
||||
'last_message_date': str,
|
||||
'channel_code': str|None, # email, matrix, or None
|
||||
'message_count': int,
|
||||
}]
|
||||
"""
|
||||
mailbox_tags = self.env['tier.mailbox.tag'].search([
|
||||
('mailbox_id', '=', mailbox_id),
|
||||
], order='message_id desc', limit=500)
|
||||
|
||||
if not mailbox_tags:
|
||||
return []
|
||||
|
||||
message_ids = mailbox_tags.mapped('message_id')
|
||||
|
||||
# Group by author — show last message in conversation (ours or theirs)
|
||||
conversations_by_partner = {}
|
||||
for message in message_ids.sorted(key=lambda m: m.id, reverse=True):
|
||||
author = message.author_id
|
||||
partner_id = author.id if author else 0
|
||||
|
||||
if partner_id not in conversations_by_partner:
|
||||
# Strip HTML for preview
|
||||
body_text = re.sub(r'<[^>]+>', '', message.body or '')
|
||||
preview = body_text[:80].strip() if body_text else (message.subject or '')
|
||||
|
||||
# If preview is still empty, use subject
|
||||
if not preview and message.subject:
|
||||
preview = message.subject[:80]
|
||||
|
||||
# Get channel code
|
||||
channel_code = None
|
||||
if message.channel_tag_ids:
|
||||
channel_code = message.channel_tag_ids[0].code
|
||||
|
||||
conversations_by_partner[partner_id] = {
|
||||
'partner_id': partner_id,
|
||||
'partner_name': author.name if author else (message.email_from or 'Неизвестный'),
|
||||
'last_message_preview': preview or 'Нет текста',
|
||||
'last_message_date': str(message.date or message.create_date or ''),
|
||||
'channel_code': channel_code,
|
||||
'message_count': 0,
|
||||
}
|
||||
|
||||
conversations_by_partner[partner_id]['message_count'] += 1
|
||||
|
||||
result = list(conversations_by_partner.values())[:limit]
|
||||
return result
|
||||
20
tier_communications/models/tier_mailbox_tag.py
Normal file
20
tier_communications/models/tier_mailbox_tag.py
Normal file
@ -0,0 +1,20 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class TierMailboxTag(models.Model):
|
||||
_name = 'tier.mailbox.tag'
|
||||
_description = 'Mailbox Tag (Message-Mailbox link)'
|
||||
_rec_name = 'mailbox_id'
|
||||
|
||||
mailbox_id = fields.Many2one(
|
||||
'tier.mailbox',
|
||||
string='Mailbox',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
message_id = fields.Many2one(
|
||||
'mail.message',
|
||||
string='Message',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
73
tier_communications/models/tier_router.py
Normal file
73
tier_communications/models/tier_router.py
Normal file
@ -0,0 +1,73 @@
|
||||
class TierRouter:
|
||||
"""Plain Python router — not an Odoo model.
|
||||
|
||||
Handles channel routing for outgoing messages and inbox fallback for incoming.
|
||||
Mailbox assignment is now manual-only (via wizard).
|
||||
"""
|
||||
|
||||
def __init__(self, env):
|
||||
self.env = env
|
||||
|
||||
def route_outgoing(self, message, partner, source_model=None):
|
||||
"""Assign channel tag to an outgoing message based on routing rules.
|
||||
|
||||
Mailboxes are no longer auto-assigned — user does it manually.
|
||||
"""
|
||||
channel = self._resolve_channel(partner)
|
||||
if channel:
|
||||
message.channel_tag_ids = [(4, channel.id)]
|
||||
|
||||
def route_incoming(self, message):
|
||||
"""Route an incoming message.
|
||||
|
||||
If it's a reply — inherit mailboxes from parent.
|
||||
Otherwise — place in standard Odoo inbox.
|
||||
"""
|
||||
parent = message.parent_id
|
||||
if parent and parent.mailbox_tag_ids:
|
||||
for tag in parent.mailbox_tag_ids:
|
||||
self.env['tier.mailbox.tag'].create({
|
||||
'mailbox_id': tag.mailbox_id.id,
|
||||
'message_id': message.id,
|
||||
})
|
||||
else:
|
||||
self._fallback_inbox(message)
|
||||
|
||||
def _fallback_inbox(self, message):
|
||||
"""Place message in the standard Odoo inbox via mail.notification."""
|
||||
partner = self.env.user.partner_id
|
||||
if not partner:
|
||||
return
|
||||
existing = self.env['mail.notification'].sudo().search([
|
||||
('mail_message_id', '=', message.id),
|
||||
('res_partner_id', '=', partner.id),
|
||||
], limit=1)
|
||||
if not existing:
|
||||
self.env['mail.notification'].sudo().create({
|
||||
'mail_message_id': message.id,
|
||||
'res_partner_id': partner.id,
|
||||
'notification_type': 'inbox',
|
||||
'is_read': False,
|
||||
})
|
||||
|
||||
def _resolve_channel(self, partner):
|
||||
"""Return the best channel tag for partner from routing rules."""
|
||||
if not partner:
|
||||
return None
|
||||
|
||||
rule = self.env['tier.routing.channel'].search(
|
||||
[('partner_id', '=', partner.id)],
|
||||
order='priority desc',
|
||||
limit=1,
|
||||
)
|
||||
if not rule:
|
||||
return None
|
||||
|
||||
preferred = rule.preferred_channel_tag_id
|
||||
if preferred and preferred.active:
|
||||
return preferred
|
||||
|
||||
for tag in rule.channel_tag_ids.filtered('active'):
|
||||
return tag
|
||||
|
||||
return None
|
||||
24
tier_communications/models/tier_routing_channel.py
Normal file
24
tier_communications/models/tier_routing_channel.py
Normal file
@ -0,0 +1,24 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class TierRoutingChannel(models.Model):
|
||||
_name = 'tier.routing.channel'
|
||||
_description = 'Channel Routing Rule'
|
||||
|
||||
partner_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string='Partner',
|
||||
required=True,
|
||||
)
|
||||
channel_tag_ids = fields.Many2many(
|
||||
'tier.channel.tag',
|
||||
string='Allowed Channels',
|
||||
)
|
||||
preferred_channel_tag_id = fields.Many2one(
|
||||
'tier.channel.tag',
|
||||
string='Preferred Channel',
|
||||
)
|
||||
priority = fields.Integer(
|
||||
string='Priority',
|
||||
default=10,
|
||||
)
|
||||
21
tier_communications/models/tier_routing_mailbox.py
Normal file
21
tier_communications/models/tier_routing_mailbox.py
Normal file
@ -0,0 +1,21 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class TierRoutingMailbox(models.Model):
|
||||
_name = 'tier.routing.mailbox'
|
||||
_description = 'Mailbox Routing Rule'
|
||||
|
||||
model_id = fields.Many2one(
|
||||
'ir.model',
|
||||
string='Model',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
)
|
||||
mailbox_ids = fields.Many2many(
|
||||
'tier.mailbox',
|
||||
string='Target Mailboxes',
|
||||
)
|
||||
priority = fields.Integer(
|
||||
string='Priority',
|
||||
default=10,
|
||||
)
|
||||
Reference in New Issue
Block a user