Files
odoo-at-payroll/addons/l10n_at_payroll_dokumente/models/dokument_wizard.py
T
fegger 1df5fb19c4 [ADD] l10n_at_payroll_dokumente: generate documents from templates
Document generation module: templates with own text (structure reference
and source stand documented per template, no licensed full text in the
repo), fill fields resolved from hr data (company, employee, hr.version
stand) plus wizard input, preview rendering and PDF/HTML attachment via
the generic report layout.

- templates: pilot set for the general-AT track formulated as own text
  after the KB muster structure (lb-bes-09, lb-bnd-55, lb-bnd-52/62,
  lb-end-35), legal status per template marked as draft
- rendering via convert_inline_template_to_qweb + ir.qweb (t-out
  auto-escapes values) — deliberately not mail.render.mixin inline
  engine, whose expression whitelist blocks werte[...] placeholders for
  non-template-editors
- wizard resolves hr.version by date_version at the reference date;
  PDF falls back to HTML attachment without wkhtmltopdf
- security: ir.model.access (hr user read / manager write) + company
  rule; 8 tests, suite 236/236 green on a fresh -i database run
2026-09-16 10:36:49 +02:00

188 lines
7.0 KiB
Python

# Part of the odoo-at-payroll project. License: LGPL-3.
"""Wizard: Dokument aus Vorlage generieren.
Befüllt die Platzhalter der Vorlage aus hr-Stammdaten (automatische
Felder) und Wizard-Eingaben (manuelle Felder), rendert eine Vorschau und
erzeugt PDF (ir.actions.report, wkhtmltopdf) bzw. — ohne PDF-Engine —
das gerenderte HTML als Anhang.
"""
import base64
from markupsafe import Markup
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools.misc import format_date
class L10nAtPayrollDokumentWizard(models.TransientModel):
_name = 'l10n.at.payroll.dokument.wizard'
_description = 'Dokument aus Vorlage generieren'
vorlage_id = fields.Many2one(
'l10n.at.payroll.dokument.vorlage', required=True,
domain=[('active', '=', True)])
employee_id = fields.Many2one('hr.employee', required=True)
datum = fields.Date(
string='Stichtag',
default=fields.Date.context_today,
help="Stichtag für den hr.version-Stand (Vertragsdaten).")
version_id = fields.Many2one(
'hr.version', compute='_compute_version_id',
string='Vertragsstand (hr.version)')
feld_wert_ids = fields.One2many(
'l10n.at.payroll.dokument.wizard.feld', 'wizard_id',
string='Befüllung')
vorschau_html = fields.Html(
string='Vorschau', readonly=True, sanitize=False)
@api.depends('employee_id', 'datum')
def _compute_version_id(self):
for wizard in self:
wizard.version_id = wizard._at_version_am()
def _at_version_am(self):
"""hr.version gültig am Stichtag (date_version <= Stichtag)."""
self.ensure_one()
if not self.datum or not self.employee_id:
return self.env['hr.version']
return self.env['hr.version'].search(
[('employee_id', '=', self.employee_id.id),
('date_version', '<=', self.datum)],
order='date_version desc, id desc', limit=1)
@api.onchange('vorlage_id')
def _onchange_vorlage_id(self):
self.vorschau_html = False
self.feld_wert_ids = self._at_linien()
def action_befuellen(self):
"""Linien aus der Vorlage neu aufbauen (auch bei Wechsel)."""
self.ensure_one()
self.vorschau_html = False
self.feld_wert_ids = self._at_linien()
def _at_linien(self):
self.ensure_one()
if not self.vorlage_id:
return [(5, 0, 0)]
return [(5, 0, 0)] + [
(0, 0, {
'key': feld.key,
'bezeichnung': feld.bezeichnung,
'quelle': feld.quelle,
'pflicht': feld.pflicht,
'hinweis': feld.hinweis,
})
for feld in self.vorlage_id.feld_ids.sorted('sequence')
]
def _at_werte(self):
"""Befüllwerte: automatisch aufgelöst + manuelle Eingaben."""
self.ensure_one()
werte = {}
fehlen = []
for feld in self.vorlage_id.feld_ids:
line = self.feld_wert_ids.filtered(
lambda l, k=feld.key: l.key == k)[:1]
if feld.quelle == 'auto':
werte[feld.key] = self._at_auto_wert(feld.key)
else:
wert = (line.wert or '').strip()
if not wert and feld.pflicht:
fehlen.append(feld.bezeichnung or feld.key)
werte[feld.key] = wert
if fehlen:
raise UserError(_(
"Pflichtfelder nicht befüllt: %s", ', '.join(fehlen)))
return werte
def _at_auto_wert(self, key):
self.ensure_one()
employee = self.employee_id
version = self.version_id
company = employee.company_id
if key == 'firma':
return company.name or ''
if key == 'firma_strasse':
return company.partner_id.street or ''
if key == 'firma_plz':
return company.partner_id.zip or ''
if key == 'firma_ort':
return company.partner_id.city or ''
if key == 'mitarbeiter':
return employee.name or ''
if key == 'mitarbeiter_strasse':
return version.private_street or ''
if key == 'mitarbeiter_plz':
return version.private_zip or ''
if key == 'mitarbeiter_ort':
return version.private_city or ''
if key == 'ead':
datum = version.date_version or version.contract_date_start
return format_date(self.env, datum) if datum else ''
if key == 'funktion':
return version.job_title or version.job_id.name or ''
if key == 'geboren':
return format_date(self.env, employee.birthday) if employee.birthday else ''
raise UserError(_(
"Kein automatischer Wert für Platzhalter '%s' definiert.", key))
def action_vorschau(self):
self.ensure_one()
werte = self._at_werte()
self.vorschau_html = Markup(self.vorlage_id._at_renderen(
self.employee_id, self.version_id, werte, self.datum))
return {
'type': 'ir.actions.act_window',
'res_model': self._name,
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
}
def action_pdf(self):
self.ensure_one()
werte = self._at_werte()
body = Markup(self.vorlage_id._at_renderen(
self.employee_id, self.version_id, werte, self.datum))
name = '%s%s (%s)' % (
self.vorlage_id.name, self.employee_id.name, self.datum)
if self.env['ir.actions.report'].get_wkhtmltopdf_state() == 'ok':
pdf_content, _typ = self.env['ir.actions.report']._render_qweb_pdf(
'l10n_at_payroll_dokumente.action_report_dokument_generisch',
self.ids, data={'titel': name, 'body': body})
datas = base64.b64encode(pdf_content)
mimetype = 'application/pdf'
suffix = '.pdf'
else:
datas = base64.b64encode(str(body).encode('utf-8'))
mimetype = 'text/html'
suffix = '.html'
self.env['ir.attachment'].create({
'name': name + suffix,
'datas': datas,
'mimetype': mimetype,
'res_model': 'hr.employee',
'res_id': self.employee_id.id,
'description': 'Dokument aus Vorlage %s' % self.vorlage_id.name,
})
return {
'type': 'ir.actions.act_window_close',
}
class L10nAtPayrollDokumentWizardFeld(models.TransientModel):
_name = 'l10n.at.payroll.dokument.wizard.feld'
_description = 'Dokument-Wizard Befüllzeile'
wizard_id = fields.Many2one(
'l10n.at.payroll.dokument.wizard', required=True,
ondelete='cascade')
key = fields.Char(readonly=True, required=True)
bezeichnung = fields.Char(readonly=True)
quelle = fields.Selection(
[('auto', 'Automatisch (hr-Stammdaten)'),
('manuell', 'Manuelle Eingabe im Wizard')], readonly=True)
pflicht = fields.Boolean(readonly=True)
hinweis = fields.Char(readonly=True)
wert = fields.Char()