# -*- 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()