primeiro commit
This commit is contained in:
parent
bfcd5a0db6
commit
e1b0773fcb
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Angola NIF Validator (AGT)',
|
||||
'author': 'Hilari Tech LDA',
|
||||
'version': '18.0.1.0.0',
|
||||
'category': 'Accounting/Localizations',
|
||||
'summary': 'Validação automática do NIF angolano via API da AGT',
|
||||
'description': """
|
||||
Angola NIF Validator (AGT)
|
||||
==========================
|
||||
Integração com o endpoint oficial da AGT para validação de NIF.
|
||||
""",
|
||||
'license': 'LGPL-3',
|
||||
'depends': [
|
||||
'base',
|
||||
'contacts',
|
||||
'mail',
|
||||
'base_setup', # ADICIONADO: Necessário para as Configurações Gerais
|
||||
],
|
||||
'data': [
|
||||
'security/nif_validator_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'data/nif_validator_data.xml',
|
||||
'views/nif_log_view.xml', # Recomendado carregar o log antes do parceiro se houver campos relacionais
|
||||
'views/res_partner_view.xml',
|
||||
'views/res_config_settings_view.xml',
|
||||
'views/menu.xml',
|
||||
# 'data/nif_validator_rules.xml', # Certifica-te que este ficheiro existe ou comenta se estiver vazio
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'l10n_ao_nif_validator/static/src/css/nif_validator.css',
|
||||
'l10n_ao_nif_validator/static/src/js/nif_validator.js',
|
||||
],
|
||||
},
|
||||
'images': ['static/description/icon.png'],
|
||||
'installable': True,
|
||||
'application': True, # Podes colocar True se quiseres que apareça na lista principal de Apps
|
||||
'auto_install': False,
|
||||
}
|
||||
Binary file not shown.
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import main
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from odoo import http, _, fields
|
||||
from odoo.http import request
|
||||
from odoo.exceptions import ValidationError, UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NifValidatorController(http.Controller):
|
||||
_ROUTE = '/web/nif/validate'
|
||||
|
||||
@http.route(_ROUTE, type='json', auth='user', methods=['POST'], csrf=True)
|
||||
def validate_nif(self, nif=None, partner_id=None, **kwargs):
|
||||
if not nif:
|
||||
return self._error(_('O campo NIF é obrigatório.'))
|
||||
|
||||
nif = str(nif).strip()
|
||||
_logger.info('NIF Validator: requisição recebida | nif=%s | partner_id=%s', nif, partner_id)
|
||||
|
||||
try:
|
||||
partner = self._resolve_partner(partner_id)
|
||||
result = self._run_validation(partner, nif)
|
||||
return result
|
||||
except (ValidationError, UserError) as exc:
|
||||
return self._error(str(exc.args[0]))
|
||||
except Exception as exc:
|
||||
_logger.exception('NIF Validator: erro inesperado')
|
||||
return self._error(_('Ocorreu um erro interno. Contacte o administrador.'))
|
||||
|
||||
def _resolve_partner(self, partner_id):
|
||||
Partner = request.env['res.partner']
|
||||
if not partner_id:
|
||||
return Partner.browse([])
|
||||
try:
|
||||
partner_id = int(partner_id)
|
||||
except (TypeError, ValueError):
|
||||
raise UserError(_('ID de contacto inválido.'))
|
||||
|
||||
partner = Partner.browse(partner_id)
|
||||
if not partner.exists():
|
||||
raise UserError(_('O contacto com ID %d não foi encontrado.') % partner_id)
|
||||
return partner
|
||||
|
||||
# ESTA É A FUNÇÃO QUE DEVES SUBSTITUIR
|
||||
def _run_validation(self, partner, nif):
|
||||
if partner:
|
||||
# Atualiza o NIF e chama a lógica do modelo Python (res_partner.py)
|
||||
partner.sudo().write({'nif_ao': nif})
|
||||
partner.action_validate_nif()
|
||||
|
||||
if partner.nif_ao_validated:
|
||||
return self._success({
|
||||
'nif': partner.nif_ao,
|
||||
'nome': partner.name,
|
||||
'estado': partner.nif_ao_state or '',
|
||||
'tipo_contribuinte': partner.nif_ao_contributor_type or '',
|
||||
'ultima_validacao': fields.Datetime.to_string(partner.nif_ao_last_validation),
|
||||
})
|
||||
else:
|
||||
return self._error(_('Não foi possível validar o NIF %s na AGT.') % nif)
|
||||
else:
|
||||
return self._error(_('Para validar o NIF, o contacto deve estar previamente criado.'))
|
||||
|
||||
def _success(self, data):
|
||||
return {'success': True, 'data': data}
|
||||
|
||||
def _error(self, message):
|
||||
return {'success': False, 'error': str(message)}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo noupdate="1">
|
||||
<!--
|
||||
noupdate="1" garante que estes valores so sao inseridos na primeira
|
||||
instalacao. Atualizacoes do modulo NAO sobrescrevem configuracoes
|
||||
que o administrador tenha alterado manualmente.
|
||||
-->
|
||||
|
||||
<!-- URL base do endpoint AGT -->
|
||||
<record id="param_agt_endpoint_url" model="ir.config_parameter">
|
||||
<field name="key">l10n_ao_nif_validator.agt_endpoint_url</field>
|
||||
<field name="value">https://validador.agt.minfin.gov.ao</field>
|
||||
</record>
|
||||
|
||||
<!-- Timeout da requisicao HTTP (segundos) -->
|
||||
<record id="param_agt_request_timeout" model="ir.config_parameter">
|
||||
<field name="key">l10n_ao_nif_validator.agt_request_timeout</field>
|
||||
<field name="value">3</field>
|
||||
</record>
|
||||
|
||||
<!-- Validacao automatica activa por defeito -->
|
||||
<record id="param_agt_auto_validate" model="ir.config_parameter">
|
||||
<field name="key">l10n_ao_nif_validator.agt_auto_validate</field>
|
||||
<field name="value">True</field>
|
||||
</record>
|
||||
|
||||
<!-- Registo de logs activo por defeito -->
|
||||
<record id="param_agt_enable_logs" model="ir.config_parameter">
|
||||
<field name="key">l10n_ao_nif_validator.agt_enable_logs</field>
|
||||
<field name="value">True</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!--
|
||||
ir.rule carregadas DEPOIS do CSV de ACLs e dos dados padrão.
|
||||
Neste ponto o modelo nif.validation.log já existe na BD
|
||||
e os grupos já foram criados pelo security XML.
|
||||
-->
|
||||
|
||||
<!-- Utilizador: vê apenas logs de contactos da sua empresa -->
|
||||
<record id="rule_nif_log_user" model="ir.rule">
|
||||
<field name="name">Log NIF: acesso por empresa (utilizador)</field>
|
||||
<field name="model_id" ref="model_nif_validation_log"/>
|
||||
<field name="groups" eval="[(4, ref('l10n_ao_nif_validator.group_nif_ao_user'))]"/>
|
||||
<field name="domain_force">
|
||||
['|',
|
||||
('partner_id.company_id', '=', False),
|
||||
('partner_id.company_id', '=', company_id)
|
||||
]
|
||||
</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="False"/>
|
||||
<field name="perm_create" eval="False"/>
|
||||
<field name="perm_unlink" eval="False"/>
|
||||
</record>
|
||||
|
||||
<!-- Gestor: vê todos os logs sem restrição -->
|
||||
<record id="rule_nif_log_manager" model="ir.rule">
|
||||
<field name="name">Log NIF: acesso total (gestor)</field>
|
||||
<field name="model_id" ref="model_nif_validation_log"/>
|
||||
<field name="groups" eval="[(4, ref('l10n_ao_nif_validator.group_nif_ao_manager'))]"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from . import nif_log
|
||||
from . import res_partner
|
||||
from . import res_config_settings
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,29 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class NifValidationLog(models.Model):
|
||||
_name = 'nif.validation.log'
|
||||
_description = 'Log de Validação de NIF Angola'
|
||||
_order = 'create_date desc'
|
||||
_inherit = ['mail.thread'] # Para chatter funcionar como no teu XML
|
||||
|
||||
partner_id = fields.Many2one('res.partner', string="Contacto", readonly=True)
|
||||
nif = fields.Char(string="NIF", readonly=True)
|
||||
state = fields.Selection([
|
||||
('success', 'Sucesso'),
|
||||
('not_found', 'Não Encontrado'),
|
||||
('error', 'Erro'),
|
||||
('connection_error', 'Erro de Ligação')
|
||||
], string="Estado", readonly=True)
|
||||
|
||||
http_status_code = fields.Integer(string="Código HTTP", readonly=True)
|
||||
duration_ms = fields.Float(string="Duração (ms)", readonly=True)
|
||||
error_message = fields.Text(string="Mensagem de Erro", readonly=True)
|
||||
raw_response = fields.Text(string="Resposta JSON", readonly=True)
|
||||
is_success = fields.Boolean(string="Sucesso", readonly=True)
|
||||
|
||||
# Campos da resposta AGT
|
||||
response_nif = fields.Char(string="NIF (AGT)")
|
||||
response_name = fields.Char(string="Nome (AGT)")
|
||||
response_state = fields.Char(string="Estado Fiscal")
|
||||
response_type = fields.Char(string="Tipo Contribuinte")
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
from odoo import models, fields
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
nif_agt_endpoint_url = fields.Char(
|
||||
string="URL do Endpoint AGT",
|
||||
config_parameter='l10n_ao_nif_validator.agt_endpoint_url'
|
||||
)
|
||||
nif_agt_request_timeout = fields.Integer(
|
||||
string="Timeout (segundos)",
|
||||
config_parameter='l10n_ao_nif_validator.agt_request_timeout'
|
||||
)
|
||||
nif_agt_auto_validate = fields.Boolean(
|
||||
string="Validação Automática",
|
||||
config_parameter='l10n_ao_nif_validator.agt_auto_validate'
|
||||
)
|
||||
nif_agt_enable_logs = fields.Boolean(
|
||||
string="Activar Registo de Logs",
|
||||
config_parameter='l10n_ao_nif_validator.agt_enable_logs'
|
||||
)
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import requests
|
||||
import re
|
||||
import urllib3
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
# 1. CAMPOS
|
||||
nif_ao = fields.Char(string="NIF Angola", copy=False)
|
||||
nif_ao_validated = fields.Boolean(string="NIF Validado", default=False, copy=False, readonly=True)
|
||||
nif_ao_state = fields.Char(string="Estado Fiscal (AGT)", readonly=True)
|
||||
nif_ao_contributor_type = fields.Char(string="Tipo de Contribuinte", readonly=True)
|
||||
nif_ao_inadimplente = fields.Char(string="Inadimplente", readonly=True)
|
||||
nif_ao_regime_iva = fields.Char(string="Regime de IVA", readonly=True)
|
||||
nif_ao_last_validation = fields.Datetime(string="Última Validação", readonly=True)
|
||||
nif_ao_validation_count = fields.Integer(string="Consultas", compute='_compute_nif_logs_count')
|
||||
|
||||
# Contagem dos logs para mostrar no botão inteligente
|
||||
def _compute_nif_logs_count(self):
|
||||
for record in self:
|
||||
if self.env.get('nif.validation.log'):
|
||||
record.nif_ao_validation_count = self.env['nif.validation.log'].search_count([
|
||||
('partner_id', '=', record.id)
|
||||
])
|
||||
else:
|
||||
record.nif_ao_validation_count = 0
|
||||
|
||||
# Abre a janela do histórico
|
||||
def action_open_nif_logs(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Logs de Consulta NIF'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'nif.validation.log',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('partner_id', '=', self.id)],
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
# NOVO: Função para apagar o histórico de consultas deste cliente
|
||||
def action_clear_nif_logs(self):
|
||||
self.ensure_one()
|
||||
if self.env.get('nif.validation.log'):
|
||||
logs = self.env['nif.validation.log'].search([('partner_id', '=', self.id)])
|
||||
logs.unlink() # Apaga os registos da base de dados
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Histórico Apagado'),
|
||||
'message': _('Todo o histórico de consultas deste NIF foi apagado.'),
|
||||
'type': 'warning',
|
||||
}
|
||||
}
|
||||
|
||||
# 2. LÓGICA DE EXTRAÇÃO (API AGT)
|
||||
@api.onchange('nif_ao')
|
||||
def _onchange_nif_ao_fetch_agt(self):
|
||||
if not self.nif_ao or len(self.nif_ao.strip()) < 9:
|
||||
return
|
||||
|
||||
nif = self.nif_ao.strip()
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
url = f"https://portaldocontribuinte.minfin.gov.ao/consultar-nif-do-contribuinte?nif={nif}"
|
||||
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/122.0.0.0'}
|
||||
|
||||
try:
|
||||
_logger.info(">>> A consultar AGT para NIF: %s", nif)
|
||||
response = requests.get(url, headers=headers, timeout=15, verify=False)
|
||||
|
||||
if response.status_code == 200 and response.text:
|
||||
html = response.text
|
||||
clean_text = re.sub(r'<[^>]+>', ' ', html)
|
||||
clean_text = re.sub(r'\s+', ' ', clean_text).strip()
|
||||
|
||||
nome_m = re.search(r'Nome:\s*(.*?)\s*Tipo:', clean_text, re.IGNORECASE)
|
||||
tipo_m = re.search(r'Tipo:\s*(.*?)\s*Estado:', clean_text, re.IGNORECASE)
|
||||
est_m = re.search(r'Estado:\s*(.*?)\s*Inadimplente:', clean_text, re.IGNORECASE)
|
||||
inad_m = re.search(r'Inadimplente:\s*(.*?)\s*Regime de IVA:', clean_text, re.IGNORECASE)
|
||||
iva_m = re.search(r'Regime de IVA:\s*(.*?)\s*Residente Fiscal', clean_text, re.IGNORECASE)
|
||||
|
||||
if nome_m:
|
||||
self.name = nome_m.group(1).strip()
|
||||
if tipo_m:
|
||||
self.nif_ao_contributor_type = tipo_m.group(1).strip()
|
||||
if est_m:
|
||||
self.nif_ao_state = est_m.group(1).strip()
|
||||
if inad_m:
|
||||
self.nif_ao_inadimplente = inad_m.group(1).strip()
|
||||
if iva_m:
|
||||
self.nif_ao_regime_iva = iva_m.group(1).strip()
|
||||
|
||||
self.nif_ao_validated = True
|
||||
self.nif_ao_last_validation = fields.Datetime.now()
|
||||
|
||||
except Exception as e:
|
||||
_logger.error("Erro na busca: %s", str(e))
|
||||
|
||||
# 3. AÇÃO DO BOTÃO "CONSULTAR" (Agora regista os Logs a sério!)
|
||||
def action_validate_nif(self):
|
||||
self.ensure_one()
|
||||
if not self.nif_ao:
|
||||
raise UserError(_("Por favor, preencha o campo NIF Angola primeiro."))
|
||||
|
||||
# 1. Faz a pesquisa no portal da AGT
|
||||
self._onchange_nif_ao_fetch_agt()
|
||||
|
||||
# 2. Se a pesquisa correu bem, cria o registo no Histórico (Log)
|
||||
if self.nif_ao_validated:
|
||||
if self.env.get('nif.validation.log'):
|
||||
self.env['nif.validation.log'].create({
|
||||
'partner_id': self.id,
|
||||
'nif': self.nif_ao,
|
||||
'state': 'Sucesso',
|
||||
'raw_response': f"Nome: {self.name} | Estado: {self.nif_ao_state} | IVA: {self.nif_ao_regime_iva}"
|
||||
})
|
||||
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Sucesso'),
|
||||
'message': _('Dados da AGT consultados com sucesso e registados no histórico.'),
|
||||
'type': 'success',
|
||||
}
|
||||
}
|
||||
else:
|
||||
raise UserError(_("Não foi possível aceder à AGT. Verifique o NIF."))
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_nif_log_user,nif.validation.log.user,model_nif_validation_log,group_nif_ao_user,1,0,0,0
|
||||
access_nif_log_manager,nif.validation.log.manager,model_nif_validation_log,group_nif_ao_manager,1,1,1,1
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1"> <record id="module_category_nif_ao" model="ir.module.category">
|
||||
<field name="name">Validação NIF Angola (AGT)</field>
|
||||
<field name="description">Controle de níveis de acesso para consulta de NIFs na AGT.</field>
|
||||
<field name="sequence">90</field>
|
||||
</record>
|
||||
|
||||
<record id="group_nif_ao_user" model="res.groups">
|
||||
<field name="name">Utilizador</field> <field name="category_id" ref="module_category_nif_ao"/>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_nif_ao_manager" model="res.groups">
|
||||
<field name="name">Gestor</field>
|
||||
<field name="category_id" ref="module_category_nif_ao"/>
|
||||
<field name="implied_ids" eval="[(4, ref('group_nif_ao_user'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,382 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Testes unitarios — integracao com a API AGT
|
||||
Foco: logica de chamada HTTP, tratamento de erros e registo de logs
|
||||
|
||||
Todos os testes que tocam a rede usam unittest.mock.patch para interceptar
|
||||
requests.get e simular respostas sem precisar de conectividade real.
|
||||
"""
|
||||
|
||||
import json
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
# Caminho completo do metodo a fazer mock dentro do modulo res.partner
|
||||
_REQUESTS_GET = 'odoo.addons.l10n_ao_nif_validator.models.res_partner.requests.get'
|
||||
|
||||
|
||||
def _mock_response(status_code=200, json_data=None, raise_exc=None):
|
||||
"""
|
||||
Fabrica um objeto mock que imita requests.Response.
|
||||
|
||||
:param status_code: codigo HTTP a retornar
|
||||
:param json_data: dict retornado por response.json()
|
||||
:param raise_exc: excecao a lancar ao chamar requests.get (timeout, etc.)
|
||||
"""
|
||||
mock = MagicMock()
|
||||
mock.status_code = status_code
|
||||
if json_data is not None:
|
||||
mock.json.return_value = json_data
|
||||
if raise_exc:
|
||||
mock.side_effect = raise_exc
|
||||
return mock
|
||||
|
||||
|
||||
class TestNifFormatValidation(TransactionCase):
|
||||
"""
|
||||
Grupo 1 — Validacao de formato local (sem chamada HTTP)
|
||||
Testa o metodo estatico _is_valid_nif_format diretamente.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.Partner = self.env['res.partner']
|
||||
|
||||
# --- Formatos validos -------------------------------------------------------
|
||||
|
||||
def test_valid_nif_10_digits(self):
|
||||
"""NIF com exactamente 10 digitos deve ser valido."""
|
||||
self.assertTrue(self.Partner._is_valid_nif_format('5001234567'))
|
||||
|
||||
def test_valid_nif_starts_with_zero(self):
|
||||
"""NIF iniciado por zero deve ser aceite (e uma string, nao inteiro)."""
|
||||
self.assertTrue(self.Partner._is_valid_nif_format('0001234567'))
|
||||
|
||||
# --- Formatos invalidos -----------------------------------------------------
|
||||
|
||||
def test_invalid_nif_too_short(self):
|
||||
"""NIF com menos de 10 digitos deve ser invalido."""
|
||||
self.assertFalse(self.Partner._is_valid_nif_format('500123'))
|
||||
|
||||
def test_invalid_nif_too_long(self):
|
||||
"""NIF com mais de 10 digitos deve ser invalido."""
|
||||
self.assertFalse(self.Partner._is_valid_nif_format('50012345678'))
|
||||
|
||||
def test_invalid_nif_with_letters(self):
|
||||
"""NIF com letras deve ser invalido."""
|
||||
self.assertFalse(self.Partner._is_valid_nif_format('500123456A'))
|
||||
|
||||
def test_invalid_nif_with_spaces(self):
|
||||
"""NIF com espacos deve ser invalido."""
|
||||
self.assertFalse(self.Partner._is_valid_nif_format('500 123456'))
|
||||
|
||||
def test_invalid_nif_with_dots(self):
|
||||
"""NIF com pontos (formatado) deve ser invalido."""
|
||||
self.assertFalse(self.Partner._is_valid_nif_format('500.123.456'))
|
||||
|
||||
def test_invalid_nif_empty(self):
|
||||
"""NIF vazio deve ser invalido."""
|
||||
self.assertFalse(self.Partner._is_valid_nif_format(''))
|
||||
|
||||
def test_invalid_nif_none(self):
|
||||
"""NIF None deve ser invalido sem lancar excecao."""
|
||||
self.assertFalse(self.Partner._is_valid_nif_format(None))
|
||||
|
||||
|
||||
class TestAgtApiCall(TransactionCase):
|
||||
"""
|
||||
Grupo 2 — Chamada HTTP ao endpoint AGT
|
||||
Testa _fetch_and_fill_agt_data com respostas simuladas.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# Configura o URL do endpoint para que os testes nao falhem
|
||||
# por falta de configuracao
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_endpoint_url',
|
||||
'https://mock.agt.test'
|
||||
)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_request_timeout', '3'
|
||||
)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_enable_logs', 'True'
|
||||
)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_auto_validate', 'True'
|
||||
)
|
||||
|
||||
self.partner = self.env['res.partner'].create({
|
||||
'name' : 'Parceiro Teste',
|
||||
'nif_ao' : '5001234567',
|
||||
})
|
||||
|
||||
# --- Resposta 200 (sucesso) -------------------------------------------------
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_api_success_fills_partner_fields(self, mock_get):
|
||||
"""Resposta 200 deve preencher nome, estado e tipo no parceiro."""
|
||||
mock_get.return_value = _mock_response(200, {
|
||||
'nif' : '5001234567',
|
||||
'nome' : 'Hilari Tech LDA',
|
||||
'estado' : 'Ativo',
|
||||
'tipo_contribuinte': 'Empresa',
|
||||
})
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
self.assertEqual(self.partner.name, 'Hilari Tech LDA')
|
||||
self.assertEqual(self.partner.nif_ao_state, 'Ativo')
|
||||
self.assertEqual(self.partner.nif_ao_contributor_type, 'Empresa')
|
||||
self.assertTrue(self.partner.nif_ao_validated)
|
||||
self.assertIsNotNone(self.partner.nif_ao_last_validation)
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_api_success_partial_data(self, mock_get):
|
||||
"""Resposta 200 com campos opcionais em falta nao deve lancar erro."""
|
||||
mock_get.return_value = _mock_response(200, {
|
||||
'nif' : '5001234567',
|
||||
'nome': 'Empresa Parcial LDA',
|
||||
# 'estado' e 'tipo_contribuinte' ausentes intencionalmente
|
||||
})
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
self.assertEqual(self.partner.name, 'Empresa Parcial LDA')
|
||||
self.assertTrue(self.partner.nif_ao_validated)
|
||||
|
||||
# --- Resposta 404 (nao encontrado) ------------------------------------------
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_api_404_partner_not_validated(self, mock_get):
|
||||
"""Resposta 404 nao deve marcar o parceiro como validado."""
|
||||
mock_get.return_value = _mock_response(404)
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
self.assertFalse(self.partner.nif_ao_validated)
|
||||
|
||||
# --- Outros codigos de erro HTTP -------------------------------------------
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_api_500_partner_not_validated(self, mock_get):
|
||||
"""Resposta 500 nao deve marcar o parceiro como validado."""
|
||||
mock_get.return_value = _mock_response(500)
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
self.assertFalse(self.partner.nif_ao_validated)
|
||||
|
||||
# --- Timeout ----------------------------------------------------------------
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_api_timeout_partner_not_validated(self, mock_get):
|
||||
"""Timeout na requisicao nao deve marcar o parceiro como validado."""
|
||||
import requests as req_lib
|
||||
mock_get.side_effect = req_lib.exceptions.Timeout()
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
self.assertFalse(self.partner.nif_ao_validated)
|
||||
|
||||
# --- Erro de conexao --------------------------------------------------------
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_api_connection_error_partner_not_validated(self, mock_get):
|
||||
"""Erro de conexao nao deve marcar o parceiro como validado."""
|
||||
import requests as req_lib
|
||||
mock_get.side_effect = req_lib.exceptions.ConnectionError("recusada")
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
self.assertFalse(self.partner.nif_ao_validated)
|
||||
|
||||
# --- Resposta JSON invalida -------------------------------------------------
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_api_invalid_json_partner_not_validated(self, mock_get):
|
||||
"""Resposta 200 com JSON invalido nao deve marcar o parceiro como validado."""
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status_code = 200
|
||||
mock_resp.json.side_effect = ValueError("JSON invalido")
|
||||
mock_resp.text = "<html>erro</html>"
|
||||
mock_get.return_value = mock_resp
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
self.assertFalse(self.partner.nif_ao_validated)
|
||||
|
||||
# --- Endpoint nao configurado -----------------------------------------------
|
||||
|
||||
def test_missing_endpoint_raises_user_error(self):
|
||||
"""Sem URL configurado deve lancar UserError."""
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_endpoint_url', ''
|
||||
)
|
||||
|
||||
with self.assertRaises(UserError):
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
|
||||
class TestNifValidationLogs(TransactionCase):
|
||||
"""
|
||||
Grupo 3 — Registo de logs (nif.validation.log)
|
||||
Verifica que os logs sao criados correctamente em cada cenario.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_endpoint_url', 'https://mock.agt.test'
|
||||
)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_enable_logs', 'True'
|
||||
)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_request_timeout', '3'
|
||||
)
|
||||
|
||||
self.partner = self.env['res.partner'].create({
|
||||
'name' : 'Parceiro Log Teste',
|
||||
'nif_ao' : '5009876543',
|
||||
})
|
||||
self.NifLog = self.env['nif.validation.log']
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_log_created_on_success(self, mock_get):
|
||||
"""Um log com state='success' deve ser criado apos validacao bem-sucedida."""
|
||||
mock_get.return_value = _mock_response(200, {
|
||||
'nif' : '5009876543',
|
||||
'nome': 'Teste Log LDA',
|
||||
'estado': 'Ativo',
|
||||
'tipo_contribuinte': 'Empresa',
|
||||
})
|
||||
|
||||
count_before = self.NifLog.search_count([
|
||||
('partner_id', '=', self.partner.id),
|
||||
('state', '=', 'success'),
|
||||
])
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
count_after = self.NifLog.search_count([
|
||||
('partner_id', '=', self.partner.id),
|
||||
('state', '=', 'success'),
|
||||
])
|
||||
|
||||
self.assertEqual(count_after, count_before + 1)
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_log_created_on_404(self, mock_get):
|
||||
"""Um log com state='not_found' deve ser criado quando AGT retorna 404."""
|
||||
mock_get.return_value = _mock_response(404)
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
log = self.NifLog.search([
|
||||
('partner_id', '=', self.partner.id),
|
||||
('state', '=', 'not_found'),
|
||||
], limit=1)
|
||||
|
||||
self.assertTrue(bool(log), "Log 'not_found' nao foi criado")
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_log_created_on_timeout(self, mock_get):
|
||||
"""Um log com state='timeout' deve ser criado em caso de timeout."""
|
||||
import requests as req_lib
|
||||
mock_get.side_effect = req_lib.exceptions.Timeout()
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
log = self.NifLog.search([
|
||||
('partner_id', '=', self.partner.id),
|
||||
('state', '=', 'timeout'),
|
||||
], limit=1)
|
||||
|
||||
self.assertTrue(bool(log), "Log 'timeout' nao foi criado")
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_log_created_on_connection_error(self, mock_get):
|
||||
"""Um log com state='connection_error' deve ser criado em erros de rede."""
|
||||
import requests as req_lib
|
||||
mock_get.side_effect = req_lib.exceptions.ConnectionError()
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
log = self.NifLog.search([
|
||||
('partner_id', '=', self.partner.id),
|
||||
('state', '=', 'connection_error'),
|
||||
], limit=1)
|
||||
|
||||
self.assertTrue(bool(log), "Log 'connection_error' nao foi criado")
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_no_log_when_logs_disabled(self, mock_get):
|
||||
"""Nenhum log deve ser criado quando agt_enable_logs=False."""
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_enable_logs', 'False'
|
||||
)
|
||||
mock_get.return_value = _mock_response(200, {
|
||||
'nif' : '5009876543',
|
||||
'nome': 'Sem Log LDA',
|
||||
'estado': 'Ativo',
|
||||
'tipo_contribuinte': 'Empresa',
|
||||
})
|
||||
|
||||
count_before = self.NifLog.search_count([
|
||||
('partner_id', '=', self.partner.id),
|
||||
])
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
count_after = self.NifLog.search_count([
|
||||
('partner_id', '=', self.partner.id),
|
||||
])
|
||||
|
||||
self.assertEqual(count_after, count_before, "Log criado com logs desactivados")
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_log_contains_nif_and_partner(self, mock_get):
|
||||
"""O log de sucesso deve conter o NIF consultado e o partner correcto."""
|
||||
mock_get.return_value = _mock_response(200, {
|
||||
'nif' : '5009876543',
|
||||
'nome': 'Dados Log LDA',
|
||||
'estado': 'Ativo',
|
||||
'tipo_contribuinte': 'Empresa',
|
||||
})
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
log = self.NifLog.search([
|
||||
('partner_id', '=', self.partner.id),
|
||||
('nif', '=', '5009876543'),
|
||||
('state', '=', 'success'),
|
||||
], limit=1)
|
||||
|
||||
self.assertTrue(bool(log))
|
||||
self.assertEqual(log.nif, '5009876543')
|
||||
self.assertEqual(log.partner_id, self.partner)
|
||||
self.assertEqual(log.response_name, 'Dados Log LDA')
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_log_duration_ms_recorded(self, mock_get):
|
||||
"""O log deve conter a duracao da chamada em milissegundos (>= 0)."""
|
||||
mock_get.return_value = _mock_response(200, {
|
||||
'nif' : '5009876543',
|
||||
'nome': 'Duracao LDA',
|
||||
'estado': 'Ativo',
|
||||
'tipo_contribuinte': 'Empresa',
|
||||
})
|
||||
|
||||
self.partner._fetch_and_fill_agt_data()
|
||||
|
||||
log = self.NifLog.search([
|
||||
('partner_id', '=', self.partner.id),
|
||||
('state', '=', 'success'),
|
||||
], order='create_date desc', limit=1)
|
||||
|
||||
self.assertGreaterEqual(log.duration_ms, 0)
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Testes de integracao — modelo res.partner
|
||||
Foco: campos NIF, accoes do formulario, validacao manual e reset de campos.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from odoo.tests.common import TransactionCase
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
|
||||
_REQUESTS_GET = 'odoo.addons.l10n_ao_nif_validator.models.res_partner.requests.get'
|
||||
|
||||
|
||||
def _agt_ok(nif='5001234567', nome='Empresa Teste LDA',
|
||||
estado='Ativo', tipo='Empresa'):
|
||||
"""Atalho para uma resposta AGT bem-sucedida."""
|
||||
mock = MagicMock()
|
||||
mock.status_code = 200
|
||||
mock.json.return_value = {
|
||||
'nif' : nif,
|
||||
'nome' : nome,
|
||||
'estado' : estado,
|
||||
'tipo_contribuinte': tipo,
|
||||
}
|
||||
return mock
|
||||
|
||||
|
||||
class TestResPartnerFields(TransactionCase):
|
||||
"""
|
||||
Grupo 1 — Campos NIF no modelo res.partner
|
||||
Verifica que os campos existem e tem os defaults correctos.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.partner = self.env['res.partner'].create({'name': 'Teste Campos'})
|
||||
|
||||
def test_nif_ao_field_exists(self):
|
||||
"""Campo nif_ao deve existir no modelo."""
|
||||
self.assertIn('nif_ao', self.env['res.partner']._fields)
|
||||
|
||||
def test_nif_ao_validated_default_false(self):
|
||||
"""nif_ao_validated deve ser False por defeito."""
|
||||
self.assertFalse(self.partner.nif_ao_validated)
|
||||
|
||||
def test_nif_ao_state_default_empty(self):
|
||||
"""nif_ao_state deve estar vazio por defeito."""
|
||||
self.assertFalse(self.partner.nif_ao_state)
|
||||
|
||||
def test_nif_ao_contributor_type_default_empty(self):
|
||||
"""nif_ao_contributor_type deve estar vazio por defeito."""
|
||||
self.assertFalse(self.partner.nif_ao_contributor_type)
|
||||
|
||||
def test_nif_ao_last_validation_default_empty(self):
|
||||
"""nif_ao_last_validation deve estar vazio por defeito."""
|
||||
self.assertFalse(self.partner.nif_ao_last_validation)
|
||||
|
||||
def test_nif_ao_validation_count_default_zero(self):
|
||||
"""nif_ao_validation_count deve ser 0 para parceiro sem historico."""
|
||||
self.assertEqual(self.partner.nif_ao_validation_count, 0)
|
||||
|
||||
|
||||
class TestResPartnerValidation(TransactionCase):
|
||||
"""
|
||||
Grupo 2 — Accao action_validate_nif (botao manual)
|
||||
Verifica comportamento correcto com e sem NIF preenchido.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_endpoint_url', 'https://mock.agt.test'
|
||||
)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_request_timeout', '3'
|
||||
)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_enable_logs', 'True'
|
||||
)
|
||||
|
||||
def test_action_validate_nif_raises_if_no_nif(self):
|
||||
"""action_validate_nif deve lancar UserError se nif_ao estiver vazio."""
|
||||
partner = self.env['res.partner'].create({'name': 'Sem NIF'})
|
||||
with self.assertRaises(UserError):
|
||||
partner.action_validate_nif()
|
||||
|
||||
def test_action_validate_nif_raises_if_format_invalid(self):
|
||||
"""action_validate_nif deve lancar ValidationError se formato invalido."""
|
||||
partner = self.env['res.partner'].create({
|
||||
'name' : 'NIF Invalido',
|
||||
'nif_ao': '123',
|
||||
})
|
||||
with self.assertRaises(ValidationError):
|
||||
partner.action_validate_nif()
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_action_validate_nif_returns_notification(self, mock_get):
|
||||
"""action_validate_nif deve retornar uma accao de notificacao."""
|
||||
mock_get.return_value = _agt_ok()
|
||||
partner = self.env['res.partner'].create({
|
||||
'name' : 'Com NIF',
|
||||
'nif_ao': '5001234567',
|
||||
})
|
||||
result = partner.action_validate_nif()
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertEqual(result.get('type'), 'ir.actions.client')
|
||||
self.assertEqual(result.get('tag'), 'display_notification')
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_action_validate_nif_success_sets_validated(self, mock_get):
|
||||
"""Validacao bem-sucedida deve definir nif_ao_validated=True."""
|
||||
mock_get.return_value = _agt_ok(nome='Hilari Tech LDA')
|
||||
partner = self.env['res.partner'].create({
|
||||
'name' : 'Antes da Validacao',
|
||||
'nif_ao': '5001234567',
|
||||
})
|
||||
partner.action_validate_nif()
|
||||
self.assertTrue(partner.nif_ao_validated)
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_partner_name_updated_from_agt(self, mock_get):
|
||||
"""O nome do parceiro deve ser actualizado com o nome retornado pela AGT."""
|
||||
mock_get.return_value = _agt_ok(nome='Novo Nome AGT LDA')
|
||||
partner = self.env['res.partner'].create({
|
||||
'name' : 'Nome Antigo',
|
||||
'nif_ao': '5001234567',
|
||||
})
|
||||
partner.action_validate_nif()
|
||||
self.assertEqual(partner.name, 'Novo Nome AGT LDA')
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_partner_state_filled_from_agt(self, mock_get):
|
||||
"""nif_ao_state deve ser preenchido com o estado retornado pela AGT."""
|
||||
mock_get.return_value = _agt_ok(estado='Ativo')
|
||||
partner = self.env['res.partner'].create({
|
||||
'name' : 'Estado Teste',
|
||||
'nif_ao': '5001234567',
|
||||
})
|
||||
partner.action_validate_nif()
|
||||
self.assertEqual(partner.nif_ao_state, 'Ativo')
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_partner_type_filled_from_agt(self, mock_get):
|
||||
"""nif_ao_contributor_type deve ser preenchido com o tipo retornado pela AGT."""
|
||||
mock_get.return_value = _agt_ok(tipo='Empresa')
|
||||
partner = self.env['res.partner'].create({
|
||||
'name' : 'Tipo Teste',
|
||||
'nif_ao': '5001234567',
|
||||
})
|
||||
partner.action_validate_nif()
|
||||
self.assertEqual(partner.nif_ao_contributor_type, 'Empresa')
|
||||
|
||||
|
||||
class TestResPartnerResetFields(TransactionCase):
|
||||
"""
|
||||
Grupo 3 — Reset de campos ao alterar o NIF
|
||||
Verifica que _reset_nif_fields limpa os dados anteriores.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_endpoint_url', 'https://mock.agt.test'
|
||||
)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_request_timeout', '3'
|
||||
)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_enable_logs', 'False'
|
||||
)
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_reset_clears_validated_flag(self, mock_get):
|
||||
"""_reset_nif_fields deve limpar nif_ao_validated."""
|
||||
mock_get.return_value = _agt_ok()
|
||||
partner = self.env['res.partner'].create({
|
||||
'name' : 'Reset Teste',
|
||||
'nif_ao': '5001234567',
|
||||
})
|
||||
partner._fetch_and_fill_agt_data()
|
||||
self.assertTrue(partner.nif_ao_validated)
|
||||
|
||||
# Simula alteracao do NIF — chama o reset
|
||||
partner._reset_nif_fields()
|
||||
self.assertFalse(partner.nif_ao_validated)
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_reset_clears_state_and_type(self, mock_get):
|
||||
"""_reset_nif_fields deve limpar nif_ao_state e nif_ao_contributor_type."""
|
||||
mock_get.return_value = _agt_ok(estado='Ativo', tipo='Empresa')
|
||||
partner = self.env['res.partner'].create({
|
||||
'name' : 'Reset State Tipo',
|
||||
'nif_ao': '5001234567',
|
||||
})
|
||||
partner._fetch_and_fill_agt_data()
|
||||
self.assertTrue(partner.nif_ao_state)
|
||||
|
||||
partner._reset_nif_fields()
|
||||
self.assertFalse(partner.nif_ao_state)
|
||||
self.assertFalse(partner.nif_ao_contributor_type)
|
||||
|
||||
|
||||
class TestResPartnerLogLink(TransactionCase):
|
||||
"""
|
||||
Grupo 4 — Ligacao entre res.partner e nif.validation.log
|
||||
Verifica o One2many e o smart button de contagem.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_endpoint_url', 'https://mock.agt.test'
|
||||
)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_request_timeout', '3'
|
||||
)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'l10n_ao_nif_validator.agt_enable_logs', 'True'
|
||||
)
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_validation_count_increments(self, mock_get):
|
||||
"""Cada validacao deve incrementar nif_ao_validation_count."""
|
||||
mock_get.return_value = _agt_ok()
|
||||
partner = self.env['res.partner'].create({
|
||||
'name' : 'Contador',
|
||||
'nif_ao': '5001234567',
|
||||
})
|
||||
|
||||
self.assertEqual(partner.nif_ao_validation_count, 0)
|
||||
partner._fetch_and_fill_agt_data()
|
||||
self.assertEqual(partner.nif_ao_validation_count, 1)
|
||||
partner._fetch_and_fill_agt_data()
|
||||
self.assertEqual(partner.nif_ao_validation_count, 2)
|
||||
|
||||
@patch(_REQUESTS_GET)
|
||||
def test_action_open_nif_logs_returns_window(self, mock_get):
|
||||
"""action_open_nif_logs deve retornar um act_window filtrado pelo partner."""
|
||||
mock_get.return_value = _agt_ok()
|
||||
partner = self.env['res.partner'].create({
|
||||
'name' : 'Abrir Logs',
|
||||
'nif_ao': '5001234567',
|
||||
})
|
||||
partner._fetch_and_fill_agt_data()
|
||||
|
||||
result = partner.action_open_nif_logs()
|
||||
|
||||
self.assertEqual(result['type'], 'ir.actions.act_window')
|
||||
self.assertEqual(result['res_model'], 'nif.validation.log')
|
||||
self.assertIn(('partner_id', '=', partner.id), result['domain'])
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<menuitem id="menu_nif_ao_root"
|
||||
name="Validação NIF (AGT)"
|
||||
parent="contacts.menu_contacts"
|
||||
sequence="99"
|
||||
groups="base.group_user"/>
|
||||
|
||||
<menuitem id="menu_nif_ao_logs"
|
||||
name="Histórico de Consultas"
|
||||
parent="menu_nif_ao_root"
|
||||
action="action_nif_validation_log"
|
||||
sequence="10"
|
||||
groups="base.group_user"/>
|
||||
|
||||
<menuitem id="menu_nif_ao_settings"
|
||||
name="Configurações AGT"
|
||||
parent="menu_nif_ao_root"
|
||||
action="base_setup.action_general_configuration"
|
||||
sequence="20"
|
||||
groups="base.group_system"/>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_nif_log_tree" model="ir.ui.view">
|
||||
<field name="name">nif.validation.log.list</field>
|
||||
<field name="model">nif.validation.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Logs de Validação NIF" default_order="create_date desc" create="false" edit="false">
|
||||
<field name="create_date" string="Data"/>
|
||||
<field name="nif" string="NIF"/>
|
||||
<field name="partner_id" string="Contacto"/>
|
||||
<field name="state" string="Estado" widget="badge"
|
||||
decoration-success="state == 'success'"
|
||||
decoration-danger="state == 'error'"
|
||||
decoration-warning="state == 'not_found'"/>
|
||||
<field name="duration_ms" string="Duração (ms)"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_nif_log_kanban" model="ir.ui.view">
|
||||
<field name="name">nif.validation.log.kanban</field>
|
||||
<field name="model">nif.validation.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban class="o_kanban_mobile" create="false" edit="false">
|
||||
<field name="nif"/>
|
||||
<field name="state"/>
|
||||
<field name="response_name"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
<div class="o_kanban_record_title">
|
||||
<strong>NIF: <field name="nif"/></strong>
|
||||
</div>
|
||||
<div>Nome: <field name="response_name"/></div>
|
||||
<div>Data: <field name="create_date"/></div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<field name="state" widget="badge" decoration-success="state == 'success'"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_nif_log_form" model="ir.ui.view">
|
||||
<field name="name">nif.validation.log.form</field>
|
||||
<field name="model">nif.validation.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Detalhes do Log" create="false" edit="false">
|
||||
<sheet>
|
||||
<group>
|
||||
<group string="Dados da Consulta">
|
||||
<field name="create_date" readonly="1"/>
|
||||
<field name="nif" readonly="1"/>
|
||||
<field name="partner_id" readonly="1"/>
|
||||
</group>
|
||||
<group string="Status">
|
||||
<field name="state" readonly="1" widget="badge"/>
|
||||
<field name="is_success" readonly="1" widget="boolean_toggle"/>
|
||||
<field name="http_status_code" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<group string="Resposta AGT" invisible="not response_name">
|
||||
<field name="response_nif" readonly="1"/>
|
||||
<field name="response_name" readonly="1"/>
|
||||
<field name="response_state" readonly="1"/>
|
||||
</group>
|
||||
<group string="Erro/Raw" invisible="not error_message">
|
||||
<field name="error_message" readonly="1"/>
|
||||
<field name="raw_response" readonly="1"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_ids"/>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_nif_validation_log" model="ir.actions.act_window">
|
||||
<field name="name">Logs de Validação NIF</field>
|
||||
<field name="res_model">nif.validation.log</field>
|
||||
<field name="view_mode">list,kanban,form</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_res_config_settings_nif_ao" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.nif.ao</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//form" position="inside">
|
||||
|
||||
<app string="NIF Angola (AGT)"
|
||||
name="l10n_ao_nif_validator"
|
||||
data-string="NIF Angola (AGT)"
|
||||
data-key="l10n_ao_nif_validator">
|
||||
|
||||
<block title="Configurações do Validador NIF AGT" id="nif_ao_validation_block">
|
||||
<div class="text-muted">
|
||||
Parâmetros de integração com o endpoint oficial da Administração Geral Tributária (AGT) de Angola.
|
||||
</div>
|
||||
|
||||
<div class="row mt16 o_settings_container">
|
||||
<setting id="nif_agt_endpoint_url"
|
||||
string="URL do Endpoint AGT"
|
||||
help="URL base do serviço de validação de NIF. Ex: https://validador.agt.minfin.gov.ao">
|
||||
<field name="nif_agt_endpoint_url"
|
||||
placeholder="https://validador.agt.minfin.gov.ao"/>
|
||||
</setting>
|
||||
|
||||
<setting id="nif_agt_request_timeout"
|
||||
string="Timeout (segundos)"
|
||||
help="Tempo máximo de espera pela resposta da AGT (Recomendado: 3s).">
|
||||
<field name="nif_agt_request_timeout"/>
|
||||
</setting>
|
||||
|
||||
<setting id="nif_agt_auto_validate"
|
||||
string="Validação Automática"
|
||||
help="O NIF é consultado automaticamente ao salvar um contacto.">
|
||||
<field name="nif_agt_auto_validate" />
|
||||
</setting>
|
||||
|
||||
<setting id="nif_agt_enable_logs"
|
||||
string="Activar Registo de Logs"
|
||||
help="Guarda todas as consultas à API no histórico.">
|
||||
<field name="nif_agt_enable_logs" />
|
||||
</setting>
|
||||
</div>
|
||||
</block>
|
||||
</app>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="view_partner_form_nif_ao" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.nif.ao</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<xpath expr="//sheet/group" position="before">
|
||||
<group name="nif_ao_group" string="Identificação Fiscal Angola" col="4">
|
||||
<field name="nif_ao"
|
||||
placeholder="Ex: 5001234567"
|
||||
readonly="nif_ao_validated"/>
|
||||
|
||||
<button name="action_validate_nif"
|
||||
string="Validar NIF"
|
||||
type="object"
|
||||
icon="fa-search"
|
||||
class="btn-primary"
|
||||
invisible="not nif_ao"/>
|
||||
|
||||
<field name="nif_ao_state" readonly="1" invisible="not nif_ao_state"/>
|
||||
<field name="nif_ao_inadimplente" readonly="1" invisible="not nif_ao_inadimplente"/>
|
||||
<field name="nif_ao_regime_iva" readonly="1" invisible="not nif_ao_regime_iva"/>
|
||||
<field name="nif_ao_contributor_type" readonly="1" invisible="not nif_ao_contributor_type"/>
|
||||
|
||||
<field name="nif_ao_validated"
|
||||
widget="boolean_toggle"
|
||||
readonly="1"
|
||||
string="NIF Validado"/>
|
||||
|
||||
<field name="nif_ao_last_validation"
|
||||
readonly="1"
|
||||
invisible="not nif_ao_last_validation"/>
|
||||
</group>
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button name="action_open_nif_logs"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-history"
|
||||
invisible="nif_ao_validation_count == 0">
|
||||
<field name="nif_ao_validation_count"
|
||||
widget="statinfo"
|
||||
string="Validações NIF"/>
|
||||
</button>
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_partner_tree_nif_ao" model="ir.ui.view">
|
||||
<field name="name">res.partner.list.nif.ao</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//list//field[@name='email']" position="before">
|
||||
<field name="nif_ao" string="NIF" optional="show"/>
|
||||
<field name="nif_ao_validated" string="✓ NIF" widget="boolean" optional="show"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Loading…
Reference in New Issue