Compare commits
15 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c143ee4560 | ||
|
|
5d59bc82b2 | ||
|
|
5fd6364665 | ||
|
|
83dc8d346e | ||
|
|
b58ff52597 | ||
|
|
ea05972fee | ||
|
|
cf124a758f | ||
|
|
e17f32dfaf | ||
|
|
f7de7d31af | ||
|
|
ed021edc40 | ||
|
|
68dd96a5a8 | ||
|
|
bf4786be6f | ||
|
|
093418ec59 | ||
|
|
3080254b8c | ||
|
|
b797015c0e |
65 changed files with 2237 additions and 2 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
|
.aider*
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
# bemade-addons
|
# bemade-addons
|
||||||
Odoo Addons made by Bemade
|
Odoo Addons made by Bemade
|
||||||
|
|
||||||
|
|
|
||||||
1
bemade_helpdesk_mailcow_blacklist/__init__.py
Normal file
1
bemade_helpdesk_mailcow_blacklist/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
36
bemade_helpdesk_mailcow_blacklist/__manifest__.py
Normal file
36
bemade_helpdesk_mailcow_blacklist/__manifest__.py
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Helpdesk Mailcow Blacklist',
|
||||||
|
'version': '1.0.0',
|
||||||
|
'category': 'Administration',
|
||||||
|
'summary': 'Module for adding blacklist functionality to the helpdesk.',
|
||||||
|
'description': """
|
||||||
|
Helpdesk Mailcow Blacklist
|
||||||
|
==========================
|
||||||
|
|
||||||
|
This module extends the functionality of the Helpdesk and Mailcow Blacklist modules by adding an action to blacklist the sender of a Helpdesk ticket.
|
||||||
|
|
||||||
|
Main Features:
|
||||||
|
--------------
|
||||||
|
- Adds a button on Helpdesk tickets to blacklist the email sender.
|
||||||
|
- This button is only visible when an email is associated with the ticket.
|
||||||
|
- Blacklisting an email sender automatically moves the Helpdesk ticket to a new 'Spam' stage, which is marked as closed.
|
||||||
|
- The 'Spam' stage is automatically created by this module.
|
||||||
|
""",
|
||||||
|
'sequence': 10,
|
||||||
|
'license': 'GPL-3',
|
||||||
|
'author': 'Bemade',
|
||||||
|
'website': 'https://www.bemade.org',
|
||||||
|
'depends': ['helpdesk', 'bemade_mailcow_blacklist'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'data/helpdesk_stages.xml',
|
||||||
|
'views/helpdesk_ticket_views.xml',
|
||||||
|
],
|
||||||
|
'demo': [
|
||||||
|
'demo/demo.xml'
|
||||||
|
],
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'auto_install': False
|
||||||
|
}
|
||||||
10
bemade_helpdesk_mailcow_blacklist/data/helpdesk_stages.xml
Normal file
10
bemade_helpdesk_mailcow_blacklist/data/helpdesk_stages.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="helpdesk_stage_spam" model="helpdesk.stage">
|
||||||
|
<field name="name">Spam</field>
|
||||||
|
<field name="sequence">50</field>
|
||||||
|
<field name="is_closed">True</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
27
bemade_helpdesk_mailcow_blacklist/demo/demo.xml
Normal file
27
bemade_helpdesk_mailcow_blacklist/demo/demo.xml
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Demo User -->
|
||||||
|
<record id="demo_user_1" model="res.users">
|
||||||
|
<field name="name">Demo User</field>
|
||||||
|
<field name="login">demo_user</field>
|
||||||
|
<field name="password">demo_user</field>
|
||||||
|
<field name="email">demo_user@example.com</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Demo Helpdesk Team -->
|
||||||
|
<record id="demo_helpdesk_team_1" model="helpdesk.team">
|
||||||
|
<field name="name">Demo Helpdesk Team</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Demo Helpdesk Ticket -->
|
||||||
|
<record id="demo_helpdesk_ticket_1" model="helpdesk.ticket">
|
||||||
|
<field name="name">Demo Helpdesk Ticket</field>
|
||||||
|
<field name="team_id" ref="demo_helpdesk_team_1"/>
|
||||||
|
<field name="user_id" ref="demo_user_1"/>
|
||||||
|
<field name="partner_email">demo_sender@example.com</field>
|
||||||
|
<field name="partner_id" ref="base.res_partner_1"/>
|
||||||
|
<field name="description">This is a demo ticket for testing the blacklist feature.</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
1
bemade_helpdesk_mailcow_blacklist/models/__init__.py
Normal file
1
bemade_helpdesk_mailcow_blacklist/models/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import helpdesk_ticket
|
||||||
24
bemade_helpdesk_mailcow_blacklist/models/helpdesk_ticket.py
Normal file
24
bemade_helpdesk_mailcow_blacklist/models/helpdesk_ticket.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
from odoo import api, fields, models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
|
||||||
|
class HelpdeskTicket(models.Model):
|
||||||
|
_inherit = 'helpdesk.ticket'
|
||||||
|
|
||||||
|
def add_blacklist(self):
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
# Create a new blacklist record
|
||||||
|
blacklist = self.env['mailcow.blacklist'].create({
|
||||||
|
'email': self.email,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Assign 'spam' as a closed stage
|
||||||
|
spam_stage = self.env.ref('bemade_helpdesk_mailcow_blacklist.helpdesk_stage_spam')
|
||||||
|
|
||||||
|
if spam_stage:
|
||||||
|
self.stage_id = spam_stage.id
|
||||||
|
else:
|
||||||
|
raise UserError(_('The Spam stage does not exist.'))
|
||||||
|
|
||||||
|
return True
|
||||||
11
bemade_helpdesk_mailcow_blacklist/models/res_partner.py
Normal file
11
bemade_helpdesk_mailcow_blacklist/models/res_partner.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from odoo import models, api
|
||||||
|
|
||||||
|
class ResPartner(models.Model):
|
||||||
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def send_validation_email(self):
|
||||||
|
for partner in self:
|
||||||
|
if not partner.email_validated:
|
||||||
|
# Assuming you have a method called send_validation_email that sends the validation email.
|
||||||
|
partner.send_validation_email()
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_bemade_helpdesk_mailcow_blacklist,access_bemade_helpdesk_mailcow_blacklist,model_bemade_helpdesk_mailcow_blacklist,base.group_user,1,1,1,1
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_ticket_form_inherit_mailcow" model="ir.ui.view">
|
||||||
|
<field name="name">helpdesk.ticket.form.mailcow</field>
|
||||||
|
<field name="model">helpdesk.ticket</field>
|
||||||
|
<field name="inherit_id" ref="helpdesk.helpdesk_ticket_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<header position="inside">
|
||||||
|
<button name="action_add_blacklist" type="object" string="Add to Blacklist" class="oe_highlight" attrs="{'invisible': [('email_from', '=', False)]}"/>
|
||||||
|
</header>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
3
bemade_mailcow_blacklist/__init__.py
Normal file
3
bemade_mailcow_blacklist/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import models
|
||||||
45
bemade_mailcow_blacklist/__manifest__.py
Normal file
45
bemade_mailcow_blacklist/__manifest__.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Mailcow Integration',
|
||||||
|
'version': '1.0.0',
|
||||||
|
'category': 'Administration',
|
||||||
|
'summary': 'Module for integrating Mailcow email server with Odoo.',
|
||||||
|
'description': """
|
||||||
|
Mailcow Integration
|
||||||
|
===================
|
||||||
|
|
||||||
|
This module integrates the Mailcow email server with Odoo, providing a seamless email communication solution for your Odoo instance. It allows for syncing of mailboxes and email aliases from Mailcow to Odoo and vice versa.
|
||||||
|
|
||||||
|
Main Features:
|
||||||
|
--------------
|
||||||
|
* Synchronize Mailcow mailboxes with Odoo users.
|
||||||
|
* Synchronize Mailcow email aliases with Odoo.
|
||||||
|
* Configuration of Mailcow API credentials in Odoo settings.
|
||||||
|
* Automatically create and manage mailboxes and aliases in Mailcow when they are created in Odoo.
|
||||||
|
""",
|
||||||
|
'sequence': 10,
|
||||||
|
'license': 'GPL-3',
|
||||||
|
'author': 'Bemade',
|
||||||
|
'website': 'https://www.bemade.org',
|
||||||
|
'depends': ['hr', 'mail', 'bemade_user_password_bundle'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/res_config_settings_views.xml',
|
||||||
|
'views/mailcow_mailbox_views.xml',
|
||||||
|
'views/mailcow_alias_views.xml',
|
||||||
|
'views/mailcow_blacklist_views.xml',
|
||||||
|
'views/res_users_views.xml',
|
||||||
|
],
|
||||||
|
"assets": {
|
||||||
|
"web.assets_backend": [
|
||||||
|
"bemade_mailcow_blacklist/static/src/js/mailcow.js",
|
||||||
|
],
|
||||||
|
"web.assets_qweb": [
|
||||||
|
"bemade_mailcow_blacklist/static/src/xml/mailcow_templates.xml",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'demo': [],
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'auto_install': False
|
||||||
|
}
|
||||||
13
bemade_mailcow_blacklist/controllers/controllers.py
Normal file
13
bemade_mailcow_blacklist/controllers/controllers.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
|
||||||
|
class EmailValidationController(http.Controller):
|
||||||
|
|
||||||
|
@http.route('/email_validation/<int:partner_id>/<token>', type='http', auth='public', website=True)
|
||||||
|
def email_validation(self, partner_id, token):
|
||||||
|
partner = request.env['res.partner'].sudo().browse(partner_id)
|
||||||
|
if partner and partner.validation_token == token:
|
||||||
|
partner.sudo().write({'email_validated': True})
|
||||||
|
return "Email validated!"
|
||||||
|
else:
|
||||||
|
return "Invalid validation link."
|
||||||
30
bemade_mailcow_blacklist/demo/demo.xml
Normal file
30
bemade_mailcow_blacklist/demo/demo.xml
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!--
|
||||||
|
<record id="object0" model="bemade_mailcow_blacklist.bemade_mailcow_blacklist">
|
||||||
|
<field name="name">Object 0</field>
|
||||||
|
<field name="value">0</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="object1" model="bemade_mailcow_blacklist.bemade_mailcow_blacklist">
|
||||||
|
<field name="name">Object 1</field>
|
||||||
|
<field name="value">10</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="object2" model="bemade_mailcow_blacklist.bemade_mailcow_blacklist">
|
||||||
|
<field name="name">Object 2</field>
|
||||||
|
<field name="value">20</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="object3" model="bemade_mailcow_blacklist.bemade_mailcow_blacklist">
|
||||||
|
<field name="name">Object 3</field>
|
||||||
|
<field name="value">30</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="object4" model="bemade_mailcow_blacklist.bemade_mailcow_blacklist">
|
||||||
|
<field name="name">Object 4</field>
|
||||||
|
<field name="value">40</field>
|
||||||
|
</record>
|
||||||
|
-->
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
9
bemade_mailcow_blacklist/models/__init__.py
Normal file
9
bemade_mailcow_blacklist/models/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import mail_alias
|
||||||
|
from . import mailcow
|
||||||
|
from . import mailcow_alias
|
||||||
|
from . import mailcow_blacklist
|
||||||
|
from . import mailcow_mailbox
|
||||||
|
from . import res_config_settings
|
||||||
|
from . import res_users
|
||||||
30
bemade_mailcow_blacklist/models/mail_alias.py
Normal file
30
bemade_mailcow_blacklist/models/mail_alias.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
class MailAlias(models.Model):
|
||||||
|
_inherit = 'mail.alias'
|
||||||
|
|
||||||
|
mailcow_id = fields.One2many('mail.mailcow.alias', 'alias_id')
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
alias = super(MailAlias, self).create(vals)
|
||||||
|
|
||||||
|
alias_domain = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain"),
|
||||||
|
catchall_alias = self.env["ir.config_parameter"].sudo().get_param("mail.catchall.alias"),
|
||||||
|
|
||||||
|
alias_domain = alias_domain[0]
|
||||||
|
catchall_alias = catchall_alias[0]
|
||||||
|
|
||||||
|
if not alias_domain:
|
||||||
|
raise ValidationError(_("No catchall domain is set in the system parameters. Please set one and try again."))
|
||||||
|
mailcow_alias = self.env['mail.mailcow.alias'].search([('address', '=', alias.alias_name + '@' + alias_domain)])
|
||||||
|
if mailcow_alias:
|
||||||
|
mailcow_alias.write({'active': True})
|
||||||
|
else:
|
||||||
|
self.env['mail.mailcow.alias'].create({
|
||||||
|
'address': alias.alias_name + '@' + alias_domain,
|
||||||
|
'goto': catchall_alias + '@' + alias_domain,
|
||||||
|
'alias_id': alias.id,
|
||||||
|
})
|
||||||
|
return alias
|
||||||
64
bemade_mailcow_blacklist/models/mailcow.py
Normal file
64
bemade_mailcow_blacklist/models/mailcow.py
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class MailMailcow(models.AbstractModel):
|
||||||
|
_name = 'mail.mailcow'
|
||||||
|
_description = 'Mailcow API'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_credentials(self):
|
||||||
|
params = self.env['ir.config_parameter'].sudo()
|
||||||
|
|
||||||
|
base_url = params.get_param('mailcow.base_url'),
|
||||||
|
base_url = base_url[0]
|
||||||
|
api_key = params.get_param('mailcow.api_key'),
|
||||||
|
api_key = api_key[0]
|
||||||
|
|
||||||
|
if not base_url or not api_key:
|
||||||
|
_logger.error('No API key or base URL is set in the system parameters')
|
||||||
|
raise ValidationError(_("No API key or base URL is set in the system parameters. Please set one and try again."))
|
||||||
|
# return False
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'base_url': base_url,
|
||||||
|
'api_key': api_key
|
||||||
|
}
|
||||||
|
|
||||||
|
def api_request(self, endpoint, method='GET', data=None):
|
||||||
|
creds = self.get_credentials
|
||||||
|
if not creds:
|
||||||
|
return False
|
||||||
|
url = creds['base_url'] + endpoint
|
||||||
|
headers = {
|
||||||
|
'accept': 'application/json',
|
||||||
|
'X-API-Key': creds['api_key'],
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
if method == 'GET':
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
elif method == 'POST':
|
||||||
|
response = requests.post(url, headers=headers, json=data)
|
||||||
|
elif method == 'DELETE':
|
||||||
|
response = requests.delete(url, headers=headers)
|
||||||
|
elif method == 'PUT':
|
||||||
|
response = requests.put(url, headers=headers, json=data)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as error:
|
||||||
|
_logger.error('HTTP error occurred: %s', error)
|
||||||
|
return False
|
||||||
|
except Exception as error:
|
||||||
|
_logger.error('An error occurred: %s', error)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return response.json()
|
||||||
108
bemade_mailcow_blacklist/models/mailcow_alias.py
Normal file
108
bemade_mailcow_blacklist/models/mailcow_alias.py
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MailcowAlias(models.Model):
|
||||||
|
_name = 'mail.mailcow.alias'
|
||||||
|
_description = 'Mailcow Alias'
|
||||||
|
_inherit = ['mail.mailcow', 'mail.thread', 'mail.activity.mixin']
|
||||||
|
_rec_name = 'address'
|
||||||
|
|
||||||
|
address = fields.Char(string='Alias Address', required=True, tracking=True)
|
||||||
|
goto = fields.Char(string='Alias Destination', required=True, tracking=True)
|
||||||
|
active = fields.Boolean(string='Active', default=True, tracking=True)
|
||||||
|
catchall = fields.Boolean(string='Catchall', default=False, tracking=True)
|
||||||
|
alias_id = fields.Many2one(comodel_name='mail.alias', string='Alias')
|
||||||
|
create_date_mailcow = fields.Datetime(string='Created on Mailcow', readonly=True)
|
||||||
|
modify_date_mailcow = fields.Datetime(string='Modified on Mailcow', readonly=True)
|
||||||
|
mc_id = fields.Integer(string='Mailcow ID', readonly=True)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
('address_unique', 'UNIQUE(address)', 'The alias address must be unique!'),
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
alias = super(MailcowAlias, self).create(vals)
|
||||||
|
|
||||||
|
if 'mc_id' not in vals:
|
||||||
|
data = {
|
||||||
|
"active": bool(alias.active),
|
||||||
|
"address": alias.address,
|
||||||
|
"catchall": bool(alias.catchall),
|
||||||
|
"goto": alias.goto,
|
||||||
|
"private_comment": f"Created by {self.env.user.name} on {fields.Datetime.now()}",
|
||||||
|
"public_comment": "Alias created in Odoo"
|
||||||
|
}
|
||||||
|
result = self.env['mail.mailcow'].api_request('/api/v1/add/alias', 'POST', data)
|
||||||
|
if not result:
|
||||||
|
#pass
|
||||||
|
raise ValidationError("Failed to create alias on Mailcow server.")
|
||||||
|
|
||||||
|
return alias
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
for record in self:
|
||||||
|
endpoint = f"api/v1/delete/alias/{record.mc_id}"
|
||||||
|
result = self.env['mail.mailcow'].api_request(endpoint, 'POST')
|
||||||
|
if not result:
|
||||||
|
raise ValidationError(_("Failed to delete alias on Mailcow server."))
|
||||||
|
return super(MailcowAlias, self).unlink()
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
for record in self:
|
||||||
|
data = {
|
||||||
|
"attr": {
|
||||||
|
"active": str(int(vals.get('active', record.active))),
|
||||||
|
"address": vals.get('address', record.address),
|
||||||
|
"goto": vals.get('goto', record.goto),
|
||||||
|
"private_comment": f"Modified by {self.env.user.name} on {fields.Datetime.now()}",
|
||||||
|
"public_comment": f"Alias modified in Odoo. Changes: {vals}",
|
||||||
|
},
|
||||||
|
"items": [record.mc_id]
|
||||||
|
}
|
||||||
|
result = self.env['mail.mailcow'].api_request('/api/v1/edit/alias', 'POST', data)
|
||||||
|
if not result:
|
||||||
|
raise ValidationError(_("Failed to update alias on Mailcow server."))
|
||||||
|
|
||||||
|
return super(MailcowAlias, self).write(vals)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def sync_aliases(self):
|
||||||
|
"""
|
||||||
|
Synchronize the list of aliases from Mailcow server with Odoo.
|
||||||
|
For each alias fetched from Mailcow server, it tries to find a matching record
|
||||||
|
in Odoo. If it doesn't exist, it creates a new record.
|
||||||
|
"""
|
||||||
|
endpoint = '/api/v1/get/alias/all'
|
||||||
|
mailcow_aliases = self.api_request(endpoint)
|
||||||
|
|
||||||
|
if not mailcow_aliases:
|
||||||
|
return
|
||||||
|
|
||||||
|
for mc_alias in mailcow_aliases:
|
||||||
|
domain = mc_alias['domain']
|
||||||
|
|
||||||
|
alias_domain = self.env['ir.config_parameter'].sudo().get_param('mail.catchall.domain')
|
||||||
|
if domain == alias_domain:
|
||||||
|
alias = self.search([('mc_id', '=', mc_alias['id'])], limit=1)
|
||||||
|
if not alias:
|
||||||
|
self.create({
|
||||||
|
'address': mc_alias['address'],
|
||||||
|
'active': bool(mc_alias['active']),
|
||||||
|
'goto': mc_alias['goto'],
|
||||||
|
'mc_id': mc_alias['id'],
|
||||||
|
'create_date_mailcow': mc_alias['created'],
|
||||||
|
'modify_date_mailcow': mc_alias['modified'],
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
alias.write({
|
||||||
|
'address': mc_alias['address'],
|
||||||
|
'active': bool(mc_alias['active']),
|
||||||
|
'goto': mc_alias['goto'],
|
||||||
|
'create_date_mailcow': mc_alias['created'],
|
||||||
|
'modify_date_mailcow': mc_alias['modified'],
|
||||||
|
})
|
||||||
99
bemade_mailcow_blacklist/models/mailcow_blacklist.py
Normal file
99
bemade_mailcow_blacklist/models/mailcow_blacklist.py
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import models, fields, api
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class MailcowBlacklist(models.Model):
|
||||||
|
_name = 'mail.mailcow.blacklist'
|
||||||
|
_description = 'Mailcow Blacklist'
|
||||||
|
_inherit = ['mail.mailcow', 'mail.thread', 'mail.activity.mixin']
|
||||||
|
|
||||||
|
email = fields.Char(string='Email', required=True, tracking=True)
|
||||||
|
mc_id = fields.Integer(string='Mailcow ID', required=True, tracking=True)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
"""
|
||||||
|
Overridden create method to add the new blacklist entry to the Mailcow server.
|
||||||
|
"""
|
||||||
|
res = super().create(vals)
|
||||||
|
domain = self.env['ir.config_parameter'].sudo().get_param('mail.catchall.domain')
|
||||||
|
|
||||||
|
endpoint_add = '/api/v1/add/domain-policy'
|
||||||
|
endpoint_get_bl = f"/api/v1/get/policy_bl_domain/{domain}"
|
||||||
|
data = {
|
||||||
|
'domain': domain,
|
||||||
|
'object_from': res.email,
|
||||||
|
'object_list': 'bl'
|
||||||
|
}
|
||||||
|
res.api_request(endpoint_add, 'POST', data)
|
||||||
|
_logger.info(f'Added {vals["email"]} to Mailcow blacklist')
|
||||||
|
|
||||||
|
res.api_request(endpoint_get_bl, 'GET', None)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
"""
|
||||||
|
Overridden write method to update the blacklist entry on the Mailcow server.
|
||||||
|
"""
|
||||||
|
old_email = self.email
|
||||||
|
res = super().write(vals)
|
||||||
|
if 'email' in vals:
|
||||||
|
delete_endpoint = '/api/v1/delete/domain-policy'
|
||||||
|
add_endpoint = '/api/v1/add/domain-policy'
|
||||||
|
delete_data = {
|
||||||
|
'items': [old_email]
|
||||||
|
}
|
||||||
|
add_data = {
|
||||||
|
'items': [vals['email']]
|
||||||
|
}
|
||||||
|
self.api_request(delete_endpoint, 'POST', delete_data)
|
||||||
|
self.api_request(add_endpoint, 'POST', add_data)
|
||||||
|
_logger.info(f'Updated {old_email} to {vals["email"]} in Mailcow blacklist')
|
||||||
|
return res
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
"""
|
||||||
|
Overridden unlink method to remove the blacklist entry from the Mailcow server.
|
||||||
|
"""
|
||||||
|
for record in self:
|
||||||
|
endpoint = '/api/v1/delete/blacklist'
|
||||||
|
data = {
|
||||||
|
'items': [record.email]
|
||||||
|
}
|
||||||
|
record.api_request(endpoint, 'POST', data)
|
||||||
|
_logger.info(f'Removed {record.email} from Mailcow blacklist')
|
||||||
|
return super().unlink()
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def sync_blacklist(self):
|
||||||
|
"""
|
||||||
|
Function to sync Mailcow blacklist with Odoo's blacklist. It fetches the list from Mailcow
|
||||||
|
and updates Odoo's blacklist accordingly.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the sync is successful, False otherwise
|
||||||
|
"""
|
||||||
|
params = self.env['ir.config_parameter'].sudo()
|
||||||
|
domain = params.get_param('mail.catchall.domain')
|
||||||
|
endpoint = f'/api/v1/get/policy_bl_domain/{domain}'
|
||||||
|
try:
|
||||||
|
response = self.api_request(endpoint, 'GET')
|
||||||
|
if response:
|
||||||
|
for item in response:
|
||||||
|
existing = self.search([('mc_id', '=', item['prefid'])], limit=1)
|
||||||
|
if existing:
|
||||||
|
if existing.email != item['value']:
|
||||||
|
existing.write({'email': item['valuw']})
|
||||||
|
else:
|
||||||
|
self.create({
|
||||||
|
'email': item['value'],
|
||||||
|
'mc_id': item['prefid'],
|
||||||
|
})
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error('An error occurred while syncing blacklist: %s', str(e))
|
||||||
|
return False
|
||||||
143
bemade_mailcow_blacklist/models/mailcow_mailbox.py
Normal file
143
bemade_mailcow_blacklist/models/mailcow_mailbox.py
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
import logging
|
||||||
|
import secrets
|
||||||
|
import string
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class MailcowMailbox(models.Model):
|
||||||
|
_name = 'mail.mailcow.mailbox'
|
||||||
|
_inherit = ['mail.mailcow', 'mail.thread', 'mail.activity.mixin']
|
||||||
|
_description = 'Mailcow Mailbox'
|
||||||
|
|
||||||
|
def _default_domain(self):
|
||||||
|
return self.env["ir.config_parameter"].sudo().get_param("mail.catchall.domain")
|
||||||
|
|
||||||
|
name = fields.Char(tracking=True)
|
||||||
|
address = fields.Char(compute='_compute_address', store=True, readonly=True, tracking=True)
|
||||||
|
local_part = fields.Char(required=True, tracking=True)
|
||||||
|
domain = fields.Char(required=True, tracking=True, default=_default_domain)
|
||||||
|
active = fields.Boolean(default=True, tracking=True)
|
||||||
|
user_id = fields.Many2one('res.users', ondelete='cascade', tracking=True)
|
||||||
|
password = fields.Char(readonly=True, tracking=True)
|
||||||
|
|
||||||
|
@api.depends('local_part', 'domain')
|
||||||
|
def _compute_address(self):
|
||||||
|
for record in self:
|
||||||
|
record.address = f"{record.local_part}@{record.domain}"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def sync_mailboxes(self):
|
||||||
|
"""
|
||||||
|
Synchronize Mailcow mailboxes with Odoo
|
||||||
|
"""
|
||||||
|
endpoint = '/api/v1/get/mailbox/all'
|
||||||
|
data = self.api_request(endpoint)
|
||||||
|
domain = self.env['ir.config_parameter'].sudo().get_param('mail.catchall.domain')
|
||||||
|
if data:
|
||||||
|
for item in data:
|
||||||
|
if item['domain'] == domain:
|
||||||
|
mailbox = self.env['mail.mailcow.mailbox'].search([('address', '=', f"{item['local_part']}@{item['domain']}")])
|
||||||
|
if not mailbox:
|
||||||
|
self.create({
|
||||||
|
'name': item['name'],
|
||||||
|
'local_part': item['local_part'],
|
||||||
|
'domain': item['domain'],
|
||||||
|
'active': item['active'],
|
||||||
|
})
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
"""
|
||||||
|
Override the create function to create a Mailcow mailbox whenever a user is created in Odoo.
|
||||||
|
"""
|
||||||
|
password = secrets.token_hex(16)
|
||||||
|
vals['password'] = password
|
||||||
|
|
||||||
|
# Check if email exists on Mailcow
|
||||||
|
endpoint = f"/api/v1/get/mailbox/{vals['local_part']}@{vals['domain']}"
|
||||||
|
response = self.api_request(endpoint)
|
||||||
|
|
||||||
|
if not response:
|
||||||
|
# If email does not exist on Mailcow, create it
|
||||||
|
endpoint = '/api/v1/add/mailbox'
|
||||||
|
data = {
|
||||||
|
'local_part': vals['local_part'],
|
||||||
|
'domain': vals['domain'],
|
||||||
|
'name': vals['name'],
|
||||||
|
'password': password,
|
||||||
|
'password2': password,
|
||||||
|
'quota': "3072",
|
||||||
|
'active': "1",
|
||||||
|
'force_pw_update': "0",
|
||||||
|
'tls_enforce_in': "0",
|
||||||
|
'tls_enforce_out': "0",
|
||||||
|
}
|
||||||
|
self.api_request(endpoint, method='POST', data=data)
|
||||||
|
_logger.info(f"Mailbox {vals['local_part']}@{vals['domain']} has been created on Mailcow server")
|
||||||
|
|
||||||
|
return super().create(vals)
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
"""
|
||||||
|
Override the write function to update a Mailcow mailbox whenever a user is updated in Odoo.
|
||||||
|
"""
|
||||||
|
if 'active' in vals or 'local_part' in vals or 'domain' in vals:
|
||||||
|
endpoint = f'/api/v1/edit/mailbox/{self.address}'
|
||||||
|
data = {
|
||||||
|
'items': [self.address],
|
||||||
|
'attr': {
|
||||||
|
'active': '1' if vals.get('active', self.active) else '0',
|
||||||
|
'local_part': vals.get('local_part', self.local_part),
|
||||||
|
'domain': vals.get('domain', self.domain),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.api_request(endpoint, method='POST', data=data)
|
||||||
|
_logger.info(f'Mailbox {self.address} has been updated on Mailcow server')
|
||||||
|
|
||||||
|
return super().write(vals)
|
||||||
|
|
||||||
|
def unlink(self):
|
||||||
|
"""
|
||||||
|
Override the unlink function to delete a Mailcow mailbox whenever a user is deleted in Odoo.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for mailbox in self:
|
||||||
|
data = {
|
||||||
|
'username': mailbox.address
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint = f'/api/v1/delete/mailbox'
|
||||||
|
mailbox.api_request(endpoint, method='POST', data=data)
|
||||||
|
_logger.info(f'Mailbox {mailbox.address} has been deleted on Mailcow server')
|
||||||
|
|
||||||
|
return super().unlink()
|
||||||
|
|
||||||
|
def create_mailbox_for_user(self, user):
|
||||||
|
"""
|
||||||
|
Function to create a Mailcow mailbox for a new Odoo user.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
user (res.users): The newly created Odoo user
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
mail.mailcow.mailbox: The newly created Mailcow mailbox
|
||||||
|
"""
|
||||||
|
data = {
|
||||||
|
'local_part': user.login.split('@')[0],
|
||||||
|
'domain': user.login.split('@')[1],
|
||||||
|
'name': user.name,
|
||||||
|
'password': user.password,
|
||||||
|
}
|
||||||
|
endpoint = '/api/v1/add/mailbox'
|
||||||
|
self.api_request(endpoint, method='POST', data=data)
|
||||||
|
_logger.info(f'Mailbox for user {user.login} has been created on Mailcow server')
|
||||||
|
|
||||||
|
pw_bundle = seld.env['password.bundle'].search([('name', '=', user.name)])
|
||||||
|
self.env['password.key'].create_mailbox_for_user(res)
|
||||||
|
|
||||||
|
return self.create({
|
||||||
|
'address': user.login,
|
||||||
|
'user_id': user.id,
|
||||||
|
})
|
||||||
15
bemade_mailcow_blacklist/models/res_config_settings.py
Normal file
15
bemade_mailcow_blacklist/models/res_config_settings.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
mailcow_base_url = fields.Char(
|
||||||
|
string="Mailcow Base URL",
|
||||||
|
help="URL for the Mailcow server API",
|
||||||
|
config_parameter='mailcow.base_url',
|
||||||
|
)
|
||||||
|
mailcow_api_key = fields.Char(
|
||||||
|
string="Mailcow API Key",
|
||||||
|
help="API key for the Mailcow server",
|
||||||
|
config_parameter='mailcow.api_key',
|
||||||
|
)
|
||||||
15
bemade_mailcow_blacklist/models/res_users.py
Normal file
15
bemade_mailcow_blacklist/models/res_users.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
class ResUsers(models.Model):
|
||||||
|
_inherit = 'res.users'
|
||||||
|
|
||||||
|
mailcow_mailbox = fields.Boolean(string='Mailcow Mailbox', default=False)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
res = super(ResUsers, self).create(vals)
|
||||||
|
|
||||||
|
if vals.get('mailcow_mailbox', false):
|
||||||
|
self.env['mail.mailcow.mailbox'].create_mailbox_for_user(res)
|
||||||
|
|
||||||
|
return res
|
||||||
7
bemade_mailcow_blacklist/security/ir.model.access.csv
Normal file
7
bemade_mailcow_blacklist/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_mail_mailcow_alias_all,access_mail_mailcow_alias,model_mail_mailcow_alias,base.group_user,1,0,0,0
|
||||||
|
access_mail_mailcow_alias_admin,access_mail_mailcow_alias,model_mail_mailcow_alias,base.group_system,1,1,1,1
|
||||||
|
access_mail_mailcow_mailbox_all,access_mail_mailcow_mailbox,model_mail_mailcow_mailbox,base.group_user,1,0,0,0
|
||||||
|
access_mail_mailcow_mailbox_admin,access_mail_mailcow_mailbox,model_mail_mailcow_mailbox,base.group_system,1,1,1,1
|
||||||
|
access_mail_mailcow_blacklist_all,access_mail_mailcow_blacklist,model_mail_mailcow_blacklist,base.group_user,1,0,0,0
|
||||||
|
access_mail_mailcow_blacklist_admin,access_mail_mailcow_blacklist,model_mail_mailcow_blacklist,base.group_system,1,1,1,1
|
||||||
|
BIN
bemade_mailcow_blacklist/static/description/icon.png
Normal file
BIN
bemade_mailcow_blacklist/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
187
bemade_mailcow_blacklist/static/description/logomailcow.svg
Normal file
187
bemade_mailcow_blacklist/static/description/logomailcow.svg
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
width="271.27399"
|
||||||
|
height="298.871"
|
||||||
|
viewBox="0 0 271.27398 298.871"
|
||||||
|
enable-background="new 0 0 1600 1200"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
|
||||||
|
sodipodi:docname="logomailcow.svg"
|
||||||
|
inkscape:export-filename="logomailcow.png"
|
||||||
|
inkscape:export-xdpi="96"
|
||||||
|
inkscape:export-ydpi="96"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata
|
||||||
|
id="metadata144"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
|
||||||
|
id="defs142" /><sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="1203"
|
||||||
|
inkscape:window-height="1168"
|
||||||
|
id="namedview140"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.1125147"
|
||||||
|
inkscape:cx="119.09955"
|
||||||
|
inkscape:cy="137.52627"
|
||||||
|
inkscape:window-x="1223"
|
||||||
|
inkscape:window-y="1133"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="g5"
|
||||||
|
fit-margin-top="0"
|
||||||
|
fit-margin-left="0"
|
||||||
|
fit-margin-right="0"
|
||||||
|
fit-margin-bottom="0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1" /><g
|
||||||
|
id="g3"
|
||||||
|
transform="translate(-648.292,-401.988)"><g
|
||||||
|
id="g5"><g
|
||||||
|
id="g3618"
|
||||||
|
transform="matrix(0.94035712,0,0,0.94035712,46.755777,32.888487)"><g
|
||||||
|
id="email"
|
||||||
|
transform="translate(0,-58)"><path
|
||||||
|
style="fill:#5a3620"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path10"
|
||||||
|
d="m 890.306,557.81 29.26,11.373 V 741.21 c 0,9.753 -7.895,17.649 -17.638,17.649 H 665.93 c -9.743,0 -17.638,-7.896 -17.638,-17.649 V 569.184 l 29.259,-8.937" /><path
|
||||||
|
style="fill:#fee70f;fill-opacity:0.895"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path12"
|
||||||
|
d="M 758.871,656.221 649.49,747.45 c 2.507,6.648 8.901,11.409 16.44,11.409 h 235.998 c 7.536,0 13.933,-4.761 16.444,-11.409 L 810.97,656.221 Z" /><g
|
||||||
|
id="g14"><path
|
||||||
|
style="fill:#f9e82d;fill-opacity:1"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path16"
|
||||||
|
d="m 810.391,656.686 107.981,90.764 c -0.331,0.881 -0.744,1.726 -1.205,2.536 l 0.028,0.035 c 1.501,-2.596 2.371,-5.594 2.371,-8.81 V 569.207 Z" /><path
|
||||||
|
style="fill:#f9e82d;fill-opacity:1"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path18"
|
||||||
|
d="M 649.49,747.45 758.354,656.686 648.293,569.207 V 741.21 c 0,3.216 0.876,6.214 2.367,8.81 l 0.039,-0.035 c -0.466,-0.809 -0.877,-1.654 -1.209,-2.535 z" /></g></g><path
|
||||||
|
style="opacity:0.1;fill:#3d5263"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path26"
|
||||||
|
d="m 783.931,446.247 c -66.396,0 -120.223,53.827 -120.223,120.223 0,66.396 53.827,120.221 120.223,120.221 66.397,0 120.222,-53.825 120.222,-120.221 0,-66.395 -53.825,-120.223 -120.222,-120.223 z m -11.96,215.702 c -53.009,0 -95.982,-43.855 -95.982,-97.953 0,-54.098 42.973,-97.952 95.982,-97.952 53.007,0 95.98,43.855 95.98,97.952 -10e-4,54.098 -42.973,97.953 -95.98,97.953 z" /><g
|
||||||
|
id="g28"><g
|
||||||
|
id="g30"><polyline
|
||||||
|
style="fill:#3d5263"
|
||||||
|
id="polyline32"
|
||||||
|
points="691.144,492.5 673.257,540.276 686.55,605.582 712.496,631.852 " /><g
|
||||||
|
id="g34"><g
|
||||||
|
id="g36"><polyline
|
||||||
|
style="fill:#fef3df"
|
||||||
|
id="polyline38"
|
||||||
|
points="658.248,450.81 673.501,487.076 693.836,496.903 724.04,458.731 " /><g
|
||||||
|
id="g40"><path
|
||||||
|
style="fill:#b58765"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path42"
|
||||||
|
d="m 710.634,473.205 c 0,0 -22.482,-25.556 -49.793,-18.975 0,0 4.667,34.118 46.349,44.019 l 2.61,8.533 c 0,0 -65.612,-9.689 -59.339,-67.593 0,0 49.008,-19.884 72.598,15.106" /><polyline
|
||||||
|
style="fill:#fef3df"
|
||||||
|
id="polyline44"
|
||||||
|
points="909.697,450.81 894.447,487.076 874.114,496.903 843.907,458.731 " /><path
|
||||||
|
style="fill:#b58765"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path46"
|
||||||
|
d="m 857.314,473.205 c 0,0 22.48,-25.556 49.79,-18.975 0,0 -4.664,34.118 -46.347,44.019 l -2.613,8.533 c 0,0 65.611,-9.689 59.339,-67.593 0,0 -49.006,-19.884 -72.6,15.106" /></g></g><path
|
||||||
|
sodipodi:nodetypes="cccscccccc"
|
||||||
|
style="fill:#b58765"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path48"
|
||||||
|
d="m 726.619,647.067 h 55.945 l 16.40428,-204.81407 c -55.814,0 -112.41728,30.01707 -112.41728,77.85207 0,1.454 0.085,2.787 0.121,4.175 0.127,3.934 0.448,7.585 0.856,11.135 1.689,14.816 5.451,27.177 8.461,43.383 1.452,7.831 5.002,23.374 5.002,23.374 0.056,0.408 0.165,0.804 0.224,1.211 2.535,16.546 11.832,32.027 25.404,43.684 z" /><path
|
||||||
|
style="fill:#b58765"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path50"
|
||||||
|
d="m 781.992,433.489 v 213.577 h 55.944 c 13.572,-11.657 22.867,-27.138 25.406,-43.684 0.058,-0.407 0.163,-0.803 0.221,-1.211 0,0 3.549,-15.543 5.002,-23.374 3.011,-16.206 6.774,-28.567 8.464,-43.381 0.405,-3.552 0.724,-7.203 0.846,-11.137 0.042,-1.388 0.126,-2.721 0.126,-4.175 0,-47.834 -40.191,-86.615 -96.009,-86.615 z" /><g
|
||||||
|
id="g52"><g
|
||||||
|
id="g54"><path
|
||||||
|
style="fill:#fef3df"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path56"
|
||||||
|
d="m 860.944,613.502 c 0,28.321 -35.091,51.289 -78.383,51.289 -43.299,0 -78.388,-22.968 -78.388,-51.289 0,-28.325 35.089,-51.289 78.388,-51.289 43.292,0 78.383,22.964 78.383,51.289 z" /></g></g><g
|
||||||
|
id="g58"><g
|
||||||
|
id="g60"><g
|
||||||
|
id="g62"><path
|
||||||
|
style="fill:#5a3620"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path64"
|
||||||
|
d="m 747.044,605.582 c 0,6.215 -5.04,11.256 -11.261,11.256 -6.21,0 -11.253,-5.041 -11.253,-11.256 0,-6.223 5.043,-11.257 11.253,-11.257 6.22,0 11.261,5.034 11.261,11.257 z" /></g></g><g
|
||||||
|
id="g66"><g
|
||||||
|
id="g68"><path
|
||||||
|
style="fill:#5a3620"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path70"
|
||||||
|
d="m 840.856,605.582 c 0,6.215 -5.037,11.256 -11.257,11.256 -6.218,0 -11.259,-5.041 -11.259,-11.256 0,-6.223 5.041,-11.257 11.259,-11.257 6.22,0 11.257,5.034 11.257,11.257 z" /></g></g></g><g
|
||||||
|
id="g72"><path
|
||||||
|
style="fill:#87654a"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path74"
|
||||||
|
d="m 875.228,525.835 c 0.354,-3.113 0.634,-6.311 0.743,-9.754 0.035,-1.218 0.109,-2.384 0.109,-3.661 0,-40.785 -33.369,-74.043 -80.237,-75.775 l -7.335,0.005 c -0.003,0 -0.003,0 -0.006,0 -0.007,0.018 -28.632,88.422 76.583,140.268 0.946,-4.317 2.078,-9.585 2.73,-13.088 2.64,-14.196 5.934,-25.021 7.413,-37.995 z" /></g><g
|
||||||
|
id="g76"><g
|
||||||
|
id="g78"><g
|
||||||
|
id="g80"><g
|
||||||
|
id="g82"><path
|
||||||
|
style="fill:#5a3620"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path84"
|
||||||
|
d="m 843.907,519.681 c 0,6.964 -5.65,12.611 -12.618,12.611 -6.963,0 -12.614,-5.646 -12.614,-12.611 0,-6.97 5.651,-12.614 12.614,-12.614 6.968,0 12.618,5.644 12.618,12.614 z" /></g></g></g><g
|
||||||
|
id="g86"><g
|
||||||
|
id="g88"><g
|
||||||
|
id="g90"><path
|
||||||
|
style="fill:#5a3620"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path92"
|
||||||
|
d="m 752.028,519.681 c 0,6.964 -5.649,12.611 -12.612,12.611 -6.969,0 -12.612,-5.646 -12.612,-12.611 0,-6.97 5.642,-12.614 12.612,-12.614 6.964,0 12.612,5.644 12.612,12.614 z" /></g></g></g><g
|
||||||
|
id="g94"><g
|
||||||
|
id="g96"><path
|
||||||
|
style="fill:#ffffff"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path98"
|
||||||
|
d="m 748.75,515.894 c 0,2.558 -2.071,4.629 -4.63,4.629 -2.558,0 -4.633,-2.072 -4.633,-4.629 0,-2.552 2.076,-4.626 4.633,-4.626 2.559,0 4.63,2.073 4.63,4.626 z" /></g></g><g
|
||||||
|
id="g100"><g
|
||||||
|
id="g102"><path
|
||||||
|
style="fill:#ffffff"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path104"
|
||||||
|
d="m 839.771,515.894 c 0,2.558 -2.073,4.629 -4.629,4.629 -2.558,0 -4.631,-2.072 -4.631,-4.629 0,-2.552 2.072,-4.626 4.631,-4.626 2.555,0 4.629,2.073 4.629,4.626 z" /></g></g></g></g><path
|
||||||
|
style="fill:#fef3df"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path106"
|
||||||
|
d="m 734.557,443.625 c 0,0 -18.236,-25.199 0,-41.637 0,0 13.125,32.012 40.242,31.502" /><path
|
||||||
|
style="fill:#fef3df"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path108"
|
||||||
|
d="m 834.496,443.625 c 0,0 18.236,-25.199 0,-41.637 0,0 -13.126,32.012 -40.242,31.502" /><path
|
||||||
|
style="fill:#f1f2f2"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path110"
|
||||||
|
d="m 786.264,431.965 c -66.396,0 -120.223,53.827 -120.223,120.223 0,66.396 53.827,120.221 120.223,120.221 66.397,0 120.222,-53.825 120.222,-120.221 10e-4,-66.395 -53.825,-120.223 -120.222,-120.223 z m -11.96,215.702 c -53.009,0 -95.982,-43.855 -95.982,-97.953 0,-54.098 42.973,-97.952 95.982,-97.952 53.007,0 95.979,43.855 95.979,97.952 0,54.098 -42.972,97.953 -95.979,97.953 z" /></g><g
|
||||||
|
id="g112"><path
|
||||||
|
style="fill:#ffffff"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path114"
|
||||||
|
d="m 781.737,436.751 c 66.396,0 120.221,53.827 120.221,120.223 0,30.718 -11.526,58.74 -30.482,79.991 21.636,-21.74 35.01,-51.708 35.01,-84.803 0,-66.395 -53.825,-120.222 -120.222,-120.222 -35.678,0 -67.721,15.549 -89.739,40.233 21.772,-21.879 51.91,-35.422 85.212,-35.422 z" /></g></g><path
|
||||||
|
d="m 648.292,644.7595 v 46.15088 c 0,5.49435 7.88,9.94862 17.6,9.94862 h 236.073 c 9.72,0 17.6,-4.45427 17.6,-9.94862 V 676.8342 c 10e-4,0 -175.814,20.0804 -271.273,-32.0747 z"
|
||||||
|
id="path124"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
style="opacity:0.1;fill:#3d5263" /></g></g><g
|
||||||
|
id="g126" /></g></svg>
|
||||||
|
After Width: | Height: | Size: 11 KiB |
89
bemade_mailcow_blacklist/static/src/js/mailcow.js
Normal file
89
bemade_mailcow_blacklist/static/src/js/mailcow.js
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import ListController from 'web.ListController';
|
||||||
|
import ListView from 'web.ListView';
|
||||||
|
const viewRegistry = require('web.view_registry');
|
||||||
|
|
||||||
|
const AliasesListController = ListController.extend({
|
||||||
|
// buttons_template must match the t-name on the template for the button (static xml)
|
||||||
|
buttons_template: 'mail.mailcow_aliases_list_view_buttons',
|
||||||
|
events: _.extend({}, ListController.prototype.events, {
|
||||||
|
'click .o_button_sync_aliases': '_onSyncAliases',
|
||||||
|
}),
|
||||||
|
// This may need to be async function() if there needs to be an await this._rpc({ ... }); call to not reload early
|
||||||
|
_onSyncAliases: function () {
|
||||||
|
this._rpc({
|
||||||
|
model: 'mail.mailcow.alias',
|
||||||
|
method: 'sync_aliases',
|
||||||
|
args: [],
|
||||||
|
}).then(() => {
|
||||||
|
this.reload();
|
||||||
|
});
|
||||||
|
// Couldn't test this for real, but it runs the action. May need a this.reload()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const AliasesListView = ListView.extend({
|
||||||
|
config: _.extend({}, ListView.prototype.config, {
|
||||||
|
Controller: AliasesListController,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// key must match with the js_class attribute of the tree view you want to modify
|
||||||
|
viewRegistry.add('mail_mailcow_aliases_tree', AliasesListView);
|
||||||
|
|
||||||
|
const BlacklistListController = ListController.extend({
|
||||||
|
// buttons_template must match the t-name on the template for the button (static xml)
|
||||||
|
buttons_template: 'mail.mailcow_blacklist_list_view_buttons',
|
||||||
|
events: _.extend({}, ListController.prototype.events, {
|
||||||
|
'click .o_button_sync_blacklist': '_onSyncBlacklist',
|
||||||
|
}),
|
||||||
|
// This may need to be async function() if there needs to be an await this._rpc({ ... }); call to not reload early
|
||||||
|
_onSyncBlacklist: function () {
|
||||||
|
this._rpc({
|
||||||
|
model: 'mail.mailcow.blacklist',
|
||||||
|
method: 'sync_blacklist',
|
||||||
|
args: [],
|
||||||
|
}).then(() => {
|
||||||
|
this.reload();
|
||||||
|
});
|
||||||
|
// Couldn't test this for real, but it runs the action. May need a this.reload()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const BlacklistListView = ListView.extend({
|
||||||
|
config: _.extend({}, ListView.prototype.config, {
|
||||||
|
Controller: BlacklistListController,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// key must match with the js_class attribute of the tree view you want to modify
|
||||||
|
viewRegistry.add('mail_mailcow_blacklist_tree', BlacklistListView);
|
||||||
|
|
||||||
|
const MailboxesListController = ListController.extend({
|
||||||
|
// buttons_template must match the t-name on the template for the button (static xml)
|
||||||
|
buttons_template: 'mail.mailcow_mailbox_list_view_buttons',
|
||||||
|
events: _.extend({}, ListController.prototype.events, {
|
||||||
|
'click .o_button_sync_mailboxes': '_onSyncMailboxes',
|
||||||
|
}),
|
||||||
|
// This may need to be async function() if there needs to be an await this._rpc({ ... }); call to not reload early
|
||||||
|
_onSyncMailboxes: function () {
|
||||||
|
this._rpc({
|
||||||
|
model: 'mail.mailcow.mailbox',
|
||||||
|
method: 'sync_mailboxes',
|
||||||
|
args: [],
|
||||||
|
}).then(() => {
|
||||||
|
this.reload();
|
||||||
|
});
|
||||||
|
// Couldn't test this for real, but it runs the action. May need a this.reload()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const MailboxesListView = ListView.extend({
|
||||||
|
config: _.extend({}, ListView.prototype.config, {
|
||||||
|
Controller: MailboxesListController,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// key must match with the js_class attribute of the tree view you want to modify
|
||||||
|
viewRegistry.add('mail_mailcow_mailbox_tree', MailboxesListView);
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates>
|
||||||
|
|
||||||
|
<t t-extend="ListView.buttons" t-name="mail.mailcow_aliases_list_view_buttons"> <!-- t-name must match with js controller button template -->
|
||||||
|
<t t-jquery="button.o_list_button_add" t-operation="after">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-primary ml4 o_button_sync_aliases"
|
||||||
|
title="Sync Aliases"> Sync Aliases </button>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-extend="ListView.buttons" t-name="mail.mailcow_blacklist_list_view_buttons"> <!-- t-name must match with js controller button template -->
|
||||||
|
<t t-jquery="button.o_list_button_add" t-operation="after">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-primary ml4 o_button_sync_blacklist"
|
||||||
|
title="Sync Blacklist"> Sync Blacklist </button>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-extend="ListView.buttons" t-name="mail.mailcow_mailbox_list_view_buttons"> <!-- t-name must match with js controller button template -->
|
||||||
|
<t t-jquery="button.o_list_button_add" t-operation="after">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-primary ml4 o_button_sync_mailboxes"
|
||||||
|
title="Sync Mailboxes"> Sync Mailboxes </button>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
1
bemade_mailcow_blacklist/tests/__init__.py
Normal file
1
bemade_mailcow_blacklist/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import test_mailcow
|
||||||
12
bemade_mailcow_blacklist/tests/test_mailcow.py
Normal file
12
bemade_mailcow_blacklist/tests/test_mailcow.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
from odoo.tests.common import TransactionCase, tagged
|
||||||
|
|
||||||
|
|
||||||
|
@tagged('-at_install', 'post_install')
|
||||||
|
class TestMailcow(TransactionCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
|
||||||
|
def test_new_mail_alias(self):
|
||||||
|
model_id = self.env['ir.model']._get('res.partner').id
|
||||||
|
self.alias = self.env['mail.alias'].create({'alias_name': 'test', 'alias_model_id': model_id})
|
||||||
76
bemade_mailcow_blacklist/views/mailcow_alias_views.xml
Normal file
76
bemade_mailcow_blacklist/views/mailcow_alias_views.xml
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="mailcow_alias_view_tree" model="ir.ui.view">
|
||||||
|
<field name="name">mailcow.alias.view.tree</field>
|
||||||
|
<field name="model">mail.mailcow.alias</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Alias" decoration-info="active == False" js_class="mail_mailcow_aliases_tree">
|
||||||
|
<field name="address"/>
|
||||||
|
<field name="alias_id"/>
|
||||||
|
<field name="goto"/>
|
||||||
|
<field name="create_date_mailcow"/>
|
||||||
|
<field name="modify_date_mailcow"/>
|
||||||
|
<field name="mc_id"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mailcow_alias_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">mailcow.alias.view.form</field>
|
||||||
|
<field name="model">mail.mailcow.alias</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Alias">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="active"/>
|
||||||
|
<field name="address"/>
|
||||||
|
<field name="goto"/>
|
||||||
|
<field name="alias_id"/>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Messages" name="messages">
|
||||||
|
<field name="message_ids" widget="mail_thread"/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_mailcow_alias" model="ir.actions.act_window">
|
||||||
|
<field name="name">Aliases</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="res_model">mail.mailcow.alias</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Create a new Alias
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Add the details of the new Alias.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="mailcow_menu_alias"
|
||||||
|
name="Aliases"
|
||||||
|
parent="mailcow_menu"
|
||||||
|
action="action_mailcow_alias"
|
||||||
|
sequence="20"/>
|
||||||
|
|
||||||
|
<record id="action_server_sync_aliases" model="ir.actions.server">
|
||||||
|
<field name="name">Sync Aliases</field>
|
||||||
|
<field name="model_id" ref="model_mail_mailcow_alias"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.sync_aliases()</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_sync_aliases" model="ir.actions.act_window.view">
|
||||||
|
<field name="sequence" eval="10"/>
|
||||||
|
<field name="view_mode">tree</field>
|
||||||
|
<field name="act_window_id" ref="action_mailcow_alias"/>
|
||||||
|
<field name="view_id" ref="mailcow_alias_view_tree"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
66
bemade_mailcow_blacklist/views/mailcow_blacklist_views.xml
Normal file
66
bemade_mailcow_blacklist/views/mailcow_blacklist_views.xml
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="mailcow_blacklist_view_tree" model="ir.ui.view">
|
||||||
|
<field name="name">mailcow.blacklist.view.tree</field>
|
||||||
|
<field name="model">mail.mailcow.blacklist</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Blacklist" js_class="mail_mailcow_blacklist_tree">
|
||||||
|
<field name="email"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mailcow_blacklist_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">mailcow.blacklist.view.form</field>
|
||||||
|
<field name="model">mail.mailcow.blacklist</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Blacklist">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="email"/>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Messages" name="messages">
|
||||||
|
<field name="message_ids" widget="mail_thread"/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_mailcow_blacklist" model="ir.actions.act_window">
|
||||||
|
<field name="name">Blacklist</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="res_model">mail.mailcow.blacklist</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Create a new Blacklist Entry
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Add the details of the new Blacklist Entry.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="mailcow_menu_blacklist"
|
||||||
|
name="Blacklist"
|
||||||
|
parent="mailcow_menu"
|
||||||
|
action="action_mailcow_blacklist"
|
||||||
|
sequence="30"/>
|
||||||
|
|
||||||
|
<record id="action_server_sync_blacklist" model="ir.actions.server">
|
||||||
|
<field name="name">Sync Blacklist</field>
|
||||||
|
<field name="model_id" ref="model_mail_mailcow_blacklist"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.sync_blacklist()</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_sync_blacklist" model="ir.actions.act_window.view">
|
||||||
|
<field name="sequence" eval="10"/>
|
||||||
|
<field name="view_mode">tree</field>
|
||||||
|
<field name="act_window_id" ref="action_mailcow_blacklist"/>
|
||||||
|
<field name="view_id" ref="mailcow_blacklist_view_tree"/>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
82
bemade_mailcow_blacklist/views/mailcow_mailbox_views.xml
Normal file
82
bemade_mailcow_blacklist/views/mailcow_mailbox_views.xml
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_mailcow_mailbox_tree" model="ir.ui.view">
|
||||||
|
<field name="name">mailcow.mailbox.view.tree</field>
|
||||||
|
<field name="model">mail.mailcow.mailbox</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Mailbox" js_class="mail_mailcow_mailbox_tree"> <!-- js_class must match the name added to the viewRegistry in js -->
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="local_part"/>
|
||||||
|
<field name="domain"/>
|
||||||
|
<field name="address"/>
|
||||||
|
<field name="user_id"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="mailcow_mailbox_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">mailcow.mailbox.view.form</field>
|
||||||
|
<field name="model">mail.mailcow.mailbox</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Mailbox">
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="active"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="local_part"/>
|
||||||
|
<field name="domain"/>
|
||||||
|
<field name="address"/>
|
||||||
|
<field name="user_id"/>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Messages" name="messages">
|
||||||
|
<field name="message_ids" widget="mail_thread"/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_mailcow_mailbox" model="ir.actions.act_window">
|
||||||
|
<field name="name">Mailboxes</field>
|
||||||
|
<field name="type">ir.actions.act_window</field>
|
||||||
|
<field name="res_model">mail.mailcow.mailbox</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Create a new Mailbox
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Add the details of the new Mailbox.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="mailcow_menu"
|
||||||
|
name="Mailcow"
|
||||||
|
sequence="10"
|
||||||
|
web_icon="bemade_mailcow_blacklist,static/description/icon.png"/>
|
||||||
|
|
||||||
|
<menuitem id="mailcow_menu_mailbox"
|
||||||
|
name="Mailboxes"
|
||||||
|
parent="mailcow_menu"
|
||||||
|
action="action_mailcow_mailbox"
|
||||||
|
sequence="10"/>
|
||||||
|
|
||||||
|
<record id="action_server_sync_mailboxes" model="ir.actions.server">
|
||||||
|
<field name="name">Sync Mailboxes</field>
|
||||||
|
<field name="model_id" ref="model_mail_mailcow_mailbox"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.sync_mailboxes()</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_sync_mailboxes" model="ir.actions.act_window.view">
|
||||||
|
<field name="sequence" eval="10"/>
|
||||||
|
<field name="view_mode">tree</field>
|
||||||
|
<field name="act_window_id" ref="action_mailcow_mailbox"/>
|
||||||
|
<field name="view_id" ref="view_mailcow_mailbox_tree"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
33
bemade_mailcow_blacklist/views/res_config_settings_views.xml
Normal file
33
bemade_mailcow_blacklist/views/res_config_settings_views.xml
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_res_config_settings_form_inherit_mailcow" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.form.inherit.mailcow</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<div id="emails" position='after'>
|
||||||
|
<div id="mailcow">
|
||||||
|
<h2>Mailcow Settings</h2>
|
||||||
|
<div class="row mt16 o_settings_container">
|
||||||
|
<div class="col-12 col-lg-6 o_setting_box">
|
||||||
|
<label for="mailcow_base_url" string="Mailcow URL"/>
|
||||||
|
<div class="text-muted">
|
||||||
|
Base URL for Mailcow server
|
||||||
|
</div>
|
||||||
|
<div class="content-group">
|
||||||
|
<field name="mailcow_base_url"/>
|
||||||
|
</div>
|
||||||
|
<label for="mailcow_api_key" string="API Key"/>
|
||||||
|
<div class="text-muted">
|
||||||
|
Mailcow API Key
|
||||||
|
</div>
|
||||||
|
<div class="content-group">
|
||||||
|
<field name="mailcow_api_key"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
19
bemade_mailcow_blacklist/views/res_users_views.xml
Normal file
19
bemade_mailcow_blacklist/views/res_users_views.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_users_form_mailcow" model="ir.ui.view">
|
||||||
|
<field name="name">res.users.mailcow.form</field>
|
||||||
|
<field name="model">res.users</field>
|
||||||
|
<field name="inherit_id" ref="base.view_users_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<data>
|
||||||
|
<xpath expr="//notebook" position="inside">
|
||||||
|
<page string="Mailcow" name="mailcow">
|
||||||
|
<group>
|
||||||
|
<field name="mailcow_mailbox"/>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
</xpath>
|
||||||
|
</data>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
24
bemade_mailcow_blacklist/views/templates.xml
Normal file
24
bemade_mailcow_blacklist/views/templates.xml
Normal 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>
|
||||||
60
bemade_mailcow_blacklist/views/views.xml
Normal file
60
bemade_mailcow_blacklist/views/views.xml
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- explicit list view definition -->
|
||||||
|
<!--
|
||||||
|
<record model="ir.ui.view" id="bemade_mailcow_blacklist.list">
|
||||||
|
<field name="name">bemade_mailcow_blacklist list</field>
|
||||||
|
<field name="model">bemade_mailcow_blacklist.bemade_mailcow_blacklist</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="value"/>
|
||||||
|
<field name="value2"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- actions opening views on models -->
|
||||||
|
<!--
|
||||||
|
<record model="ir.actions.act_window" id="bemade_mailcow_blacklist.action_window">
|
||||||
|
<field name="name">bemade_mailcow_blacklist window</field>
|
||||||
|
<field name="res_model">bemade_mailcow_blacklist.bemade_mailcow_blacklist</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- server action to the one above -->
|
||||||
|
<!--
|
||||||
|
<record model="ir.actions.server" id="bemade_mailcow_blacklist.action_server">
|
||||||
|
<field name="name">bemade_mailcow_blacklist server</field>
|
||||||
|
<field name="model_id" ref="model_bemade_mailcow_blacklist_bemade_mailcow_blacklist"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">
|
||||||
|
action = {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"view_mode": "tree,form",
|
||||||
|
"res_model": model._name,
|
||||||
|
}
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Top menu item -->
|
||||||
|
<!--
|
||||||
|
<menuitem name="bemade_mailcow_blacklist" id="bemade_mailcow_blacklist.menu_root"/>
|
||||||
|
-->
|
||||||
|
<!-- menu categories -->
|
||||||
|
<!--
|
||||||
|
<menuitem name="Menu 1" id="bemade_mailcow_blacklist.menu_1" parent="bemade_mailcow_blacklist.menu_root"/>
|
||||||
|
<menuitem name="Menu 2" id="bemade_mailcow_blacklist.menu_2" parent="bemade_mailcow_blacklist.menu_root"/>
|
||||||
|
-->
|
||||||
|
<!-- actions -->
|
||||||
|
<!--
|
||||||
|
<menuitem name="List" id="bemade_mailcow_blacklist.menu_1_list" parent="bemade_mailcow_blacklist.menu_1"
|
||||||
|
action="bemade_mailcow_blacklist.action_window"/>
|
||||||
|
<menuitem name="Server to list" id="bemade_mailcow_blacklist" parent="bemade_mailcow_blacklist.menu_2"
|
||||||
|
action="bemade_mailcow_blacklist.action_server"/>
|
||||||
|
-->
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
3
bemade_odoo_partner_scrapper/__init__.py
Normal file
3
bemade_odoo_partner_scrapper/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import models
|
||||||
37
bemade_odoo_partner_scrapper/__manifest__.py
Normal file
37
bemade_odoo_partner_scrapper/__manifest__.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Bemade Odoo Partner Scrapper',
|
||||||
|
'version': '1.0.0',
|
||||||
|
'category': 'Administration',
|
||||||
|
'summary': 'Module for scraping partners from odoo.com.',
|
||||||
|
'description': """
|
||||||
|
This module enables the scraping of partners from odoo.com. It allows for the collection and management of partner contact information.
|
||||||
|
|
||||||
|
Main Features:
|
||||||
|
* Automatically scrape partners from odoo.com.
|
||||||
|
* Extracts partner name, website, and contact information.
|
||||||
|
* Manage contact information of partners.
|
||||||
|
""",
|
||||||
|
'sequence': 10,
|
||||||
|
'license': 'GPL-3',
|
||||||
|
'author': 'Bemade',
|
||||||
|
'website': 'https://www.bemade.org',
|
||||||
|
'depends': ['base', 'contacts', 'sale_management', 'sale' , 'partner_multi_relation'],
|
||||||
|
'data': [
|
||||||
|
# No security
|
||||||
|
'views/res_partner_views.xml',
|
||||||
|
'data/res_partner_relation_type.xml'
|
||||||
|
],
|
||||||
|
"assets": {
|
||||||
|
"web.assets_backend": [
|
||||||
|
"bemade_odoo_partner_scrapper/static/src/js/odoo_scrapper.js",
|
||||||
|
],
|
||||||
|
"web.assets_qweb": [
|
||||||
|
"bemade_odoo_partner_scrapper/static/src/xml/odoo_scrapper_templates.xml",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'demo': [],
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'auto_install': False
|
||||||
|
}
|
||||||
106
bemade_odoo_partner_scrapper/data/demo.xml
Normal file
106
bemade_odoo_partner_scrapper/data/demo.xml
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Added partner categories and partners to this file, because it
|
||||||
|
turned out to be a bad idea to rely on demo data in base module,
|
||||||
|
that can change from release to release. Only dependency on
|
||||||
|
countries remain. They are less likely to change/disappear.
|
||||||
|
-->
|
||||||
|
<!-- Partner relation types -->
|
||||||
|
<record id="rel_type_assistant" model="res.partner.relation.type">
|
||||||
|
<field name="name">Is assistant of</field>
|
||||||
|
<field name="name_inverse">Has assistant</field>
|
||||||
|
<field name="contact_type_left">p</field>
|
||||||
|
<field name="contact_type_right">p</field>
|
||||||
|
</record>
|
||||||
|
<record id="rel_type_competitor" model="res.partner.relation.type">
|
||||||
|
<field name="name">Is competitor of</field>
|
||||||
|
<field name="name_inverse">Is competitor of</field>
|
||||||
|
<field name="contact_type_left">c</field>
|
||||||
|
<field name="contact_type_right">c</field>
|
||||||
|
<field name="is_symmetric" eval="True" />
|
||||||
|
</record>
|
||||||
|
<record id="rel_type_has_worked_for" model="res.partner.relation.type">
|
||||||
|
<field name="name">Has worked for</field>
|
||||||
|
<field name="name_inverse">Has former employee</field>
|
||||||
|
<field name="contact_type_left">p</field>
|
||||||
|
<field name="contact_type_right">c</field>
|
||||||
|
</record>
|
||||||
|
<!-- Categories -->
|
||||||
|
<record id="res_partner_category_pmr_0" model="res.partner.category">
|
||||||
|
<field name="name">Washing Companies</field>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_category_pmr_4" model="res.partner.category">
|
||||||
|
<field name="name">Washing Gold</field>
|
||||||
|
<field name="parent_id" ref="res_partner_category_pmr_0" />
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_category_pmr_5" model="res.partner.category">
|
||||||
|
<field name="name">Washing Silver</field>
|
||||||
|
<field name="parent_id" ref="res_partner_category_pmr_0" />
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_category_pmr_11" model="res.partner.category">
|
||||||
|
<field name="name">Washing Services</field>
|
||||||
|
<field name="parent_id" ref="res_partner_category_pmr_0" />
|
||||||
|
</record>
|
||||||
|
<!-- Partners -->
|
||||||
|
<record id="res_partner_pmr_great" model="res.partner">
|
||||||
|
<field name="name">Great Washing Powder Company</field>
|
||||||
|
<field
|
||||||
|
name="category_id"
|
||||||
|
eval="[(6, 0, [ref('res_partner_category_pmr_11'), ref('res_partner_category_pmr_4')])]"
|
||||||
|
/>
|
||||||
|
<field name="is_company">1</field>
|
||||||
|
<field name="city">Le Bourget du Lac</field>
|
||||||
|
<field name="zip">73377</field>
|
||||||
|
<field name="phone">+33 4 49 23 44 54</field>
|
||||||
|
<field name="country_id" ref="base.fr" />
|
||||||
|
<field name="street">93, Press Avenue</field>
|
||||||
|
<field name="email">great@yourcompany.example.com</field>
|
||||||
|
<field name="website">http://www.great.com</field>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_pmr_best" model="res.partner">
|
||||||
|
<field name="name">Best Washing Powder Company</field>
|
||||||
|
<field
|
||||||
|
name="category_id"
|
||||||
|
eval="[(6, 0, [ref('res_partner_category_pmr_4'), ref('res_partner_category_pmr_11')])]"
|
||||||
|
/>
|
||||||
|
<field name="is_company">1</field>
|
||||||
|
<field name="city">Champs sur Marne</field>
|
||||||
|
<field name="zip">77420</field>
|
||||||
|
<field name="country_id" ref="base.fr" />
|
||||||
|
<field name="email">best@yourcompany.example.com</field>
|
||||||
|
<field name="phone">+33 1 64 61 04 01</field>
|
||||||
|
<field name="street">12 rue Albert Einstein</field>
|
||||||
|
<field name="website">http://www.best.com/</field>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_pmr_super" model="res.partner">
|
||||||
|
<field name="name">Super Washing Powder Company</field>
|
||||||
|
<field name="category_id" eval="[(6,0,[ref('res_partner_category_pmr_5')])]" />
|
||||||
|
<field name="is_company">1</field>
|
||||||
|
<field name="street">3rd Floor, Room 3-C,</field>
|
||||||
|
<field
|
||||||
|
name="street2"
|
||||||
|
>Carretera Panamericana, Km 1, Urb. Delgado Chalbaud</field>
|
||||||
|
<field name="city">Caracas</field>
|
||||||
|
<field name="zip">1090</field>
|
||||||
|
<field name="email">super@yourcompany.example.com</field>
|
||||||
|
<field name="phone">+58 212 681 0538</field>
|
||||||
|
<field name="country_id" ref="base.ve" />
|
||||||
|
<field name="website">super.com</field>
|
||||||
|
</record>
|
||||||
|
<!-- Relations -->
|
||||||
|
<record id="rel_1" model="res.partner.relation">
|
||||||
|
<field name="left_partner_id" ref="res_partner_pmr_great" />
|
||||||
|
<field name="right_partner_id" ref="res_partner_pmr_super" />
|
||||||
|
<field name="type_id" ref="rel_type_competitor" />
|
||||||
|
</record>
|
||||||
|
<record id="rel_2" model="res.partner.relation">
|
||||||
|
<field name="left_partner_id" ref="res_partner_pmr_best" />
|
||||||
|
<field name="right_partner_id" ref="res_partner_pmr_super" />
|
||||||
|
<field name="type_id" ref="rel_type_competitor" />
|
||||||
|
</record>
|
||||||
|
<record id="rel_3" model="res.partner.relation">
|
||||||
|
<field name="left_partner_id" ref="res_partner_pmr_great" />
|
||||||
|
<field name="right_partner_id" ref="res_partner_pmr_best" />
|
||||||
|
<field name="type_id" ref="rel_type_competitor" />
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<!-- Added partner categories and partners to this file, because it
|
||||||
|
turned out to be a bad idea to rely on demo data in base module,
|
||||||
|
that can change from release to release. Only dependency on
|
||||||
|
countries remain. They are less likely to change/disappear.
|
||||||
|
-->
|
||||||
|
<!-- Partner relation types -->
|
||||||
|
<record id="rel_type_odoo_partner" model="res.partner.relation.type">
|
||||||
|
<field name="name">Is Odoo Partner Of</field>
|
||||||
|
<field name="name_inverse">Is Odoo Client Of</field>
|
||||||
|
<field name="contact_type_left">c</field>
|
||||||
|
<field name="contact_type_right">c</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Categories -->
|
||||||
|
<record id="res_partner_category_odoo_partner" model="res.partner.category">
|
||||||
|
<field name="name">Odoo Partner</field>
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_category_odoo_partner_learning" model="res.partner.category">
|
||||||
|
<field name="name">Learning Partner</field>
|
||||||
|
<field name="parent_id" ref="res_partner_category_odoo_partner" />
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_category_odoo_partner_ready" model="res.partner.category">
|
||||||
|
<field name="name">Ready Partner</field>
|
||||||
|
<field name="parent_id" ref="res_partner_category_odoo_partner" />
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_category_odoo_partner_silver" model="res.partner.category">
|
||||||
|
<field name="name">Silver Partner</field>
|
||||||
|
<field name="parent_id" ref="res_partner_category_odoo_partner" />
|
||||||
|
</record>
|
||||||
|
<record id="res_partner_category_odoo_partner_gold" model="res.partner.category">
|
||||||
|
<field name="name">Gold Partner</field>
|
||||||
|
<field name="parent_id" ref="res_partner_category_odoo_partner" />
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
3
bemade_odoo_partner_scrapper/models/__init__.py
Normal file
3
bemade_odoo_partner_scrapper/models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import res_partner
|
||||||
208
bemade_odoo_partner_scrapper/models/res_partner.py
Normal file
208
bemade_odoo_partner_scrapper/models/res_partner.py
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
import requests
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class ResPartner(models.Model):
|
||||||
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
|
is_odoo_partner = fields.Boolean(string="Is Odoo Partner", default=False, tracking=True)
|
||||||
|
is_odoo_user = fields.Boolean(string="Is Odoo User", default=False, tracking=True)
|
||||||
|
odoo_name = fields.Char(string="Name on Odoo", tracking=True)
|
||||||
|
odoo_id = fields.Char(string="ID on Odoo", tracking=True)
|
||||||
|
odoo_url = fields.Char(string="Odoo Partner URL", tracking=True, index=True)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def get_odoo_partner(self):
|
||||||
|
|
||||||
|
def extract_partner_data(url):
|
||||||
|
page = requests.get("https://www.odoo.com" + url)
|
||||||
|
soup = BeautifulSoup(page.content, 'html.parser')
|
||||||
|
|
||||||
|
partner_data = {}
|
||||||
|
|
||||||
|
partner_data['name'] = soup.find(id="partner_name").text
|
||||||
|
|
||||||
|
partner_type = soup.find('h3', class_='col-lg-12 text-center text-muted')
|
||||||
|
if partner_type and partner_type.span:
|
||||||
|
partner_data['partner_type'] = partner_type.span.text.strip()
|
||||||
|
|
||||||
|
partner_data['odoo_name'] = soup.find(id="partner_name").text
|
||||||
|
address_div = soup.find('span', itemprop='streetAddress')
|
||||||
|
email_div = soup.find('span', itemprop='email')
|
||||||
|
phone_div = soup.find('span', itemprop='telephone')
|
||||||
|
website_div = soup.find('span', itemprop='website')
|
||||||
|
state_data = None
|
||||||
|
|
||||||
|
if address_div is not None:
|
||||||
|
# Extract address components
|
||||||
|
address_parts = address_div.get_text(separator="\n").split("\n")
|
||||||
|
partner_data['street'] = address_parts[0]
|
||||||
|
city_state_postal = address_parts[len(address_parts) - 2].split(', ')
|
||||||
|
|
||||||
|
if city_state_postal != [',']:
|
||||||
|
partner_data['city'] = city_state_postal[0]
|
||||||
|
state_postal = city_state_postal[1].split(' ')
|
||||||
|
if len(state_postal) == 3 and len(state_postal[2]) == 3:
|
||||||
|
partner_data['zip'] = state_postal[1] + state_postal[2]
|
||||||
|
state_data = state_postal[0]
|
||||||
|
elif len(state_postal) == 2 and state_postal[1]:
|
||||||
|
if len(state_postal[1]) == 6:
|
||||||
|
partner_data['zip'] = state_postal[1]
|
||||||
|
state_data = state_postal[0]
|
||||||
|
if len(state_postal[1]) == 3:
|
||||||
|
if len(state_postal[0]) == 2:
|
||||||
|
partner_data['zip'] = state_postal[1]
|
||||||
|
state_data = state_postal[0]
|
||||||
|
else:
|
||||||
|
partner_data['zip'] = state_postal[0] + state_postal[1]
|
||||||
|
else:
|
||||||
|
if len(state_postal[0]) == 6:
|
||||||
|
partner_data['zip'] = state_postal[0]
|
||||||
|
else:
|
||||||
|
state_data = state_postal[0]
|
||||||
|
|
||||||
|
# partner_data['country'] = address_parts[len(address_parts) - 1]
|
||||||
|
|
||||||
|
partner_data['email'] = email_div.string if email_div and email_div.string else ''
|
||||||
|
partner_data['phone'] = phone_div.string if phone_div and phone_div.string else ''
|
||||||
|
partner_data['website'] = website_div.string if website_div and website_div.string else ''
|
||||||
|
partner_data['is_odoo_partner'] = True
|
||||||
|
partner_data['is_odoo_user'] = True
|
||||||
|
|
||||||
|
if state_data:
|
||||||
|
print(state_data)
|
||||||
|
country_id = self.env['res.country'].search([('code', '=', 'CA')]).id
|
||||||
|
partner_data['state_id'] = self.env['res.country.state'].search([('code', '=', state_data),('country_id', '=', country_id)]).id
|
||||||
|
|
||||||
|
return partner_data
|
||||||
|
|
||||||
|
# Load the webpage at the given url
|
||||||
|
page = requests.get("https://www.odoo.com/fr_FR/partners/country/canada-36")
|
||||||
|
# Parse the HTML content
|
||||||
|
soup = BeautifulSoup(page.content, 'html.parser')
|
||||||
|
div = soup.find(id="ref_content")
|
||||||
|
partners_url = set()
|
||||||
|
other_pages = set()
|
||||||
|
|
||||||
|
for link in div.find_all('a'):
|
||||||
|
href = link.get('href')
|
||||||
|
if href is not None:
|
||||||
|
# Ignore any URL containing 'country/canada-36'
|
||||||
|
if '/page/' in href:
|
||||||
|
# Add to set. Duplicates will be ignored automatically.
|
||||||
|
other_pages.add(href)
|
||||||
|
|
||||||
|
for link in div.find_all('a'):
|
||||||
|
href = link.get('href')
|
||||||
|
if href is not None and href != '':
|
||||||
|
# Ignore any URL containing 'country/canada-36'
|
||||||
|
if 'country/canada-36' not in href:
|
||||||
|
# Add to set. Duplicates will be ignored automatically.
|
||||||
|
partners_url.add(href.split('#', 1)[0].split('?', 1)[0])
|
||||||
|
|
||||||
|
for other_page in other_pages:
|
||||||
|
page = requests.get("https://www.odoo.com" + other_page)
|
||||||
|
soup = BeautifulSoup(page.content, 'html.parser')
|
||||||
|
div = soup.find(id="ref_content")
|
||||||
|
for link in div.find_all('a'):
|
||||||
|
href = link.get('href')
|
||||||
|
if href is not None and href != '':
|
||||||
|
# Ignore any URL containing 'country/canada-36'
|
||||||
|
if 'country/canada-36' not in href:
|
||||||
|
# Add to set. Duplicates will be ignored automatically.
|
||||||
|
partners_url.add(href.split('#', 1)[0].split('?', 1)[0])
|
||||||
|
|
||||||
|
for partner_url in partners_url:
|
||||||
|
page = requests.get("https://www.odoo.com" + partner_url)
|
||||||
|
soup = BeautifulSoup(page.content, 'html.parser')
|
||||||
|
|
||||||
|
print (partner_url)
|
||||||
|
odoo_partner = extract_partner_data(partner_url)
|
||||||
|
print (odoo_partner)
|
||||||
|
|
||||||
|
# partner_name = soup.find(id="partner_name").text
|
||||||
|
# address_div = soup.find('span', itemprop='streetAddress')
|
||||||
|
# email_div = soup.find('span', itemprop='email')
|
||||||
|
# phone_div = soup.find('span', itemprop='telephone')
|
||||||
|
# website_div = soup.find('span', itemprop='website')
|
||||||
|
# if address_div is not None:
|
||||||
|
# # Extract address components
|
||||||
|
# address_parts = address_div.get_text(separator="\n").split("\n")
|
||||||
|
# street_address = address_parts[0]
|
||||||
|
# city_state_postal = address_parts[len(address_parts) - 2].split(', ')
|
||||||
|
#
|
||||||
|
# if city_state_postal != [',']:
|
||||||
|
# city = city_state_postal[0]
|
||||||
|
# state_postal = city_state_postal[1].split(' ')
|
||||||
|
#
|
||||||
|
# if len(state_postal) == 3 and len(state_postal[2]) == 3:
|
||||||
|
# postal_code = state_postal[1] + state_postal[2]
|
||||||
|
# state = state_postal[0]
|
||||||
|
# elif len(state_postal) == 2 and state_postal[1]:
|
||||||
|
# if len(state_postal[1]) == 6:
|
||||||
|
# postal_code = state_postal[1]
|
||||||
|
# state = state_postal[0]
|
||||||
|
# if len(state_postal[1]) == 3:
|
||||||
|
# if len(state_postal[0]) == 2:
|
||||||
|
# postal_code = state_postal[1]
|
||||||
|
# state = state_postal[0]
|
||||||
|
# else:
|
||||||
|
# postal_code = state_postal[0] + state_postal[1]
|
||||||
|
# state = ''
|
||||||
|
# else:
|
||||||
|
# if len(state_postal[0]) == 6:
|
||||||
|
# postal_code = state_postal[0]
|
||||||
|
# state = ''
|
||||||
|
# else:
|
||||||
|
# postal_code = ''
|
||||||
|
# state = state_postal[0]
|
||||||
|
# else:
|
||||||
|
# city = ''
|
||||||
|
# state = ''
|
||||||
|
# postal_code = ''
|
||||||
|
# # state = state_postal[0]
|
||||||
|
# # postal_code = state_postal[1] + state_postal[2]
|
||||||
|
# country = address_parts[len(address_parts) - 1]
|
||||||
|
# if email_div and email_div.string:
|
||||||
|
# email = email_div.string
|
||||||
|
# else:
|
||||||
|
# email = ''
|
||||||
|
# if phone_div and phone_div.string:
|
||||||
|
# phone = phone_div.string
|
||||||
|
# else:
|
||||||
|
# phone = ''
|
||||||
|
# if website_div and website_div.string:
|
||||||
|
# website = website_div.string
|
||||||
|
# else:
|
||||||
|
# website = ''
|
||||||
|
|
||||||
|
_logger.info(f"Processing {odoo_partner['name']} with email {odoo_partner['email']} and phone {odoo_partner['phone']} and website {odoo_partner['website']}")
|
||||||
|
|
||||||
|
exist_odoo_name = self.env['res.partner'].search([('odoo_name', '=', odoo_partner['odoo_name'])])
|
||||||
|
if not exist_odoo_name:
|
||||||
|
exist_email = self.env['res.partner'].search([('email', '=', odoo_partner['email'])])
|
||||||
|
if not exist_email:
|
||||||
|
exist_phone = self.env['res.partner'].search([('phone', '=', odoo_partner['phone'])])
|
||||||
|
if not exist_phone:
|
||||||
|
exist_website = self.env['res.partner'].search([('website', '=', odoo_partner['website'])])
|
||||||
|
|
||||||
|
if not exist_website or odoo_partner['website'] == '':
|
||||||
|
if odoo_partner['website'] == '':
|
||||||
|
del odoo_partner['website']
|
||||||
|
self.env['res.partner'].create(odoo_partner)
|
||||||
|
# self.env['res.partner'].create({
|
||||||
|
# 'name': partner_name,
|
||||||
|
# 'street': street_address,
|
||||||
|
# 'city': city,
|
||||||
|
# 'state_id': state_id.id if state_id else False,
|
||||||
|
# 'zip': postal_code,
|
||||||
|
# 'country_id': country_id.id if country_id else False,
|
||||||
|
# 'email': email,
|
||||||
|
# 'phone': phone,
|
||||||
|
# 'website': website,
|
||||||
|
# 'is_odoo_partner': True,
|
||||||
|
# })
|
||||||
71
bemade_odoo_partner_scrapper/static/src/js/odoo_scrapper.js
Normal file
71
bemade_odoo_partner_scrapper/static/src/js/odoo_scrapper.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import ListController from 'web.ListController';
|
||||||
|
import ListView from 'web.ListView';
|
||||||
|
import KanbanController from 'web.KanbanController';
|
||||||
|
import KanbanView from 'web.KanbanView';
|
||||||
|
const viewRegistry = require('web.view_registry');
|
||||||
|
|
||||||
|
const OdooScrapperListController = ListController.extend({
|
||||||
|
// buttons_template must match the t-name on the template for the button (static xml)
|
||||||
|
buttons_template: 'odoo_scrapper.list_view_buttons',
|
||||||
|
events: _.extend({}, ListController.prototype.events, {
|
||||||
|
'click .o_button_get_partner': '_onGetPartnerClick',
|
||||||
|
}),
|
||||||
|
// This may need to be async function() if there needs to be an await this._rpc({ ... }); call to not reload early
|
||||||
|
_onGetPartnerClick: function () {
|
||||||
|
this._rpc({
|
||||||
|
model: 'res.partner',
|
||||||
|
method: 'get_odoo_partner',
|
||||||
|
args: [],
|
||||||
|
}).then(() => {
|
||||||
|
this.reload();
|
||||||
|
});
|
||||||
|
// Couldn't test this for real, but it runs the action. May need a this.reload()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const OdooScrapperListView = ListView.extend({
|
||||||
|
config: _.extend({}, ListView.prototype.config, {
|
||||||
|
Controller: OdooScrapperListController,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// key must match with the js_class attribute of the tree view you want to modify
|
||||||
|
viewRegistry.add('res_partner_odoo_scrapper_tree', OdooScrapperListView);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const OdooScrapperKanbanController = KanbanController.extend({
|
||||||
|
// buttons_template must match the t-name on the template for the button (static xml)
|
||||||
|
buttons_template: 'odoo_scrapper.kanban_view_buttons',
|
||||||
|
events: _.extend({}, KanbanController.prototype.events, {
|
||||||
|
'click .o_button_get_partner': '_onGetPartnerClick',
|
||||||
|
}),
|
||||||
|
// This may need to be async function() if there needs to be an await this._rpc({ ... }); call to not reload early
|
||||||
|
_onGetPartnerClick: function () {
|
||||||
|
this._rpc({
|
||||||
|
model: 'res.partner',
|
||||||
|
method: 'get_odoo_partner',
|
||||||
|
args: [],
|
||||||
|
}).then(() => {
|
||||||
|
this.reload();
|
||||||
|
});
|
||||||
|
// Couldn't test this for real, but it runs the action. May need a this.reload()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const OdooScrapperKanbanView = KanbanView.extend({
|
||||||
|
config: _.extend({}, KanbanView.prototype.config, {
|
||||||
|
Controller: OdooScrapperKanbanController,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// key must match with the js_class attribute of the tree view you want to modify
|
||||||
|
viewRegistry.add('res_partner_odoo_scrapper_kanban', OdooScrapperKanbanView);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-extend="ListView.buttons" t-name="odoo_scrapper.list_view_buttons"> <!-- t-name must match with js controller button template -->
|
||||||
|
<t t-jquery="button.o_list_button_add" t-operation="after">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-primary ml4 o_button_get_partner"
|
||||||
|
title="Get Odoo Partner"> Get Odoo Partner </button>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
|
||||||
|
<t t-extend="KanbanView.buttons" t-name="odoo_scrapper.kanban_view_buttons">
|
||||||
|
<t t-jquery="button" t-operation="after">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-primary ml4 o_button_get_partner"
|
||||||
|
title="Get Odoo Partner"> Get Odoo Partner </button>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</templates>
|
||||||
41
bemade_odoo_partner_scrapper/views/res_partner_views.xml
Normal file
41
bemade_odoo_partner_scrapper/views/res_partner_views.xml
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_partner_tree_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.tree.inherit</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//tree" position="attributes">
|
||||||
|
<attribute name="js_class">res_partner_odoo_scrapper_tree</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//tree" position="inside">
|
||||||
|
<field name="is_odoo_partner"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="view_partner_kanban_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.kanban.inherit</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="inherit_id" ref="base.res_partner_kanban_view"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//kanban" position="attributes">
|
||||||
|
<attribute name="js_class">res_partner_odoo_scrapper_kanban</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record model="ir.ui.view" id="view_res_partner_odoo_search">
|
||||||
|
<field name="name">res.partner.odoo.search</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="inherit_id" ref="base.view_res_partner_filter"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<filter name="type_company" position="after">
|
||||||
|
<filter domain="[('is_odoo_partner','=','True')]" string="Odoo Partners" name="res_partner_odoo_partner_filter"/>
|
||||||
|
<filter domain="[('is_odoo_user','=','True')]" string="Odoo Users" name="res_partner_odoo_user_filter"/>
|
||||||
|
</filter>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
32
bemade_partner_email_validation/__manifest__.py
Normal file
32
bemade_partner_email_validation/__manifest__.py
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
'name': 'Validate Partner Email',
|
||||||
|
'version': '1.0.0',
|
||||||
|
'category': 'Extra Tools',
|
||||||
|
'summary': 'Module to validate the email addresses of partners.',
|
||||||
|
'description': """
|
||||||
|
Validate Partner Email
|
||||||
|
===================
|
||||||
|
|
||||||
|
This module adds a functionality to validate the email addresses of partners in Odoo. A new boolean field 'email_validated' is added to the 'res.partner' model. When an email address is entered or changed, 'email_validated' is set to False and a validation email is sent to the new email address. If the recipient clicks the link in the validation email, 'email_validated' is set to True. Before sending an email to a partner, the system checks whether the partner's email is validated and if not, a message is posted in the chatter.
|
||||||
|
|
||||||
|
Main Features:
|
||||||
|
--------------
|
||||||
|
* Add 'email_validated' field to 'res.partner'.
|
||||||
|
* Send validation email when 'email_validated' is False.
|
||||||
|
* Post message in chatter when trying to send email to partner with unvalidated email.
|
||||||
|
""",
|
||||||
|
'sequence': 10,
|
||||||
|
'license': 'GPL-3',
|
||||||
|
'author': 'Bemade',
|
||||||
|
'website': 'https://www.bemade.org',
|
||||||
|
'depends': ['base', 'mail'],
|
||||||
|
'data': [
|
||||||
|
'security/ir.model.access.csv',
|
||||||
|
'views/res_partner_views.xml',
|
||||||
|
'data/mail_template_data.xml'
|
||||||
|
],
|
||||||
|
'demo': ['demo/res_partner_demo.xml'],
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'auto_install': False
|
||||||
|
}
|
||||||
12
bemade_partner_email_validation/controllers/controllers.py
Normal file
12
bemade_partner_email_validation/controllers/controllers.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
|
||||||
|
class ValidateEmailController(http.Controller):
|
||||||
|
|
||||||
|
@http.route(['/validate_email/<model("res.partner"):partner>/<token>'], type='http', auth="public", website=True)
|
||||||
|
def validate_email(self, partner, token, **kw):
|
||||||
|
if partner.email_validation_token != token:
|
||||||
|
return request.render("bemade_validate_partner_email.invalid_token", {})
|
||||||
|
else:
|
||||||
|
partner.sudo().write({'email_validated': True})
|
||||||
|
return request.render("bemade_validate_partner_email.email_validated", {})
|
||||||
34
bemade_partner_email_validation/data/mail_template_data.xml
Normal file
34
bemade_partner_email_validation/data/mail_template_data.xml
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Email template for the email validation -->
|
||||||
|
<record id="email_template_validation" model="mail.template">
|
||||||
|
<field name="name">Email Validation</field>
|
||||||
|
<field name="model_id" ref="base.model_res_partner"/>
|
||||||
|
<field name="subject">Please validate your email address</field>
|
||||||
|
<field name="body_html">
|
||||||
|
<![CDATA[
|
||||||
|
<p>Dear ${object.name},</p>
|
||||||
|
<p>
|
||||||
|
We have recently received a request to validate your email address
|
||||||
|
for our records. Please click on the link below to confirm that
|
||||||
|
this is your email address.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a href="/validate_email/${object.id}/${object.email_validation_token}">Click here to validate your email</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you have received this email in error, no action is required.
|
||||||
|
Your email address will not be validated if you do not click the link.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Thank you!
|
||||||
|
</p>
|
||||||
|
]]>
|
||||||
|
</field>
|
||||||
|
<field name="email_from">${object.company_id.email}</field>
|
||||||
|
<field name="email_to">${object.email}</field>
|
||||||
|
<field name="auto_delete" eval="True"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
16
bemade_partner_email_validation/models/mail_mail.py
Normal file
16
bemade_partner_email_validation/models/mail_mail.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
from odoo import models, exceptions, _
|
||||||
|
|
||||||
|
class Mail(models.Model):
|
||||||
|
_inherit = 'mail.mail'
|
||||||
|
|
||||||
|
def send(self, auto_commit=False, raise_exception=False):
|
||||||
|
for mail in self:
|
||||||
|
if mail.email_to:
|
||||||
|
partner = self.env['res.partner'].search([('email', '=', mail.email_to)], limit=1)
|
||||||
|
if partner and not partner.email_validated:
|
||||||
|
model = self.env[mail.model].browse(mail.res_id)
|
||||||
|
model.message_post(body=_('Cannot send email because the recipient\'s email address is not validated.'))
|
||||||
|
if raise_exception:
|
||||||
|
raise exceptions.UserError(_('Cannot send email because the recipient\'s email address is not validated.'))
|
||||||
|
continue
|
||||||
|
return super().send(auto_commit=auto_commit, raise_exception=raise_exception)
|
||||||
30
bemade_partner_email_validation/models/res_partner.py
Normal file
30
bemade_partner_email_validation/models/res_partner.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
|
||||||
|
class Partner(models.Model):
|
||||||
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
|
email_validated = fields.Boolean(string='Email Validated', default=False)
|
||||||
|
|
||||||
|
email_validation_token = fields.Char(string="Email Validation Token", copy=False)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
if'Email' in vals:
|
||||||
|
vals['email_validated'] = False
|
||||||
|
partner = super().create(vals)
|
||||||
|
partner._send_validation_email()
|
||||||
|
return partner
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
if 'email' in vals:
|
||||||
|
vals['email_validated'] = False
|
||||||
|
res = super().write(vals)
|
||||||
|
for partner in self:
|
||||||
|
partner._send_validation_email()
|
||||||
|
return res
|
||||||
|
|
||||||
|
def send_validation_email(self):
|
||||||
|
for partner in self:
|
||||||
|
token = misc.generate_tracking_token()
|
||||||
|
partner.email_validation_token = token
|
||||||
|
partner.validation_email_template_id.send_mail(partner.id)
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
access_res_partner_validate_email,access_res_partner_validate_email,model_res_partner,base.group_user,1,1,1,1
|
||||||
|
36
bemade_partner_email_validation/views/res_partner_views.xml
Normal file
36
bemade_partner_email_validation/views/res_partner_views.xml
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_partner_form" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.form.email.validated</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='email']" position="after">
|
||||||
|
<field name="email_validated"/>
|
||||||
|
<button name="send_validation_email" type="object" string="Resend Validation Email" attrs="{'invisible': [('email_validated', '=', True)]}"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_send_validation_emails" model="ir.actions.server">
|
||||||
|
<field name="name">Send Validation Emails</field>
|
||||||
|
<field name="model_id" ref="base.model_res_partner"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model.send_validation_email()</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="res_partner_view_tree" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.tree</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree position="attributes">
|
||||||
|
<attribute name="decoration-muted">email_validated == True</attribute>
|
||||||
|
</tree>
|
||||||
|
<xpath expr="//field[@name='email']" position="after">
|
||||||
|
<field name="email_validated"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
```
|
||||||
1
bemade_time_off_follower/__init__.py
Normal file
1
bemade_time_off_follower/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
22
bemade_time_off_follower/__manifest__.py
Normal file
22
bemade_time_off_follower/__manifest__.py
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
"name": "Time Off Alternative Follower",
|
||||||
|
"version": "15.0.0.0.1",
|
||||||
|
"category": "Extra Tools",
|
||||||
|
'summary': 'Add Alternative Follower When Receiving Message While On Time Off',
|
||||||
|
"description": """
|
||||||
|
Add Alternative Follower When Receiving Message While On Time Off
|
||||||
|
""",
|
||||||
|
"author": "Bemade",
|
||||||
|
'website': 'https://www.bemade.org',
|
||||||
|
"depends": [
|
||||||
|
'hr_holidays',
|
||||||
|
'mail'
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
'views/hr_leave_views.xml',
|
||||||
|
],
|
||||||
|
"auto_install": False,
|
||||||
|
"installable": True,
|
||||||
|
'license': 'OPL-1'
|
||||||
|
}
|
||||||
2
bemade_time_off_follower/models/__init__.py
Normal file
2
bemade_time_off_follower/models/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import hr_leave
|
||||||
|
from . import mail_thread
|
||||||
10
bemade_time_off_follower/models/hr_leave.py
Normal file
10
bemade_time_off_follower/models/hr_leave.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
class HrLeave(models.Model):
|
||||||
|
_inherit = 'hr.leave'
|
||||||
|
|
||||||
|
alternate_follower_id = fields.Many2one(
|
||||||
|
comodel_name='res.users',
|
||||||
|
string='Alternate Follower',
|
||||||
|
help='User who will receive a copy of all communications related to this leave',
|
||||||
|
)
|
||||||
26
bemade_time_off_follower/models/mail_thread.py
Normal file
26
bemade_time_off_follower/models/mail_thread.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
from odoo import models, api
|
||||||
|
|
||||||
|
class MailThread(models.AbstractModel):
|
||||||
|
_inherit = 'mail.thread'
|
||||||
|
|
||||||
|
@api.returns('mail.message', lambda value: value.id)
|
||||||
|
def message_post(self, **kwargs):
|
||||||
|
message = super(MailThread, self).message_post(**kwargs)
|
||||||
|
|
||||||
|
leave_env = self.env['hr.leave']
|
||||||
|
for recipient in message.notification_ids.mapped('res_partner_id'):
|
||||||
|
leave = leave_env.search([
|
||||||
|
('state', '=', 'validate'),
|
||||||
|
('date_from', '<=', fields.Date.today()),
|
||||||
|
('date_to', '>=', fields.Date.today()),
|
||||||
|
('employee_id.user_id.partner_id', '=', recipient.id),
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if leave and leave.alternate_follower_id:
|
||||||
|
message.write({
|
||||||
|
'notification_ids': [(0, 0, {
|
||||||
|
'res_partner_id': leave.alternate_follower_id.partner_id.id,
|
||||||
|
# 'notification_type': 'email',
|
||||||
|
})]
|
||||||
|
})
|
||||||
|
return message
|
||||||
13
bemade_time_off_follower/views/hr_leave_views.xml
Normal file
13
bemade_time_off_follower/views/hr_leave_views.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="hr_leave_form_view_inherit" model="ir.ui.view">
|
||||||
|
<field name="name">hr.leave.form.view.inherit</field>
|
||||||
|
<field name="model">hr.leave</field>
|
||||||
|
<field name="inherit_id" ref="hr_holidays.hr_leave_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='holiday_status_id']" position="after">
|
||||||
|
<field name="alternate_follower_id"/>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
|
|
@ -40,7 +40,6 @@ class IrUiMenu(models.Model):
|
||||||
|
|
||||||
menus = self.env['ir.ui.menu'].search([('parent_id', '=', False)])
|
menus = self.env['ir.ui.menu'].search([('parent_id', '=', False)])
|
||||||
|
|
||||||
|
|
||||||
for self_record in self:
|
for self_record in self:
|
||||||
if 'parent_id' in vals:
|
if 'parent_id' in vals:
|
||||||
if vals.get('parent_id') == False and self_record in menus:
|
if vals.get('parent_id') == False and self_record in menus:
|
||||||
|
|
@ -62,7 +61,6 @@ class IrUiMenu(models.Model):
|
||||||
self.env['res.users.menu.order'].search([('menu_id', '=', self_record.id)]).unlink()
|
self.env['res.users.menu.order'].search([('menu_id', '=', self_record.id)]).unlink()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@api.model
|
|
||||||
def load_menus(self, debug):
|
def load_menus(self, debug):
|
||||||
menus = super().load_menus(debug)
|
menus = super().load_menus(debug)
|
||||||
|
|
||||||
|
|
|
||||||
3
bemade_user_password_bundle/__init__.py
Normal file
3
bemade_user_password_bundle/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import models
|
||||||
25
bemade_user_password_bundle/__manifest__.py
Normal file
25
bemade_user_password_bundle/__manifest__.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': "User Password Bundle",
|
||||||
|
|
||||||
|
'summary': """
|
||||||
|
Module to create password bundles and provide access to them for admins of a newly created user.
|
||||||
|
""",
|
||||||
|
|
||||||
|
'description': """
|
||||||
|
This module automate the creation of a password bundle for new users in Odoo 15 and changes the default admin
|
||||||
|
ownership of bundle to admin/setting group instead of the bundle creator.
|
||||||
|
""",
|
||||||
|
|
||||||
|
'author': "Bemade",
|
||||||
|
'website': "https://www.bemade.org",
|
||||||
|
'category': 'Technical',
|
||||||
|
'version': '0.1',
|
||||||
|
'license': 'AGPL-3',
|
||||||
|
'depends': ['odoo_password_manager'],
|
||||||
|
|
||||||
|
'data': [
|
||||||
|
],
|
||||||
|
'demo': [
|
||||||
|
],
|
||||||
|
}
|
||||||
4
bemade_user_password_bundle/models/__init__.py
Normal file
4
bemade_user_password_bundle/models/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from . import hr_employee
|
||||||
|
from . import password_bundle
|
||||||
25
bemade_user_password_bundle/models/hr_employee.py
Normal file
25
bemade_user_password_bundle/models/hr_employee.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
|
||||||
|
class HrEmployee(models.Model):
|
||||||
|
_inherit = 'hr.employee'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def create(self, vals):
|
||||||
|
newemployee = super(HrEmployee, self).create(vals)
|
||||||
|
pw_bundle = self.env['password.bundle'].create({
|
||||||
|
'name': newemployee.name,
|
||||||
|
})
|
||||||
|
self.env['password.access'].create({
|
||||||
|
'bundle_id': pw_bundle.id,
|
||||||
|
'user_id': newemployee.user_id.id,
|
||||||
|
'access_level': 'full'
|
||||||
|
})
|
||||||
|
|
||||||
|
return newemployee
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
30
bemade_user_password_bundle/models/password_bundle.py
Normal file
30
bemade_user_password_bundle/models/password_bundle.py
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
|
||||||
|
class password_bundle(models.Model):
|
||||||
|
_inherit = 'password.bundle'
|
||||||
|
_name = 'password.bundle'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _default_access_admin_ids(self):
|
||||||
|
"""
|
||||||
|
Default method for access_ids
|
||||||
|
"""
|
||||||
|
values = {
|
||||||
|
'access_level': 'admin',
|
||||||
|
'group_id': self.env.ref('base.group_system').id,
|
||||||
|
}
|
||||||
|
return [(0, 0, values)]
|
||||||
|
|
||||||
|
# BV: UGLY HACK
|
||||||
|
# I should be able to remove this field, but I can't just override the default function
|
||||||
|
# _default_access_ids, so I rename it _default_access_admin_ids and override the th field
|
||||||
|
|
||||||
|
access_ids = fields.One2many(
|
||||||
|
"password.access",
|
||||||
|
"bundle_id",
|
||||||
|
default=_default_access_admin_ids,
|
||||||
|
)
|
||||||
|
|
||||||
Loading…
Reference in a new issue