feat(dokuwiki): Add wiki-based template system and image upload

- Implemented wiki-based template system using c_template
  - Template loaded from werkstatt:ausruestung:c_template
  - Placeholder replacement for all equipment and related fields
  - Fallback to hardcoded content if template unavailable
  - Only view pages use templates, central doku page unchanged

- Added image upload capability for equipment
  - New field: image_1920 (Image field, max 1920x1920px)
  - View integration: Positioned as avatar before button_box
  - Upload to DokuWiki media directory during sync
  - Temporary file approach for dokuwiki.py library compatibility

- Extended template placeholders:
  - status: Equipment status from status_id
  - odoo_link: Formatted link back to Odoo equipment form
  - odoo_url: Direct URL to equipment in Odoo
  - image: Equipment image (300px width)
  - image_large: Equipment image (original size)
  - image_id: Media path for custom DokuWiki syntax

- Updated documentation with all available placeholders
This commit is contained in:
Matthias Lotz 2025-12-13 23:02:49 +01:00
parent e53c4028c9
commit 3fa050f153
4 changed files with 282 additions and 3 deletions

View File

@ -0,0 +1,35 @@
Verfügbare Platzhalter:
Basis-Felder (maintenance.equipment):
{name} - Equipment-Name
{serial_no} - Seriennummer
{model} - Modell
{category} - Kategoriename
{status} - Status (aus status_id)
{location} - Standort
{ows_area} - Bereichsname
{assign_date} - Zuweisungsdatum (formatiert)
{cost} - Kosten
{warranty_date} - Garantiedatum (formatiert)
{note} - Notizen
Spezial-Felder:
{view_type} - "Bereich" oder "Einsatzzweck"
{view_name} - Name des Bereichs/Einsatzzwecks
{wiki_doku_page} - ID der zentralen Doku-Seite
{wiki_doku_link} - Fertiger Link zur zentralen Doku-Seite
{odoo_link} - Fertiger Link zur Odoo Equipment-Seite
{odoo_url} - URL zur Odoo Equipment-Seite (ohne Link-Markup)
{sync_datetime} - Aktuelles Datum/Zeit
{image} - Equipment-Bild (300px Breite) - Format: {{:media_id?300}}
{image_large} - Equipment-Bild (Originalgröße) - Format: {{:media_id}}
{image_id} - Media-ID des Bildes (z.B. werkstatt:ausruestung:media:sabako-laser.jpg)
ows.machine Felder (falls verknüpft):
{ows_machine_id.name} - Name
{ows_machine_id.model} - Modell
{ows_machine_id.serial_no} - Seriennummer
{ows_machine_id.location} - Standort
{ows_machine_id.note} - Notizen
{ows_machine_id.category} - Kategorie

View File

@ -175,3 +175,67 @@ class DokuWikiClient(models.AbstractModel):
wiki_url = wiki_url.rstrip('/')
return f"{wiki_url}/doku.php?id={page_id}"
@api.model
def upload_media(self, media_id, file_content, overwrite=True):
"""
Lädt eine Mediendatei (Bild) ins DokuWiki hoch.
Args:
media_id (str): DokuWiki Media-ID (z.B. "werkstatt:ausruestung:equipment_3.jpg")
file_content (bytes): Binärer Dateiinhalt
overwrite (bool): Existierende Datei überschreiben
Returns:
bool: True bei Erfolg
Raises:
UserError: Bei Fehler beim Upload
"""
wiki = self._get_wiki_connection()
import tempfile
import os
# Temporäre Datei erstellen (dokuwiki.py erwartet Datei-Pfad)
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as tmp_file:
tmp_file.write(file_content)
tmp_path = tmp_file.name
try:
wiki.medias.add(media_id, tmp_path, overwrite=overwrite)
_logger.info(f"Mediendatei hochgeladen: {media_id}")
return True
except DokuWikiError as e:
error_msg = f"Fehler beim Hochladen der Mediendatei {media_id}: {e}"
_logger.error(error_msg)
raise UserError(error_msg)
finally:
# Temporäre Datei löschen
if os.path.exists(tmp_path):
os.unlink(tmp_path)
@api.model
def get_media_url(self, media_id):
"""
Generiert die vollständige URL zu einer DokuWiki-Mediendatei.
Args:
media_id (str): DokuWiki Media-ID
Returns:
str: Vollständige URL zur Mediendatei
"""
IrConfigParameter = self.env['ir.config_parameter'].sudo()
wiki_url = IrConfigParameter.get_param('dokuwiki.url', '')
if not wiki_url:
return ""
# Entferne trailing slash falls vorhanden
wiki_url = wiki_url.rstrip('/')
# Ersetze : durch / für Media-Pfad
media_path = media_id.replace(':', '/')
return f"{wiki_url}/lib/exe/fetch.php?media={media_id}"

