Split FSM into separate modules and switch customer to address book
This commit is contained in:
7
odoo/addons/dsrpt_address_book/models/__init__.py
Normal file
7
odoo/addons/dsrpt_address_book/models/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import dsrpt_communication_type
|
||||
from . import dsrpt_contact
|
||||
from . import dsrpt_contact_communication
|
||||
from . import contact_event
|
||||
from . import contact_source
|
||||
125
odoo/addons/dsrpt_address_book/models/contact_event.py
Normal file
125
odoo/addons/dsrpt_address_book/models/contact_event.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import timedelta
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class ContactEvent(models.Model):
|
||||
_name = 'contact.event'
|
||||
_description = 'Contact Event'
|
||||
_order = 'date_start desc'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
contact_id = fields.Many2one(
|
||||
'dsrpt.contact',
|
||||
string='Contact',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
tracking=True
|
||||
)
|
||||
date_start = fields.Datetime(
|
||||
string='Date & Time',
|
||||
required=True,
|
||||
default=fields.Datetime.now,
|
||||
tracking=True
|
||||
)
|
||||
duration = fields.Float(
|
||||
string='Duration (hours)',
|
||||
default=1.0,
|
||||
tracking=True
|
||||
)
|
||||
user_id = fields.Many2one(
|
||||
'res.users',
|
||||
string='Responsible',
|
||||
default=lambda self: self.env.user,
|
||||
tracking=True
|
||||
)
|
||||
calendar_event_id = fields.Many2one(
|
||||
'calendar.event',
|
||||
string='Calendar Event',
|
||||
readonly=True
|
||||
)
|
||||
notes = fields.Text(string='Notes', required=True, tracking=True)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
for record in records:
|
||||
# Get base URL for creating contact link
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url', 'http://localhost:8069')
|
||||
contact_url = f"{base_url}/web#id={record.contact_id.id}&model=dsrpt.contact&view_type=form"
|
||||
|
||||
# Prepare description with contact link
|
||||
description = f"📱 Contact in Odoo: {contact_url}\n\n"
|
||||
if record.notes:
|
||||
description += record.notes
|
||||
|
||||
calendar_event = self.env['calendar.event'].create({
|
||||
'name': f"[{record.contact_id.name}] {record.notes[:50] if record.notes else 'Event'}",
|
||||
'start': record.date_start,
|
||||
'stop': record.date_start + timedelta(hours=record.duration),
|
||||
'user_id': record.user_id.id,
|
||||
'partner_ids': [(6, 0, [record.user_id.partner_id.id])] if record.user_id and record.user_id.partner_id else [],
|
||||
'description': description,
|
||||
})
|
||||
record.calendar_event_id = calendar_event.id
|
||||
|
||||
# Update next_contact for affected contacts with the event date
|
||||
for record in records:
|
||||
record.contact_id._update_next_contact(record.date_start)
|
||||
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
result = super().write(vals)
|
||||
|
||||
# Update calendar event if needed
|
||||
for record in self:
|
||||
if record.calendar_event_id:
|
||||
update_vals = {}
|
||||
if 'notes' in vals or 'contact_id' in vals:
|
||||
update_vals['name'] = f"[{record.contact_id.name}] {record.notes[:50] if record.notes else 'Event'}"
|
||||
if 'date_start' in vals:
|
||||
update_vals['start'] = record.date_start
|
||||
update_vals['stop'] = record.date_start + timedelta(hours=record.duration)
|
||||
if 'duration' in vals:
|
||||
update_vals['stop'] = record.date_start + timedelta(hours=record.duration)
|
||||
if 'user_id' in vals:
|
||||
update_vals['user_id'] = record.user_id.id
|
||||
update_vals['partner_ids'] = [(6, 0, [record.user_id.partner_id.id])] if record.user_id and record.user_id.partner_id else []
|
||||
if 'notes' in vals or 'contact_id' in vals:
|
||||
# Get base URL for creating contact link
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url', 'http://localhost:8069')
|
||||
contact_url = f"{base_url}/web#id={record.contact_id.id}&model=dsrpt.contact&view_type=form"
|
||||
|
||||
# Prepare description with contact link
|
||||
description = f"📱 Contact in Odoo: {contact_url}\n\n"
|
||||
if record.notes:
|
||||
description += record.notes
|
||||
update_vals['description'] = description
|
||||
|
||||
if update_vals:
|
||||
record.calendar_event_id.write(update_vals)
|
||||
|
||||
# Update next_contact for affected contacts if relevant fields changed
|
||||
if any(field in vals for field in ['date_start', 'contact_id']):
|
||||
for record in self:
|
||||
record.contact_id._update_next_contact(record.date_start)
|
||||
|
||||
return result
|
||||
|
||||
@api.ondelete(at_uninstall=False)
|
||||
def _unlink_calendar_events(self):
|
||||
for record in self:
|
||||
if record.calendar_event_id:
|
||||
record.calendar_event_id.unlink()
|
||||
|
||||
def unlink(self):
|
||||
# Store contacts before deletion for clearing next_contact
|
||||
contacts = self.mapped('contact_id')
|
||||
result = super().unlink()
|
||||
# Clear next_contact for affected contacts after deletion
|
||||
for contact in contacts:
|
||||
contact._update_next_contact(None)
|
||||
return result
|
||||
|
||||
31
odoo/addons/dsrpt_address_book/models/contact_source.py
Normal file
31
odoo/addons/dsrpt_address_book/models/contact_source.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
class ContactSource(models.Model):
|
||||
_name = 'contact.source'
|
||||
_description = 'Contact Source'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'name'
|
||||
_parent_store = True
|
||||
|
||||
name = fields.Char(string='Name', required=True, translate=True, tracking=True)
|
||||
description = fields.Text(string='Description', tracking=True)
|
||||
active = fields.Boolean(string='Active', default=True, tracking=True)
|
||||
parent_id = fields.Many2one('contact.source', string='Parent Source', ondelete='cascade', index=True, tracking=True)
|
||||
parent_path = fields.Char(index=True, unaccent=False)
|
||||
|
||||
# Стандартные статусы согласно требованиям
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('active', 'Active')
|
||||
], string='State', default='draft', required=True, tracking=True)
|
||||
color = fields.Integer(string='Color', default=0)
|
||||
|
||||
def action_activate(self):
|
||||
"""Активировать источник"""
|
||||
self.write({'state': 'active'})
|
||||
|
||||
def action_draft(self):
|
||||
"""Вернуть в черновик"""
|
||||
self.write({'state': 'draft'})
|
||||
@@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class CommunicationType(models.Model):
|
||||
_name = 'dsrpt.communication.type'
|
||||
_description = 'Communication Type'
|
||||
_order = 'name'
|
||||
|
||||
name = fields.Char(string='Name', required=True, translate=True)
|
||||
code = fields.Char(string='Code', required=True)
|
||||
active = fields.Boolean(string='Active', default=True)
|
||||
|
||||
# Стандартные статусы согласно требованиям
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('active', 'Active')
|
||||
], string='State', default='draft', required=True)
|
||||
|
||||
def action_activate(self):
|
||||
"""Активировать тип коммуникации"""
|
||||
self.write({'state': 'active'})
|
||||
|
||||
def action_draft(self):
|
||||
"""Вернуть в черновик"""
|
||||
self.write({'state': 'draft'})
|
||||
332
odoo/addons/dsrpt_address_book/models/dsrpt_contact.py
Normal file
332
odoo/addons/dsrpt_address_book/models/dsrpt_contact.py
Normal file
@@ -0,0 +1,332 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class Contact(models.Model):
|
||||
_name = 'dsrpt.contact'
|
||||
_description = 'Contact'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_rec_name = 'name'
|
||||
_order = 'create_date desc, name'
|
||||
|
||||
name = fields.Char(string='Name', required=True, tracking=True)
|
||||
display_name = fields.Char(string='Display Name') # Will be computed in realty_sales module
|
||||
active = fields.Boolean(string='Active', default=True, tracking=True)
|
||||
status = fields.Selection([
|
||||
('awaiting_qualification', 'Awaiting Qualification'),
|
||||
('not_customer', 'Not a Customer'),
|
||||
('supplier', 'Supplier'),
|
||||
('has_request', 'Has Request'),
|
||||
('has_contract', 'Has Contract'),
|
||||
('potential_investor', 'Potential Investor'),
|
||||
('cancelled', 'Cancelled')
|
||||
], string='Status', default='awaiting_qualification', tracking=True, help='Contact processing status for AI analysis')
|
||||
user_id = fields.Many2one('res.users', string='Responsible', tracking=True)
|
||||
source_id = fields.Many2one('contact.source', string='Source', tracking=True)
|
||||
qualification_date = fields.Datetime(string='Qualification Date', tracking=True, help='When contact was qualified')
|
||||
communication_ids = fields.One2many(
|
||||
'dsrpt.contact.communication',
|
||||
'contact_id',
|
||||
string='Communications'
|
||||
)
|
||||
note = fields.Text(string='Note')
|
||||
|
||||
# Relations
|
||||
event_ids = fields.One2many(
|
||||
'contact.event',
|
||||
'contact_id',
|
||||
string='Events'
|
||||
)
|
||||
# call_ids moved to dsrpt_calls module to avoid circular dependencies
|
||||
|
||||
# Computed fields
|
||||
event_count = fields.Integer(
|
||||
string='Events',
|
||||
compute='_compute_event_count'
|
||||
)
|
||||
phone_numbers = fields.Char(
|
||||
string='Phone Numbers',
|
||||
compute='_compute_phone_numbers',
|
||||
store=True
|
||||
)
|
||||
next_contact = fields.Datetime(
|
||||
string='Next Contact',
|
||||
tracking=True,
|
||||
help='Date of the nearest planned event'
|
||||
)
|
||||
# call_count moved to dsrpt_calls module
|
||||
|
||||
@api.depends('event_ids')
|
||||
def _compute_event_count(self):
|
||||
"""Counts the number of events"""
|
||||
for record in self:
|
||||
record.event_count = len(record.event_ids)
|
||||
|
||||
@api.depends('communication_ids.value', 'communication_ids.communication_type_id')
|
||||
def _compute_phone_numbers(self):
|
||||
"""Extracts all phone numbers for search functionality"""
|
||||
for record in self:
|
||||
phones = record.communication_ids.filtered(
|
||||
lambda c: c.communication_type_id.code == 'phone'
|
||||
).mapped('value')
|
||||
record.phone_numbers = ', '.join(phones) if phones else ''
|
||||
|
||||
def _update_next_contact(self, date_value=None):
|
||||
"""Update next_contact field with provided date"""
|
||||
for record in self:
|
||||
if date_value is not None:
|
||||
record.next_contact = date_value
|
||||
|
||||
# _compute_call_count moved to dsrpt_calls module
|
||||
|
||||
def action_view_events(self):
|
||||
"""Opens contact events"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': 'Events',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'contact.event',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('contact_id', '=', self.id)],
|
||||
'context': {'default_contact_id': self.id}
|
||||
}
|
||||
|
||||
def action_qualify(self):
|
||||
"""Action to trigger qualification for selected contacts"""
|
||||
qualified_count = 0
|
||||
no_calls_count = 0
|
||||
already_qualified_count = 0
|
||||
|
||||
for contact in self:
|
||||
# Skip if already qualified (status not 'awaiting_qualification')
|
||||
if contact.status != 'awaiting_qualification':
|
||||
already_qualified_count += 1
|
||||
continue
|
||||
|
||||
# Find last call with transcription for this contact
|
||||
last_call = self.env['dsrpt_calls.call'].search([
|
||||
('contact_id', '=', contact.id),
|
||||
('transcription', '!=', False)
|
||||
], order='date_start desc', limit=1)
|
||||
|
||||
if last_call:
|
||||
# Send on_need_qualification event directly to contact
|
||||
contact._event('on_need_qualification').notify(contact)
|
||||
qualified_count += 1
|
||||
else:
|
||||
no_calls_count += 1
|
||||
|
||||
# Build notification message
|
||||
message_parts = []
|
||||
if qualified_count:
|
||||
message_parts.append(f"{qualified_count} contacts queued for qualification")
|
||||
if no_calls_count:
|
||||
message_parts.append(f"{no_calls_count} contacts have no calls with transcription")
|
||||
if already_qualified_count:
|
||||
message_parts.append(f"{already_qualified_count} contacts already qualified")
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Qualification Process',
|
||||
'message': '. '.join(message_parts) if message_parts else 'No contacts to qualify',
|
||||
'type': 'success' if qualified_count else 'info',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
def action_view_calls(self):
|
||||
"""Opens contact calls"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': 'Calls',
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'dsrpt_calls.call',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('contact_id', '=', self.id)],
|
||||
'context': {'default_contact_id': self.id}
|
||||
}
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for record in self:
|
||||
# Найти предпочитаемый способ связи
|
||||
preferred = record.communication_ids.filtered('is_preferred')
|
||||
if preferred:
|
||||
name = f"{record.name} ({preferred[0].value})"
|
||||
else:
|
||||
name = record.name
|
||||
result.append((record.id, name))
|
||||
return result
|
||||
|
||||
def action_view_telegram_card(self):
|
||||
"""Open telegram card editing form"""
|
||||
self.ensure_one()
|
||||
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url', 'http://localhost:8069')
|
||||
card_url = f"{base_url}/telegram/contact-card/{self.id}"
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': card_url,
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def _process_name_suggestion_from_call(self, call_record):
|
||||
"""Queue job method to process name suggestion from call transcription"""
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
if not call_record.transcription:
|
||||
_logger.warning(f"No transcription available for call {call_record.id}")
|
||||
return
|
||||
|
||||
# Get AI collection and component
|
||||
collection = self.env['dsrpt.ai.collection'].search([], limit=1)
|
||||
if not collection:
|
||||
_logger.error("No AI collection found")
|
||||
return
|
||||
|
||||
work = collection.work_on(model_name='field.change.suggestion')
|
||||
ai_component = work.component(usage='field.change.suggestion')
|
||||
|
||||
# Get suggested name from AI
|
||||
suggested_name = ai_component.suggest_name_from_transcription(call_record.transcription)
|
||||
|
||||
# Only create suggestion if AI returned a valid name
|
||||
if suggested_name and suggested_name not in ['Не определено', 'Undefined', '', False, 'Unknown']:
|
||||
# Find telegram user for notification
|
||||
telegram_user = self._find_telegram_user_for_suggestions()
|
||||
|
||||
# Create field suggestion with AI result
|
||||
suggestion_vals = {
|
||||
'res_model': 'dsrpt.contact',
|
||||
'res_id': self.id,
|
||||
'field_name': 'name',
|
||||
'current_value': self.name,
|
||||
'suggested_value': suggested_name,
|
||||
'telegram_user_id': telegram_user.id if telegram_user else None
|
||||
}
|
||||
|
||||
suggestion = self.env['field.change.suggestion'].create(suggestion_vals)
|
||||
_logger.info(f"Created name suggestion {suggestion.id} with value '{suggested_name}' for contact {self.id}")
|
||||
|
||||
else:
|
||||
_logger.info(f"AI did not return valid name suggestion for contact {self.id}, skipping")
|
||||
|
||||
def _find_telegram_user_for_suggestions(self):
|
||||
"""Find telegram user for notifications"""
|
||||
telegram_user = None
|
||||
|
||||
# Priority: contact owner > first admin user
|
||||
if hasattr(self, 'user_id') and self.user_id:
|
||||
telegram_user = self.env['telegram.user'].search([
|
||||
('odoo_user_id', '=', self.user_id.id),
|
||||
('active', '=', True)
|
||||
], limit=1)
|
||||
|
||||
if not telegram_user:
|
||||
# Find first active telegram admin user
|
||||
telegram_user = self.env['telegram.user'].search([
|
||||
('active', '=', True)
|
||||
], limit=1)
|
||||
|
||||
return telegram_user
|
||||
|
||||
def _process_note_suggestion_from_context(self, context_record):
|
||||
"""Queue job method to process note suggestion from contact history"""
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Get AI collection and component
|
||||
collection = self.env['dsrpt.ai.collection'].search([], limit=1)
|
||||
if not collection:
|
||||
_logger.error("No AI collection found")
|
||||
return
|
||||
|
||||
work = collection.work_on(model_name='field.change.suggestion')
|
||||
ai_component = work.component(usage='field.change.suggestion')
|
||||
|
||||
# Gather contact history for AI
|
||||
history = self._gather_contact_history_for_ai()
|
||||
|
||||
# Get suggested summary from AI
|
||||
suggested_summary = ai_component.suggest_summary_from_history(history)
|
||||
|
||||
# Only create suggestion if AI returned a valid summary
|
||||
if suggested_summary and suggested_summary not in ['Не определено', 'Undefined', '', False]:
|
||||
# Find telegram user for notification
|
||||
telegram_user = self._find_telegram_user_for_suggestions()
|
||||
|
||||
# Create field suggestion with AI result
|
||||
suggestion_vals = {
|
||||
'res_model': 'dsrpt.contact',
|
||||
'res_id': self.id,
|
||||
'field_name': 'note',
|
||||
'current_value': self.note or '',
|
||||
'suggested_value': suggested_summary,
|
||||
'telegram_user_id': telegram_user.id if telegram_user else None
|
||||
}
|
||||
|
||||
suggestion = self.env['field.change.suggestion'].create(suggestion_vals)
|
||||
_logger.info(f"Created note suggestion {suggestion.id} with summary for contact {self.id}")
|
||||
|
||||
else:
|
||||
_logger.info(f"AI did not return valid note suggestion for contact {self.id}, skipping")
|
||||
|
||||
def _gather_contact_history_for_ai(self):
|
||||
"""Gather contact history for summary generation"""
|
||||
history_parts = []
|
||||
|
||||
# Add contact basic info
|
||||
history_parts.append(f"Contact: {self.name}")
|
||||
|
||||
# Add communications
|
||||
for comm in self.communication_ids:
|
||||
history_parts.append(f"{comm.communication_type_id.name}: {comm.value}")
|
||||
|
||||
# Add recent calls if module is installed
|
||||
if 'dsrpt_calls.call' in self.env:
|
||||
calls = self.env['dsrpt_calls.call'].search([
|
||||
('contact_id', '=', self.id)
|
||||
], limit=5, order='date_start desc')
|
||||
for call in calls:
|
||||
history_parts.append(f"Call {call.date_start}: {call.duration} sec")
|
||||
if call.transcription:
|
||||
history_parts.append(f"Summary: {call.transcription[:200]}...")
|
||||
|
||||
return "\n".join(history_parts)
|
||||
|
||||
def recompute_next_contact_all(self):
|
||||
"""Clear next_contact for all contacts (simplified logic)"""
|
||||
all_contacts = self.env['dsrpt.contact'].search([])
|
||||
all_contacts._update_next_contact(None)
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': 'Reset Completed',
|
||||
'message': f'"Next Contact" cleared for {len(all_contacts)} contacts',
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
|
||||
def write(self, vals):
|
||||
"""Override write to automatically set qualification_date when status changes"""
|
||||
# Check if status is being changed
|
||||
if 'status' in vals:
|
||||
for record in self:
|
||||
old_status = record.status
|
||||
new_status = vals['status']
|
||||
|
||||
# If changing FROM awaiting_qualification TO any other status
|
||||
if (old_status == 'awaiting_qualification' and
|
||||
new_status != 'awaiting_qualification' and
|
||||
not record.qualification_date):
|
||||
# Set qualification date automatically
|
||||
vals['qualification_date'] = fields.Datetime.now()
|
||||
|
||||
return super().write(vals)
|
||||
@@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class ContactCommunication(models.Model):
|
||||
_name = 'dsrpt.contact.communication'
|
||||
_description = 'Contact Communication'
|
||||
_order = 'is_preferred desc, id'
|
||||
|
||||
contact_id = fields.Many2one(
|
||||
'dsrpt.contact',
|
||||
string='Contact',
|
||||
required=True,
|
||||
ondelete='cascade'
|
||||
)
|
||||
communication_type_id = fields.Many2one(
|
||||
'dsrpt.communication.type',
|
||||
string='Type',
|
||||
required=True
|
||||
)
|
||||
value = fields.Char(string='Value', required=True)
|
||||
is_preferred = fields.Boolean(string='Preferred', default=False)
|
||||
|
||||
@api.constrains('is_preferred')
|
||||
def _check_single_preferred(self):
|
||||
"""Ensure only one preferred communication per contact"""
|
||||
for record in self:
|
||||
if record.is_preferred:
|
||||
# Проверяем есть ли другие preferred для этого контакта
|
||||
domain = [
|
||||
('contact_id', '=', record.contact_id.id),
|
||||
('is_preferred', '=', True),
|
||||
('id', '!=', record.id)
|
||||
]
|
||||
if self.search_count(domain) > 0:
|
||||
raise ValidationError(
|
||||
'Only one communication method can be marked as preferred per contact.'
|
||||
)
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
# Если это первый способ связи для контакта, делаем его preferred
|
||||
if 'contact_id' in vals and not vals.get('is_preferred'):
|
||||
existing = self.search_count([('contact_id', '=', vals['contact_id'])])
|
||||
if existing == 0:
|
||||
vals['is_preferred'] = True
|
||||
return super().create(vals)
|
||||
|
||||
def name_get(self):
|
||||
result = []
|
||||
for record in self:
|
||||
name = f"{record.communication_type_id.name}: {record.value}"
|
||||
if record.is_preferred:
|
||||
name = f"★ {name}"
|
||||
result.append((record.id, name))
|
||||
return result
|
||||
Reference in New Issue
Block a user