# Part of the odoo-at-payroll project. License: LGPL-3. """Kontext-Builder für den PV-Agent-Review (D25/M4.1). Odoo ist System of Record: Der Builder projiziert den gerechneten Draft-Lohnzettel auf das schema-gebundene Review-Kontextformat der Agent-API v1 (facts + computation, alles als Strings). Datensparsamkeit per Whitelist: keine Namen, keine SV-/Personalnummern — nur die für die Abgabenprüfung relevanten Ist-Werte und das Odoo-Ergebnis. Verifizierte Felder/APIs (Bestand): - payslip.version_id / version.wage (versichertenmeldung.py:274) - version.kv_id (l10n_at_hr_payroll_private/models/hr_version_private.py) - payslip.input_line_ids / input.input_type_id.code (reisekosten.py:215) - payslip.line_ids mit l.code / l.total (lohnsteuer.py:266-268) - Rule-Codes: SVDG_*, LST_* (salary_rules_*.xml) und FLAFDB/KOMMST/DZ/BVG/ WIEN_DAG; Input-Code ATP_PRAMIE (input_types_private.xml). """ from odoo import _, models from odoo.exceptions import UserError PRAMIE_INPUT_CODE = "ATP_PRAMIE" # DG-/LST-relevante Regel-Codes (verifiziert aus den Salary-Rules-XMLs): # SVDG_* (SV-Dienstgeberanteil), LST_* (Lohnsteuer), FLAFDB (§ 41 FLAG), # KOMMST (§ 9 KommStG), DZ (§§ 122/126 WKG), BVG (§ 6 BMSVG), # WIEN_DAG (§ 1 DAG). DG_RULE_PREFIXES = ("SVDG", "LST") DG_RULE_EXACT = ("FLAFDB", "KOMMST", "DZ", "BVG", "WIEN_DAG") def _eur(value): return f"{value:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") + " EUR" class PvAgentContextBuilder(models.AbstractModel): _name = "pv.agent.context.builder" _description = "PV-Agent Kontext-Builder (Review)" def l10n_at_pv_build_payout_context(self, payslip): """Projiziert einen gerechneten Draft-Lohnzettel mit einer ATP_PRAMIE-Eingabe (geplante einmalige Auszahlung) auf den Review-Kontext. Rechnet nichts neu — Odoo bleibt autoritativ.""" payslip.ensure_one() if payslip.state != "draft": raise UserError(_( "Plausibilitätsprüfung nur für Draft-Lohnzettel möglich.")) pramie = payslip.input_line_ids.filtered( lambda line: line.input_type_id.code == PRAMIE_INPUT_CODE) if not pramie: raise UserError(_( "Keine Eingabe „Prämie/Ausgleich (ATP_PRAMIE)“ am Draft " "angetroffen. Die geplante Auszahlung zuerst als Eingabe " "erfassen und den Lohnzettel neu rechnen.")) pramie_total = sum(pramie.mapped("amount")) version = payslip.version_id facts = [ { "key": "bruttolohn_monat", "value": _eur(version.wage), }, { "key": "zahlung", "value": ( f"einmalig {_eur(pramie_total)} als Barzahlung " f"(Eingabe {PRAMIE_INPUT_CODE}, Zeitraum " f"{payslip.date_from} bis {payslip.date_to})" ), }, ] if version.kv_id: facts.append({ "key": "anwendender_kv", "value": ( f"{version.kv_id.name}" + (f" ({version.kv_id.code})" if version.kv_id.code else "") ), }) lines = payslip.line_ids.filtered(self._l10n_at_pv_is_dg_line) if not lines: raise UserError(_( "Der Draft-Lohnzettel enthält keine gerechneten " "Dienstgeber-/Lohnsteuer-Zeilen. Erst „Lohnzettel rechnen“ " "ausführen.")) components = [ { "key": line.code.lower(), "value": _eur(line.total), "note": line.name or "", } for line in lines.sorted(key=lambda l: l.code) ] dg_gesamt = sum( line.total for line in lines if line.code.startswith("SVDG") ) computation = { "label": "AG-Belastung der geplanten Auszahlung (Odoo-Draft)", "result": _eur(dg_gesamt), "basis": ( "gerechneter Draft-Lohnzettel, DG-/LST-Regecodes " "SVDG_*/LST_*/FLAFDB/KOMMST/DZ/BVG/WIEN_DAG" ), "components": components, } return { "facts": facts, "computation": computation, "note": "Simulationslauf auf Draft-Lohnzettel; keine Buchung.", } @staticmethod def _l10n_at_pv_is_dg_line(line): code = line.code or "" return ( code.startswith(DG_RULE_PREFIXES) or code in DG_RULE_EXACT )