Files
odoo-at-payroll/addons/l10n_at_payroll_agent/wizard/pv_agent_review.py
T
fegger a848c07e96 [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
2026-09-17 13:50:18 +02:00

74 lines
3.0 KiB
Python

# Part of the odoo-at-payroll project. License: LGPL-3.
"""PV-Agent-Review-Wizard (D25/M4.1 Pilot): Plausibilitätsprüfung einer
geplanten einmaligen Auszahlung.
Workflow: Draft-Lohnzettel mit ATP_PRAMIE-Eingabe rechnen → Wizard öffnen →
„Prüfung starten“ → Odoo baut den minimierten Kontext und ruft den Agenten
(mode=review) → Verdict + belegte Antwort werden im Dialog angezeigt. Odoo
bleibt autoritativ für Zahlen; der Agent korrigiert nichts stillschweigend.
"""
import uuid
from odoo import _, fields, models
REVIEW_QUESTION_DEFAULT = (
"Prüfe die geplante Auszahlung gegen die Regeln der Wissensbasis."
)
class PvAgentReviewWizard(models.TransientModel):
_name = "pv.agent.review.wizard"
_description = "PV-Agent: Plausibilitätsprüfung"
_order = "id desc"
payslip_id = fields.Many2one(
"hr.payslip", string="Lohnzettel (Draft)", required=True,
domain="[('state', '=', 'draft')]",
help="Gerechneter Draft-Lohnzettel mit einer ATP_PRAMIE-Eingabe "
"(geplante Auszahlung).")
question = fields.Text(
string="Frage an den Agenten", required=True,
default=REVIEW_QUESTION_DEFAULT)
answer = fields.Text(string="Agent-Antwort (belegt)", readonly=True)
verdict = fields.Selection(
[("plausible", "Plausibel"),
("implausible", "Nicht plausibel"),
("not_checkable", "Nicht prüfbar")],
string="Verdict", readonly=True)
checks = fields.Text(string="Prüfpunkte", readonly=True)
request_id = fields.Char(string="Request-ID", readonly=True)
def action_l10n_at_pv_review(self):
self.ensure_one()
context = self.env["pv.agent.context.builder"].l10n_at_pv_build_payout_context(
self.payslip_id)
request_id = "odoo-" + uuid.uuid4().hex[:12]
body = self.env["pv.agent.client"].l10n_at_pv_ask_review(
self.question, context, request_id)
plausibility = body.get("plausibility") or {}
self.write({
"answer": body.get("answer", ""),
"verdict": plausibility.get("verdict") or "not_checkable",
"checks": self._l10n_at_pv_render_checks(plausibility),
"request_id": body.get("request_id") or request_id,
})
return {
"type": "ir.actions.act_window",
"res_model": self._name,
"res_id": self.id,
"view_mode": "form",
"target": "new",
"name": _("PV-Agent Prüfung"),
}
@staticmethod
def _l10n_at_pv_render_checks(plausibility):
lines = []
for check in plausibility.get("checks", []):
label = {"ok": "OK", "warn": "WARN ⚠", "open": "OFFEN"}.get(
check.get("status"), check.get("status", ""))
ids = ", ".join(check.get("source_ids", []))
lines.append(
f"[{label}] {check.get('aspect', '')}{check.get('detail', '')}"
+ (f" ({ids})" if ids else ""))
return "\n".join(lines)