316 lines
15 KiB
Python
316 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from odoo import _, api, fields, models, modules, tools
|
|
from odoo.exceptions import UserError, ValidationError
|
|
|
|
from odoo.addons.account_edi_proxy_client.models.account_edi_proxy_user import AccountEdiProxyError
|
|
from odoo.addons.account_edi_ubl_cii.models.account_edi_common import EAS_MAPPING
|
|
from odoo.addons.account_peppol.tools.demo_utils import handle_demo
|
|
|
|
# at the moment, only European countries are accepted
|
|
ALLOWED_COUNTRIES = set(EAS_MAPPING.keys()) - {'AU', 'SG', 'NZ'}
|
|
|
|
|
|
class ResConfigSettings(models.TransientModel):
|
|
_inherit = 'res.config.settings'
|
|
|
|
account_peppol_edi_user = fields.Many2one(
|
|
comodel_name='account_edi_proxy_client.user',
|
|
string='EDI user',
|
|
compute='_compute_account_peppol_edi_user',
|
|
)
|
|
account_peppol_contact_email = fields.Char(related='company_id.account_peppol_contact_email', readonly=False)
|
|
account_peppol_eas = fields.Selection(related='company_id.peppol_eas', readonly=False)
|
|
account_peppol_edi_identification = fields.Char(related='account_peppol_edi_user.edi_identification')
|
|
account_peppol_endpoint = fields.Char(related='company_id.peppol_endpoint', readonly=False)
|
|
account_peppol_endpoint_warning = fields.Char(
|
|
string="Warning",
|
|
compute="_compute_account_peppol_endpoint_warning",
|
|
)
|
|
account_peppol_migration_key = fields.Char(related='company_id.account_peppol_migration_key', readonly=False)
|
|
account_peppol_phone_number = fields.Char(related='company_id.account_peppol_phone_number', readonly=False)
|
|
account_peppol_proxy_state = fields.Selection(related='company_id.account_peppol_proxy_state', readonly=False)
|
|
account_peppol_purchase_journal_id = fields.Many2one(related='company_id.peppol_purchase_journal_id', readonly=False)
|
|
account_peppol_verification_code = fields.Char(related='account_peppol_edi_user.peppol_verification_code', readonly=False)
|
|
is_account_peppol_participant = fields.Boolean(
|
|
string='Use PEPPOL',
|
|
related='company_id.is_account_peppol_participant', readonly=False,
|
|
help='Register as a PEPPOL user',
|
|
)
|
|
account_peppol_edi_mode = fields.Selection(
|
|
selection=[('demo', 'Demo'), ('test', 'Test'), ('prod', 'Live')],
|
|
compute='_compute_account_peppol_edi_mode',
|
|
inverse='_inverse_account_peppol_edi_mode',
|
|
readonly=False,
|
|
)
|
|
account_peppol_mode_constraint = fields.Selection(
|
|
selection=[('demo', 'Demo'), ('test', 'Test'), ('prod', 'Live')],
|
|
compute='_compute_account_peppol_mode_constraint',
|
|
help="Using the config params, this field specifies which edi modes may be selected from the UI"
|
|
)
|
|
|
|
# -------------------------------------------------------------------------
|
|
# HELPER METHODS
|
|
# -------------------------------------------------------------------------
|
|
|
|
def _call_peppol_proxy(self, endpoint, params=None, edi_user=None):
|
|
errors = {
|
|
'code_incorrect': _('The verification code is not correct'),
|
|
'code_expired': _('This verification code has expired. Please request a new one.'),
|
|
'too_many_attempts': _('Too many attempts to request an SMS code. Please try again later.'),
|
|
}
|
|
|
|
if not edi_user:
|
|
edi_user = self.company_id.account_edi_proxy_client_ids.filtered(lambda u: u.proxy_type == 'peppol')
|
|
|
|
params = params or {}
|
|
try:
|
|
response = edi_user._make_request(
|
|
f"{edi_user._get_server_url()}{endpoint}",
|
|
params=params,
|
|
)
|
|
except AccountEdiProxyError as e:
|
|
raise UserError(e.message)
|
|
|
|
if 'error' in response:
|
|
error_code = response['error'].get('code')
|
|
error_message = response['error'].get('message') or response['error'].get('data', {}).get('message')
|
|
raise UserError(errors.get(error_code) or error_message or _('Connection error, please try again later.'))
|
|
return response
|
|
|
|
# -------------------------------------------------------------------------
|
|
# COMPUTE METHODS
|
|
# -------------------------------------------------------------------------
|
|
@api.depends('is_account_peppol_eligible', 'account_peppol_edi_user')
|
|
def _compute_account_peppol_mode_constraint(self):
|
|
mode_constraint = self.env['ir.config_parameter'].sudo().get_param('account_peppol.mode_constraint')
|
|
trial_param = self.env['ir.config_parameter'].sudo().get_param('saas_trial.confirm_token')
|
|
self.account_peppol_mode_constraint = trial_param and 'demo' or mode_constraint or 'prod'
|
|
|
|
@api.depends('is_account_peppol_eligible', 'account_peppol_edi_user')
|
|
def _compute_account_peppol_edi_mode(self):
|
|
edi_mode = self.env['ir.config_parameter'].sudo().get_param('account_peppol.edi.mode')
|
|
for config in self:
|
|
if config.account_peppol_edi_user:
|
|
config.account_peppol_edi_mode = config.account_peppol_edi_user.edi_mode
|
|
else:
|
|
config.account_peppol_edi_mode = edi_mode or 'prod'
|
|
|
|
def _inverse_account_peppol_edi_mode(self):
|
|
for config in self:
|
|
if not config.account_peppol_edi_user and config.account_peppol_edi_mode:
|
|
self.env['ir.config_parameter'].sudo().set_param('account_peppol.edi.mode', config.account_peppol_edi_mode)
|
|
return
|
|
|
|
@api.depends("company_id.account_edi_proxy_client_ids")
|
|
def _compute_account_peppol_edi_user(self):
|
|
for config in self:
|
|
config.account_peppol_edi_user = config.company_id.account_edi_proxy_client_ids.filtered(
|
|
lambda u: u.proxy_type == 'peppol')
|
|
|
|
@api.depends('account_peppol_eas', 'account_peppol_endpoint')
|
|
def _compute_account_peppol_endpoint_warning(self):
|
|
for config in self:
|
|
if (
|
|
not config.account_peppol_eas
|
|
or config.company_id._check_peppol_endpoint_number(warning=True)
|
|
):
|
|
config.account_peppol_endpoint_warning = False
|
|
else:
|
|
config.account_peppol_endpoint_warning = _("The endpoint number might not be correct. "
|
|
"Please check if you entered the right identification number.")
|
|
|
|
# -------------------------------------------------------------------------
|
|
# BUSINESS ACTIONS
|
|
# -------------------------------------------------------------------------
|
|
|
|
@handle_demo
|
|
def button_create_peppol_proxy_user(self):
|
|
"""
|
|
The first step of the Peppol onboarding.
|
|
- Creates an EDI proxy user on the iap side, then the client side
|
|
- Calls /activate_participant to mark the EDI user as peppol user
|
|
"""
|
|
self.ensure_one()
|
|
|
|
if self.account_peppol_proxy_state != 'not_registered':
|
|
raise UserError(
|
|
_('Cannot register a user with a %s application', self.account_peppol_proxy_state))
|
|
|
|
if not self.account_peppol_phone_number:
|
|
raise ValidationError(_("Please enter a phone number to verify your application."))
|
|
if not self.account_peppol_contact_email:
|
|
raise ValidationError(_("Please enter a primary contact email to verify your application."))
|
|
|
|
company = self.company_id
|
|
edi_proxy_client = self.env['account_edi_proxy_client.user']
|
|
edi_identification = edi_proxy_client._get_proxy_identification(company, 'peppol')
|
|
if company.partner_id._check_peppol_participant_exists(edi_identification) and not self.account_peppol_migration_key:
|
|
raise UserError(
|
|
_("A participant with these details has already been registered on the network. "
|
|
"If you have previously registered to an alternative Peppol service, please deregister from that service, "
|
|
"or request a migration key before trying again."))
|
|
|
|
edi_user = edi_proxy_client.sudo()._register_proxy_user(company, 'peppol', self.account_peppol_edi_mode)
|
|
self.account_peppol_proxy_state = 'not_verified'
|
|
|
|
# if there is an error when activating the participant below,
|
|
# the client side is rolled back and the edi user is deleted on the client side
|
|
# but remains on the proxy side.
|
|
# it is important to keep these two in sync, so commit before activating.
|
|
if not tools.config['test_enable'] and not modules.module.current_test:
|
|
self.env.cr.commit()
|
|
|
|
company_details = {
|
|
'peppol_company_name': company.display_name,
|
|
'peppol_company_vat': company.vat,
|
|
'peppol_company_street': company.street,
|
|
'peppol_company_city': company.city,
|
|
'peppol_company_zip': company.zip,
|
|
'peppol_country_code': company.country_id.code,
|
|
'peppol_phone_number': self.account_peppol_phone_number,
|
|
'peppol_contact_email': self.account_peppol_contact_email,
|
|
}
|
|
|
|
params = {
|
|
'migration_key': self.account_peppol_migration_key,
|
|
'company_details': company_details,
|
|
}
|
|
|
|
self._call_peppol_proxy(
|
|
endpoint='/api/peppol/1/activate_participant',
|
|
params=params,
|
|
edi_user=edi_user,
|
|
)
|
|
# once we sent the migration key over, we don't need it
|
|
# but we need the field for future in case the user decided to migrate away from Odoo
|
|
self.account_peppol_migration_key = False
|
|
|
|
@handle_demo
|
|
def button_update_peppol_user_data(self):
|
|
"""
|
|
Action for the user to be able to update their contact details any time
|
|
Calls /update_user on the iap server
|
|
"""
|
|
self.ensure_one()
|
|
|
|
if not self.account_peppol_contact_email or not self.account_peppol_phone_number:
|
|
raise ValidationError(_("Contact email and phone number are required."))
|
|
|
|
params = {
|
|
'update_data': {
|
|
'peppol_phone_number': self.account_peppol_phone_number,
|
|
'peppol_contact_email': self.account_peppol_contact_email,
|
|
}
|
|
}
|
|
|
|
self._call_peppol_proxy(
|
|
endpoint='/api/peppol/1/update_user',
|
|
params=params,
|
|
)
|
|
|
|
def button_send_peppol_verification_code(self):
|
|
"""
|
|
Request user verification via SMS
|
|
Calls the /send_verification_code to send the 6-digit verification code
|
|
"""
|
|
self.ensure_one()
|
|
|
|
# update contact details in case the user made changes
|
|
self.button_update_peppol_user_data()
|
|
|
|
self._call_peppol_proxy(
|
|
endpoint='/api/peppol/1/send_verification_code',
|
|
params={'message': _("Your confirmation code is")},
|
|
)
|
|
self.account_peppol_proxy_state = 'sent_verification'
|
|
|
|
def button_check_peppol_verification_code(self):
|
|
"""
|
|
Calls /verify_phone_number to compare user's input and the
|
|
code generated on the IAP server
|
|
"""
|
|
self.ensure_one()
|
|
|
|
if len(self.account_peppol_verification_code) != 6:
|
|
raise ValidationError(_("The verification code should contain six digits."))
|
|
|
|
self._call_peppol_proxy(
|
|
endpoint='/api/peppol/1/verify_phone_number',
|
|
params={'verification_code': self.account_peppol_verification_code},
|
|
)
|
|
self.account_peppol_proxy_state = 'pending'
|
|
self.account_peppol_verification_code = False
|
|
# in case they have already been activated on the IAP side
|
|
self.env.ref('account_peppol.ir_cron_peppol_get_participant_status')._trigger()
|
|
|
|
def button_cancel_peppol_registration(self):
|
|
"""
|
|
Sets the peppol registration to canceled
|
|
- If the user is active on the SMP, we can't just cancel it.
|
|
They have to request a migration key using the `button_migrate_peppol_registration` action
|
|
or deregister.
|
|
- 'not_registered', 'rejected', 'canceled' proxy states mean that canceling the registration
|
|
makes no sense, so we don't do it
|
|
- Calls the IAP server first before setting the state as canceled on the client side,
|
|
in case they've been activated on the IAP side in the meantime
|
|
"""
|
|
self.ensure_one()
|
|
# check if the participant has been already registered
|
|
self.account_peppol_edi_user._peppol_get_participant_status()
|
|
if not tools.config['test_enable'] and not modules.module.current_test:
|
|
self.env.cr.commit()
|
|
|
|
if self.account_peppol_proxy_state == 'active':
|
|
raise UserError(_("Can't cancel an active registration. Please request a migration or deregister instead."))
|
|
|
|
if self.account_peppol_proxy_state in {'not_registered', 'rejected', 'canceled'}:
|
|
raise UserError(_(
|
|
"Can't cancel registration with this status: %s", self.account_peppol_proxy_state
|
|
))
|
|
|
|
self._call_peppol_proxy(endpoint='/api/peppol/1/cancel_peppol_registration')
|
|
self.account_peppol_proxy_state = 'not_registered'
|
|
self.account_peppol_edi_user.unlink()
|
|
|
|
@handle_demo
|
|
def button_migrate_peppol_registration(self):
|
|
"""
|
|
If the user is active, they need to request a migration key, generated on the IAP server.
|
|
The migration key is then displayed in Peppol settings.
|
|
Currently, reopening after migrating away is not supported.
|
|
"""
|
|
self.ensure_one()
|
|
|
|
if self.account_peppol_proxy_state != 'active':
|
|
raise UserError(_(
|
|
"Can't migrate registration with this status: %s", self.account_peppol_proxy_state
|
|
))
|
|
|
|
response = self._call_peppol_proxy(endpoint='/api/peppol/1/migrate_peppol_registration')
|
|
self.account_peppol_migration_key = response['migration_key']
|
|
|
|
@handle_demo
|
|
def button_deregister_peppol_participant(self):
|
|
"""
|
|
Deregister the edi user from Peppol network
|
|
"""
|
|
self.ensure_one()
|
|
|
|
if self.account_peppol_proxy_state != 'active':
|
|
raise UserError(_(
|
|
"Can't deregister with this status: %s", self.account_peppol_proxy_state
|
|
))
|
|
|
|
# fetch all documents and message statuses before unlinking the edi user
|
|
# so that the invoices are acknowledged
|
|
self.env['account_edi_proxy_client.user']._cron_peppol_get_message_status()
|
|
self.env['account_edi_proxy_client.user']._cron_peppol_get_new_documents()
|
|
if not tools.config['test_enable'] and not modules.module.current_test:
|
|
self.env.cr.commit()
|
|
|
|
self._call_peppol_proxy(endpoint='/api/peppol/1/cancel_peppol_registration')
|
|
self.account_peppol_proxy_state = 'not_registered'
|
|
self.account_peppol_edi_user.unlink()
|