19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:32:34 +01:00
parent 5faf7397c5
commit 2696f14ed7
721 changed files with 220375 additions and 91221 deletions

View file

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import iap_account
from . import res_config_settings
from . import iap_enrich_api
from . import iap_service

View file

@ -1,12 +1,17 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import hashlib
import logging
import secrets
import uuid
import werkzeug.urls
from odoo import api, fields, models
from odoo import api, fields, models, _
from odoo.addons.iap.tools import iap_tools
from odoo.exceptions import AccessError, UserError
from odoo.modules import module
from odoo.tools import get_lang
from odoo.tools.urls import urljoin as url_join
_logger = logging.getLogger(__name__)
@ -15,16 +20,119 @@ DEFAULT_ENDPOINT = 'https://iap.odoo.com'
class IapAccount(models.Model):
_name = 'iap.account'
_rec_name = 'service_name'
_description = 'IAP Account'
service_name = fields.Char()
account_token = fields.Char(default=lambda s: uuid.uuid4().hex)
name = fields.Char()
service_id = fields.Many2one('iap.service', required=True)
service_name = fields.Char(related='service_id.technical_name')
service_locked = fields.Boolean(default=False) # If True, the service can't be edited anymore
description = fields.Char(related='service_id.description')
account_token = fields.Char(
default=lambda s: uuid.uuid4().hex,
help="Account token is your authentication key for this service. Do not share it.",
size=43,
copy=False,
groups="base.group_system",
)
company_ids = fields.Many2many('res.company')
# Dynamic fields, which are received from iap server and set when loading the view
balance = fields.Char(readonly=True)
warning_threshold = fields.Float("Email Alert Threshold")
warning_user_ids = fields.Many2many('res.users', string="Email Alert Recipients")
state = fields.Selection([('banned', 'Banned'), ('registered', "Registered"), ('unregistered', "Unregistered")], readonly=True)
@api.constrains('warning_threshold', 'warning_user_ids')
def validate_warning_alerts(self):
for account in self:
if account.warning_threshold < 0:
raise UserError(_("Please set a positive email alert threshold."))
users_with_no_email = [user.name for user in self.warning_user_ids if not user.email]
if users_with_no_email:
raise UserError(_(
"One of the email alert recipients doesn't have an email address set. Users: %s",
",".join(users_with_no_email),
))
def web_read(self, *args, **kwargs):
if not self.env.context.get('disable_iap_fetch'):
self._get_account_information_from_iap()
return super().web_read(*args, **kwargs)
def web_save(self, *args, **kwargs):
return super(IapAccount, self.with_context(disable_iap_fetch=True)).web_save(*args, **kwargs)
def write(self, vals):
res = super().write(vals)
if (
not self.env.context.get('disable_iap_update')
and any(warning_attribute in vals for warning_attribute in ('warning_threshold', 'warning_user_ids'))
):
route = '/iap/1/update-warning-email-alerts'
endpoint = iap_tools.iap_get_endpoint(self.env)
url = url_join(endpoint, route)
for account in self:
data = {
'account_token': account.sudo().account_token,
'warning_threshold': account.warning_threshold,
'warning_emails': [{
'email': user.email,
'lang_code': user.lang or get_lang(self.env).code,
} for user in account.warning_user_ids],
}
try:
iap_tools.iap_jsonrpc(url=url, params=data)
except AccessError as e:
_logger.warning("Update of the warning email configuration has failed: %s", str(e))
return res
def _get_account_information_from_iap(self):
# During testing, we don't want to call the iap server
if module.current_test:
return
route = '/iap/1/get-accounts-information'
endpoint = iap_tools.iap_get_endpoint(self.env)
url = url_join(endpoint, route)
params = {
'iap_accounts': [{
'token': account.sudo().account_token,
'service': account.service_id.technical_name,
} for account in self if account.service_id],
'dbuuid': self.env['ir.config_parameter'].sudo().get_param('database.uuid'),
}
try:
accounts_information = iap_tools.iap_jsonrpc(url=url, params=params)
except AccessError as e:
_logger.warning("Fetch of the IAP accounts information has failed: %s", str(e))
return
for token, information in accounts_information.items():
information.pop('link_to_service_page', None)
accounts = self.filtered(lambda acc: secrets.compare_digest(acc.sudo().account_token, token))
for account in accounts:
# Default rounding of 4 decimal places to avoid large decimals
balance_amount = round(information['balance'], None if account.service_id.integer_balance else 4)
balance = f"{balance_amount} {account.service_id.unit_name or ''}"
account_info = self._get_account_info(account, balance, information)
account.with_context(disable_iap_update=True, tracking_disable=True).write(account_info)
def _get_account_info(self, account_id, balance, information):
return {
'balance': balance,
'warning_threshold': information['warning_threshold'],
'state': information['registered'],
'service_locked': True, # The account exist on IAP, prevent the edition of the service
}
@api.model_create_multi
def create(self, vals_list):
accounts = super().create(vals_list)
for account in accounts:
if not account.name:
account.name = account.service_id.name
if self.env['ir.config_parameter'].sudo().get_param('database.is_neutralized'):
# Disable new accounts on a neutralized database
for account in accounts:
@ -40,7 +148,7 @@ class IapAccount(models.Model):
('company_ids', '=', False)
]
accounts = self.search(domain, order='id desc')
accounts_without_token = accounts.filtered(lambda acc: not acc.account_token)
accounts_without_token = accounts.filtered(lambda acc: not acc.sudo().account_token)
if accounts_without_token:
with self.pool.cursor() as cr:
# In case of a further error that will rollback the database, we should
@ -53,6 +161,13 @@ class IapAccount(models.Model):
IapAccount.search(domain + [('account_token', '=', False)]).sudo().unlink()
accounts = accounts - accounts_without_token
if not accounts:
service = self.env['iap.service'].search([('technical_name', '=', service_name)], limit=1)
if not service:
raise UserError(self.env._("No service exists with the provided technical name"))
if module.current_test:
# During testing, we don't want to commit the creation of a new IAP account to the database
return self.sudo().create({'service_id': service.id})
with self.pool.cursor() as cr:
# Since the account did not exist yet, we will encounter a NoCreditError,
# which is going to rollback the database and undo the account creation,
@ -65,10 +180,10 @@ class IapAccount(models.Model):
if not account:
if not force_create:
return account
account = IapAccount.create({'service_name': service_name})
account = IapAccount.create({'service_id': service.id})
# fetch 'account_token' into cache with this cursor,
# as self's cursor cannot see this account
account_token = account.account_token
account_token = account.sudo().account_token
account = self.browse(account.id)
self.env.cache.set(account, IapAccount._fields['account_token'], account_token)
return account
@ -78,63 +193,55 @@ class IapAccount(models.Model):
return accounts[0]
@api.model
def get_credits_url(self, service_name, base_url='', credit=0, trial=False):
""" Called notably by ajax crash manager, buy more widget, partner_autocomplete, sanilmail. """
def get_account_id(self, service_name):
return self.get(service_name).id
@api.model
def get_credits_url(self, service_name, account_token=None):
""" Called notably by: buy more widget, partner_autocomplete, snailmail, ... """
dbuuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
if not base_url:
endpoint = iap_tools.iap_get_endpoint(self.env)
route = '/iap/1/credit'
base_url = endpoint + route
account_token = self.get(service_name).account_token
endpoint = iap_tools.iap_get_endpoint(self.env)
route = '/iap/1/credit'
base_url = url_join(endpoint, route)
account_token = account_token or self.get(service_name).sudo().account_token
hashed_account_token = self._hash_iap_token(account_token)
d = {
'dbuuid': dbuuid,
'service_name': service_name,
'account_token': account_token,
'credit': credit,
'account_token': hashed_account_token,
'hashed': 1,
}
if trial:
d.update({'trial': trial})
return '%s?%s' % (base_url, werkzeug.urls.url_encode(d))
@api.model
def get_account_url(self):
""" Called only by res settings """
route = '/iap/services'
endpoint = iap_tools.iap_get_endpoint(self.env)
all_accounts = self.search([
'|',
('company_ids', '=', self.env.company.id),
('company_ids', '=', False),
])
def _hash_iap_token(self, key):
# disregard possible suffix
key = (key or '').split('+')[0]
if not key:
raise UserError(_('The IAP token provided is invalid or empty.'))
return hashlib.sha1(key.encode('utf-8')).hexdigest()
global_account_per_service = {
account.service_name: account.account_token
for account in all_accounts.filtered(lambda acc: not acc.company_ids)
def action_buy_credits(self):
return {
'type': 'ir.actions.act_url',
'url': self.env['iap.account'].get_credits_url(
account_token=self.sudo().account_token,
service_name=self.service_name,
),
}
company_account_per_service = {
account.service_name: account.account_token
for account in all_accounts.filtered(lambda acc: acc.company_ids)
}
# Prioritize company specific accounts over global accounts
account_per_service = {**global_account_per_service, **company_account_per_service}
parameters = {'tokens': list(account_per_service.values())}
return '%s?%s' % (endpoint + route, werkzeug.urls.url_encode(parameters))
@api.model
def get_config_account_url(self):
""" Called notably by ajax partner_autocomplete. """
account = self.env['iap.account'].get('partner_autocomplete')
action = self.env.ref('iap.iap_account_action')
menu = self.env.ref('iap.iap_account_menu')
no_one = self.user_has_groups('base.group_no_one')
if not self.env.user.has_group('base.group_no_one'):
return False
if account:
url = "/web#id=%s&action=%s&model=iap.account&view_type=form&menu_id=%s" % (account.id, action.id, menu.id)
url = f"/odoo/action-iap.iap_account_action/{account.id}?menu_id={menu.id}"
else:
url = "/web#action=%s&model=iap.account&view_type=form&menu_id=%s" % (action.id, menu.id)
return no_one and url
url = f"/odoo/action-iap.iap_account_action?menu_id={menu.id}"
return url
@api.model
def get_credits(self, service_name):
@ -144,15 +251,15 @@ class IapAccount(models.Model):
if account:
route = '/iap/1/balance'
endpoint = iap_tools.iap_get_endpoint(self.env)
url = endpoint + route
url = url_join(endpoint, route)
params = {
'dbuuid': self.env['ir.config_parameter'].sudo().get_param('database.uuid'),
'account_token': account.account_token,
'account_token': account.sudo().account_token,
'service_name': service_name,
}
try:
credit = iap_tools.iap_jsonrpc(url=url, params=params)
except Exception as e:
except AccessError as e:
_logger.info('Get credit error : %s', str(e))
credit = -1

View file

@ -5,7 +5,7 @@ from odoo import models, api
from odoo.addons.iap.tools import iap_tools
class IapEnrichAPI(models.AbstractModel):
class IapEnrichApi(models.AbstractModel):
_name = 'iap.enrich.api'
_description = 'IAP Lead Enrichment API'
_DEFAULT_ENDPOINT = 'https://iap-services.odoo.com'
@ -14,7 +14,7 @@ class IapEnrichAPI(models.AbstractModel):
def _contact_iap(self, local_endpoint, params):
account = self.env['iap.account'].get('reveal')
dbuuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
params['account_token'] = account.account_token
params['account_token'] = account.sudo().account_token
params['dbuuid'] = dbuuid
base_url = self.env['ir.config_parameter'].sudo().get_param('enrich.endpoint', self._DEFAULT_ENDPOINT)
return iap_tools.iap_jsonrpc(base_url + local_endpoint, params=params, timeout=300)

View file

@ -0,0 +1,19 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class IapService(models.Model):
_name = 'iap.service'
_description = 'IAP Service'
name = fields.Char(required=True)
technical_name = fields.Char(readonly=True, required=True)
description = fields.Char(required=True, translate=True)
unit_name = fields.Char(required=True, translate=True)
integer_balance = fields.Boolean(required=True)
_unique_technical_name = models.Constraint(
'UNIQUE(technical_name)',
'Only one service can exist with a specific technical_name',
)

View file

@ -1,15 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
@api.model
def _redirect_to_iap_account(self):
return {
'type': 'ir.actions.act_url',
'url': self.env['iap.account'].get_account_url(),
}