View File

@ -11,6 +11,14 @@ _logger = logging.getLogger(__name__)
class MaintenanceEquipment(models.Model):
_inherit = 'maintenance.equipment'
# Bild-Feld
image_1920 = fields.Image(
string='Bild',
max_width=1920,
max_height=1920,
help='Equipment-Bild (wird beim Wiki-Sync hochgeladen)'
)
# Wiki-Felder
wiki_doku_id = fields.Char(
string='Wiki Dokumentations-ID',
@ -143,9 +151,163 @@ class MaintenanceEquipment(models.Model):
return name or "unnamed"
def _generate_wiki_main_page_content(self, view_type='area'):
def _render_template_from_wiki(self, view_type='area'):
"""
Generiert den Wiki-Markup-Inhalt für eine Haupt-Ansichtsseite.
Lädt c_template aus DokuWiki und ersetzt Platzhalter mit Odoo-Feldwerten.
Template-Pfad: werkstatt:ausruestung:c_template
Platzhalter-Format:
- {feldname} für maintenance.equipment Felder, z.B. {name}, {serial_no}
- {ows_machine_id.feldname} für ows.machine Felder, z.B. {ows_machine_id.power}
- {wiki_doku_page} für die zentrale Doku-Seite ID
- {wiki_doku_link} für Link zur zentralen Doku-Seite
- {ows_area} für Bereichsname
- {category} für Kategoriename
- {sync_datetime} für aktuelles Datum/Zeit
Args:
view_type (str): 'area' oder 'purpose'
Returns:
str: Gerenderter Wiki-Markup-Inhalt
"""
self.ensure_one()
dokuwiki_client = self.env['dokuwiki.client']
template_page_id = 'werkstatt:ausruestung:c_template'
try:
# Template aus Wiki laden
template_content = dokuwiki_client.get_page(template_page_id)
if not template_content:
_logger.warning(f"Template {template_page_id} nicht gefunden, verwende Fallback")
return self._generate_wiki_main_page_content_fallback(view_type)
_logger.info(f"Template geladen: {template_page_id} ({len(template_content)} Zeichen)")
# Werte-Dictionary vorbereiten
values = self._prepare_template_values(view_type)
# Platzhalter ersetzen
rendered_content = template_content
for key, value in values.items():
placeholder = '{' + key + '}'
rendered_content = rendered_content.replace(placeholder, str(value or ''))
return rendered_content
except Exception as e:
_logger.warning(f"Fehler beim Laden des Templates {template_page_id}: {e}")
return self._generate_wiki_main_page_content_fallback(view_type)
def _prepare_template_values(self, view_type='area'):
"""
Bereitet Dictionary mit allen verfügbaren Feldwerten für Template-Rendering vor.
Args:
view_type (str): 'area' oder 'purpose'
Returns:
dict: Dictionary mit Feldnamen als Keys und Werten
"""
self.ensure_one()
# Zentrale Doku-Seite
doku_page_id = self._get_wiki_doku_page_id()
# Odoo Base URL
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url', 'http://localhost:8069')
odoo_equipment_url = f"{base_url}/web#id={self.id}&model=maintenance.equipment&view_type=form"
# Basis-Werte für maintenance.equipment
values = {
# Spezielle Werte
'wiki_doku_page': doku_page_id,
'wiki_doku_link': f"[[{doku_page_id}|✏️ Zentrale Dokumentation bearbeiten]]",
'odoo_link': f"[[{odoo_equipment_url}|🔗 In Odoo öffnen]]",
'odoo_url': odoo_equipment_url,
'sync_datetime': datetime.now().strftime('%d.%m.%Y %H:%M'),
# Standard Equipment-Felder
'name': self.name or '',
'serial_no': self.serial_no or '',
'model': self.model or '',
'ows_area': self.ows_area_id.name if self.ows_area_id else '',
'category': self.category_id.name if self.category_id else '',
'status': self.status_id.name if self.status_id else '',
'location': self.location or '',
'assign_date': self.assign_date.strftime('%d.%m.%Y') if self.assign_date else '',
'cost': str(self.cost) if self.cost else '',
'warranty_date': self.warranty_date.strftime('%d.%m.%Y') if self.warranty_date else '',
'color': str(self.color) if self.color else '',
'note': self.note or '',
}
# ows.machine Felder hinzufügen (falls verknüpft)
if self.ows_machine_id:
machine = self.ows_machine_id
# Nur existierende Felder hinzufügen
ows_machine_fields = {}
# Standard-Felder von ows.machine
if hasattr(machine, 'name') and machine.name:
ows_machine_fields['ows_machine_id.name'] = machine.name
if hasattr(machine, 'model') and machine.model:
ows_machine_fields['ows_machine_id.model'] = machine.model
if hasattr(machine, 'serial_no') and machine.serial_no:
ows_machine_fields['ows_machine_id.serial_no'] = machine.serial_no
if hasattr(machine, 'location') and machine.location:
ows_machine_fields['ows_machine_id.location'] = machine.location
if hasattr(machine, 'note'):
ows_machine_fields['ows_machine_id.note'] = machine.note or ''
if hasattr(machine, 'category_id') and machine.category_id:
ows_machine_fields['ows_machine_id.category'] = machine.category_id.name
values.update(ows_machine_fields)
# Bild-Upload und Referenz (falls vorhanden)
if self.image_1920:
wiki_doku_id = self._get_wiki_doku_id()
# Media-ID: werkstatt:ausruestung:media:equipment_name.jpg
media_id = f"werkstatt:ausruestung:media:{wiki_doku_id}.jpg"
# Bild ins Wiki hochladen
try:
import base64
# image_1920 ist base64-kodiert, in bytes umwandeln für XML-RPC
image_bytes = base64.b64decode(self.image_1920)
dokuwiki_client = self.env['dokuwiki.client']
dokuwiki_client.upload_media(media_id, image_bytes, overwrite=True)
# DokuWiki Image-Syntax: {{namespace:file.jpg?300}}
values['image'] = f"{{{{:{media_id}?300}}}}"
values['image_large'] = f"{{{{:{media_id}}}}}"
values['image_id'] = media_id
_logger.info(f"Bild hochgeladen: {media_id}")
except Exception as e:
_logger.error(f"Fehler beim Bild-Upload: {e}", exc_info=True)
values['image'] = ''
values['image_large'] = ''
values['image_id'] = ''
else:
values['image'] = ''
values['image_large'] = ''
values['image_id'] = ''
# View-Typ spezifische Werte
if view_type == 'area':
values['view_type'] = 'Bereich'
values['view_name'] = self.ows_area_id.name if self.ows_area_id else 'Unbekannt'
else:
values['view_type'] = 'Einsatzzweck'
values['view_name'] = 'TODO: Einsatzzweck'
return values
def _generate_wiki_main_page_content_fallback(self, view_type='area'):
"""
Fallback-Methode: Generiert hart-codierten Wiki-Markup-Inhalt,
wenn c_template.txt nicht verfügbar ist.
Args:
view_type (str): 'area' oder 'purpose'
@ -183,6 +345,19 @@ class MaintenanceEquipment(models.Model):
"""
return content
def _generate_wiki_main_page_content(self, view_type='area'):
"""
Generiert den Wiki-Markup-Inhalt für die Haupt-Ansichtsseite.
Verwendet c_template.txt aus DokuWiki falls verfügbar, sonst Fallback.
Args:
view_type (str): 'area' oder 'purpose'
Returns:
str: Wiki-Markup-Inhalt
"""
return self._render_template_from_wiki(view_type)
def _generate_wiki_doku_page_content(self):
"""
Generiert den initialen Wiki-Markup-Inhalt für die zentrale Dokumentationsseite.
@ -270,7 +445,7 @@ Hier kann die Dokumentation für {self.name} geschrieben werden.
else:
_logger.info(f"Zentrale Doku-Seite für {self.name} existiert bereits: {doku_page_id}")
# 2. Haupt-Ansichtsseite nach Bereich erstellen/aktualisieren
# 2. Haupt-Ansichtsseite nach Bereich erstellen/aktualisieren (verwendet c_template.txt)
area_page_id = self._get_wiki_page_id_by_area()
area_content = self._generate_wiki_main_page_content(view_type='area')
dokuwiki_client.create_page(

View File

@ -8,6 +8,11 @@
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/>
<field name="arch" type="xml">
<!-- Bild-Feld oben rechts neben den Smart Buttons -->
<xpath expr="//sheet/div[@name='button_box']" position="before">
<field name="image_1920" widget="image" class="oe_avatar" options="{'preview_image': 'image_1920'}"/>
</xpath>
<!-- Smart Button im Header -->
<xpath expr="//div[@name='button_box']" position="inside">
<button name="action_open_wiki"