mirror of
http://100.103.83.12:3003/fegger/odoo-at-payroll.git
synced 2026-09-17 16:56:42 +00:00
[ADD] l10n_at_payroll_agent: PV-agent review bridge for payout plausibility
PV-agent bridge (D25/M4.1): Odoo stays the system of record; the standalone, KB-bound knowledge service (pv-agent) reviews plausibility and returns evidence-backed answers (mode=review, agent API v1). No back path: the agent never calls Odoo and receives no personal data — only the minimized, schema-bound review context. - context builder: projects a computed draft payslip with an ATP_PRAMIE input (planned one-off payout) onto the review context (facts + computation): gross wage (hr.version.wage), applying collective agreement (version.kv_id) and the draft's DG/LST lines (SVDG_*/LST_*/FLAFDB/KOMMST/DZ/BVG/WIEN_DAG) - client (stdlib urllib, no extra dependency): POST /v1/ask with X-Request-ID, bearer key from company settings (pv_agent_url / pv_agent_api_key, group_hr_payroll_user); neutral UserErrors, technical details only in the server log - review wizard on the draft payslip: agent answer (evidence-backed), structured verdict (plausible / implausible / not checkable) and checks in the dialog - security: wizard bound to hr_payroll.group_hr_payroll_user; agent service key stored on res.company, never logged; 5 tests
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
from . import test_pv_agent_review
|
||||
@@ -0,0 +1,118 @@
|
||||
# Part of the odoo-at-payroll project. License: LGPL-3.
|
||||
"""Tests: PV-Agent-Client (Konfiguration, Payload, Fehlerabbildung) und
|
||||
Check-Rendering. Läuft nur in einer Odoo-Test-Session
|
||||
(`odoo-bin -i l10n_at_payroll_agent --test-tags ...`); die Builder-Tests am
|
||||
echten Lohnzettel folgen mit der Pilot-Workflow-Verifikation."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
|
||||
from odoo.addons.l10n_at_payroll_agent.models.pv_agent_client import (
|
||||
DEFAULT_PV_AGENT_URL,
|
||||
_post_json,
|
||||
)
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class PvAgentClientTest(TransactionCase):
|
||||
"""Client-Konfiguration (res.company), Payload-Vertrag und
|
||||
Fehlerabbildung (neutral, ohne interne Details)."""
|
||||
|
||||
def test_01_params_default(self):
|
||||
client = self.env["pv.agent.client"]
|
||||
self.assertEqual(client._l10n_at_pv_agent_url(), DEFAULT_PV_AGENT_URL)
|
||||
self.assertEqual(client._l10n_at_pv_agent_api_key(), "")
|
||||
|
||||
def test_02_params_company_override(self):
|
||||
self.env.company.pv_agent_url = "http://127.0.0.1:8080"
|
||||
self.env.company.pv_agent_api_key = "secret-key"
|
||||
client = self.env["pv.agent.client"]
|
||||
self.assertEqual(client._l10n_at_pv_agent_url(), "http://127.0.0.1:8080")
|
||||
self.assertEqual(client._l10n_at_pv_agent_api_key(), "secret-key")
|
||||
|
||||
def test_03_ask_review_payload(self):
|
||||
"""mode=review, schema-gebundener Kontext und Request-ID werden
|
||||
als /v1/ask-Payload gesendet; die Antwort wird unverändert
|
||||
zurückgegeben."""
|
||||
captured = {}
|
||||
|
||||
def fake_post(url, payload, api_key, request_id, timeout_s):
|
||||
captured.update({
|
||||
"url": url,
|
||||
"payload": payload,
|
||||
"api_key": api_key,
|
||||
"request_id": request_id,
|
||||
})
|
||||
return 200, {
|
||||
"answer": "Antwort [lb-min-01].",
|
||||
"verified": True,
|
||||
"request_id": request_id,
|
||||
"plausibility": {"verdict": "plausible", "checks": []},
|
||||
}
|
||||
|
||||
self.env.company.pv_agent_api_key = "secret-key"
|
||||
with patch(
|
||||
"odoo.addons.l10n_at_payroll_agent.models.pv_agent_client._post_json",
|
||||
side_effect=fake_post,
|
||||
):
|
||||
body = self.env["pv.agent.client"].l10n_at_pv_ask_review(
|
||||
"Prüfe die Auszahlung.",
|
||||
{"facts": [{"key": "zahlung", "value": "500 EUR"}],
|
||||
"computation": None,
|
||||
"note": None},
|
||||
"odoo-abc123",
|
||||
)
|
||||
self.assertEqual(captured["url"], "http://127.0.0.1:8080/v1/ask")
|
||||
self.assertEqual(captured["payload"]["mode"], "review")
|
||||
self.assertEqual(captured["payload"]["question"], "Prüfe die Auszahlung.")
|
||||
self.assertEqual(captured["api_key"], "secret-key")
|
||||
self.assertEqual(captured["request_id"], "odoo-abc123")
|
||||
self.assertEqual(body["plausibility"]["verdict"], "plausible")
|
||||
|
||||
def test_04_http_401_maps_to_user_error(self):
|
||||
from odoo.addons.l10n_at_payroll_agent.models.pv_agent_client import (
|
||||
PvAgentHttpError,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"odoo.addons.l10n_at_payroll_agent.models.pv_agent_client._post_json",
|
||||
side_effect=PvAgentHttpError(401, '{"detail": "Authentisierung"}'),
|
||||
):
|
||||
with self.assertRaises(UserError):
|
||||
self.env["pv.agent.client"].l10n_at_pv_ask_review(
|
||||
"Frage", {"facts": []}, "odoo-abc123")
|
||||
|
||||
def test_05_unreachable_maps_to_user_error(self):
|
||||
from odoo.addons.l10n_at_payroll_agent.models.pv_agent_client import (
|
||||
PvAgentHttpError,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"odoo.addons.l10n_at_payroll_agent.models.pv_agent_client._post_json",
|
||||
side_effect=PvAgentHttpError(503, ""),
|
||||
):
|
||||
with self.assertRaises(UserError):
|
||||
self.env["pv.agent.client"].l10n_at_pv_ask_review(
|
||||
"Frage", {"facts": []}, "odoo-abc123")
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class PvAgentChecksRenderingTest(TransactionCase):
|
||||
"""Verdict-Rendering für den Dialog (deterministisch, ohne HTTP)."""
|
||||
|
||||
def test_06_render_checks(self):
|
||||
wizard = self.env["pv.agent.review.wizard"]
|
||||
checks_text = wizard._l10n_at_pv_render_checks({
|
||||
"checks": [
|
||||
{"status": "warn", "aspect": "Lohnsteuer",
|
||||
"detail": "erwartet 30 EUR — erhalten 0 EUR",
|
||||
"source_ids": ["lb-lvr-07"]},
|
||||
{"status": "open", "aspect": "DZ",
|
||||
"detail": "Gemeinde fehlt", "source_ids": []},
|
||||
],
|
||||
})
|
||||
self.assertIn("[WARN ⚠] Lohnsteuer", checks_text)
|
||||
self.assertIn("(lb-lvr-07)", checks_text)
|
||||
self.assertIn("[OFFEN] DZ", checks_text)
|
||||
self.assertNotIn("()", checks_text)
|
||||
Reference in New Issue
Block a user