Files
DigitalSignage/models/dss_settings.py
T
2026-07-23 08:54:44 +02:00

1044 lines
50 KiB
Python
Executable File

import ast
import datetime
import json
import re
import uuid
import logging
import base64
import subprocess
import tempfile
import easywebdav
import os
import os.path
import requests
from odoo import api, fields, models, _
from odoo import tools
from odoo.exceptions import ValidationError
from datetime import date
from datetime import datetime
from dateutil.relativedelta import relativedelta
from pyffmpeg import FFmpeg
from tuya_iot import TuyaOpenAPI, TUYA_LOGGER
from tuya_connector import TuyaOpenAPI, TUYA_LOGGER
import sys
TUYA_LOGGER.setLevel(logging.DEBUG)
_logger = logging.getLogger(__name__)
class dssSettings(models.Model):
_name = "dss.settings"
_description = "DigitalSignage Einstellungen"
_inherit = ['mail.thread','mail.activity.mixin']
uuid = fields.Char(default=lambda self: self._default_uuid(), required=True, readonly=True, copy=False, string='UUID')
def_cloud_url_base = fields.Char('Cloud URL Basis',help='https://xyxxxxxx.de/cccccccc/ bis zum vorkommen des des Grundsystem URL Teils',tracking=True)
def_project_cloudpath = fields.Char('Standard Projekt Pfad',tracking=True)
def_project_cloudpath_sample = fields.Char('Projekt Beispiel :',store=True,tracking=True)
def_contract_cloudpath = fields.Char('Standard Kunden Pfad',tracking=True)
def_contract_cloudpath_sample = fields.Char('Vertrags Beispiel :',tracking=True)
def_contract_cloudpath_V2 = fields.Char('Standard Kunden Pfad V2',tracking=True)
def_contract_cloudpath_sample_V2 = fields.Char('Projekt Beispiel V2 :',tracking=True)
def_ad_cloudpath = fields.Char('Standard Kampagnen Pfad',tracking=True)
def_ad_cloudpath_sample = fields.Char('Kampagnen Beispiel :',tracking=True)
marker_1_color_used = fields.Char('Marker Farbe (benutzt)',default='#00FF00',tracking=True)
marker_1_color_unused = fields.Char('Marker Farbe (unbenutzt)',default='#FF0000',tracking=True)
marker_2_color_used = fields.Char('Marker 2 Farbe (benutzt)',default='#00FF00',tracking=True)
marker_2_color_unused = fields.Char('Marker 2 Farbe (unbenutzt)',default='#FF0000',tracking=True)
marker_3_color_used = fields.Char('Marker 3 Farbe (benutzt)',default='#00FF00',tracking=True)
marker_3_color_unused = fields.Char('Marker 3 Farbe (unbenutzt)',default='#FF0000',tracking=True)
marker_3_color_value = fields.Boolean('Marker 3 Farbe aus wert ?',tracking=True)
marker_3_1_color_used = fields.Char('Marker 3.1 Farbe (benutzt)',default='#00FF00',tracking=True)
marker_3_1_color_unused = fields.Char('Marker 3.1 Farbe (unbenutzt)',default='#FF0000',tracking=True)
marker_3_1_color_value = fields.Boolean('Marker 3.1 Farbe aus wert ?',tracking=True)
marker_3_2_color_used = fields.Char('Marker 3.2 Farbe (benutzt)',default='#00FF00',tracking=True)
marker_3_2_color_unused = fields.Char('Marker 3.2 Farbe (unbenutzt)',default='#FF0000',tracking=True)
marker_3_2_color_value = fields.Boolean('Marker 3.2 Farbe aus wert ?',tracking=True)
marker_4_color_used = fields.Char('Marker 4 Farbe (benutzt)',default='#00FF00',tracking=True)
marker_4_color_unused = fields.Char ('Marker 4 Farbe (unbenutzt)',default='#FF0000',tracking=True)
marker_4_color_value = fields.Boolean('Marker 4 Farbe aus wert ?',tracking=True)
marker_4_1_color_used = fields.Char('Marker 4.1 Farbe (benutzt)',default='#00FF00',tracking=True)
marker_4_1_color_unused = fields.Char('Marker 4.1 Farbe (unbenutzt)',default='#FF0000',tracking=True)
marker_4_1_color_value = fields.Boolean('Marker 4.1 Farbe aus wert ?',tracking=True)
marker_4_2_color_used = fields.Char('Marker 4.2 Farbe (benutzt)',default='#00FF00',tracking=True)
marker_4_2_color_unused = fields.Char('Marker 4.2 Farbe (unbenutzt)',default='#FF0000',tracking=True)
marker_4_2_color_value = fields.Boolean('Marker 4.2 Farbe aus wert ?',tracking=True)
tuya_access_id = fields.Char('Tuya Access ID',tracking=True)
tuya_access_key = fields.Char('Tuya Access KEY',tracking=True)
tuya_endpoint = fields.Char('Tuya Access Endpoint',tracking=True)
google_maps_key = fields.Char('Google API Key (Maps)',tracking=True)
os_webimg_path = fields.Char('Verzeichnis für Bilder im Odooweb',tracking=True)
contact_runtime_user = fields.Many2one('res.users',string="Kalenderkontact",help="Kalenderkontakt für Ablauf von Verträgen")
runtime_auto_calandar_entry = fields.Boolean('Autom. Vertragsende Kalendereintrag ?',help='Jeden berechneten Vertrag-Ablaufzeitpunkt automatisch in Kalender eintragen ?',tracking=True)
show_expand_contract_group_ad_state = fields.Boolean('Jeden Kampagnen-Status anzeigen ?',help='im Groupierungsmodus der Verträge nach Kampagnenstatus alle (auch unbenutzte) Statusspalten anzeigen ?',tracking=True)
show_expand_contract_group_ad_work_state = fields.Boolean('Jeden Kampagnen-Arbeitsstatus anzeigen ?',help='im Groupierungsmodus der Verträge nach Kampagnen-Arbeitsstatus alle (auch unbenutzte) Statusspalten anzeigen ?',tracking=True)
show_expand_contract_group_ad_todo_state = fields.Boolean('Jeden Kampagnen-Aufgabenschritt anzeigen ?',help='im Groupierungsmodus der Verträge nach Kampagnen-Aufgabenschritt alle (auch unbenutzte) Aufgabenspalten anzeigen ?',tracking=True)
adportal_ranking_show_foreign_names = fields.Boolean('Ranking Fremdnamen anzeigen', default=True, help='Wenn deaktiviert, werden im AD Portal Ranking fremde Namen als ***** angezeigt. Der eigene Name bleibt sichtbar.', tracking=True)
adportal_graphics_base_prompt = fields.Text('AD Portal Grafik Prompt', compute='_compute_adportal_graphics_config', inverse='_inverse_adportal_graphics_config', tracking=True)
adportal_graphics_use_chatgpt = fields.Boolean('AD Portal: Benutze ChatGPT', compute='_compute_adportal_graphics_config', inverse='_inverse_adportal_graphics_config', tracking=True)
adportal_graphics_chatgpt_model = fields.Selection(
[
('gpt-image-1', 'gpt-image-1'),
('dall-e-3', 'dall-e-3'),
('dall-e-2', 'dall-e-2'),
],
string='AD Portal ChatGPT Modell',
compute='_compute_adportal_graphics_config',
inverse='_inverse_adportal_graphics_config',
tracking=True,
)
adportal_graphics_api_url = fields.Char('AD Portal Grafik API URL', compute='_compute_adportal_graphics_config', inverse='_inverse_adportal_graphics_config', tracking=True)
adportal_graphics_api_key = fields.Char('AD Portal Grafik API Key', compute='_compute_adportal_graphics_config', inverse='_inverse_adportal_graphics_config', tracking=True)
adportal_graphics_api_timeout = fields.Integer('AD Portal Grafik API Timeout (Sekunden)', compute='_compute_adportal_graphics_config', inverse='_inverse_adportal_graphics_config', tracking=True)
adportal_google_company_check = fields.Boolean('AD Portal Firmenpruefung via Google', compute='_compute_adportal_graphics_config', inverse='_inverse_adportal_graphics_config', tracking=True)
adportal_google_api_key = fields.Char('AD Portal Google API Key', compute='_compute_adportal_graphics_config', inverse='_inverse_adportal_graphics_config', tracking=True)
adportal_google_cse_id = fields.Char('AD Portal Google CSE ID', compute='_compute_adportal_graphics_config', inverse='_inverse_adportal_graphics_config', tracking=True)
freigabe_auto_time = fields.Integer('Auto Freigabetage',help='Automatische Freigabe nach x Tagen',tracking=True)
tv_vertragsankuendigung_actionID = fields.Integer('TV Vertragsankündigung ActionID',help="Aktion die ausgeführt wird, wenn im Web-Vertragseingang der Button Vertragsankündigung geklickt wird.",tracking=True)
systemonline_color = fields.Char('Farbe Online',tracking=True)
systemoffline_color = fields.Char('Farbe Offline',tracking=True)
vnnoxurl = fields.Char('VNNox URL',tracking=True)
vnnoxusername = fields.Char('VNNox Username',tracking=True)
vnnoxpassword = fields.Char('VNNox Password',tracking=True)
rporturl = fields.Char('RPort URL',tracking=True)
rportusername = fields.Char('RPort Username',tracking=True)
rporttoken = fields.Char('RPort API Token',tracking=True)
ai_agent_enabled = fields.Boolean('AI Agent aktiv', default=True, tracking=True)
ai_agent_base_url = fields.Char('AI Base URL', default='https://api.openai.com/v1', tracking=True)
ai_agent_model = fields.Char('AI Modell', default='gpt-4o-mini', tracking=True)
ai_agent_api_key = fields.Char('AI API Key', tracking=True)
ai_agent_system_prompt = fields.Text(
'AI System Prompt',
default='Du bist ein hilfreicher virtueller Mitarbeiter im Odoo Backend.',
tracking=True,
)
ai_agent_callback_prompt = fields.Text(
'AI Callback Prompt',
default=(
'Verwende Callback-Funktionen fuer Datenzugriffe. '
'Nutze get_model_records fuer gezielte Modell/Feld-Abfragen, '
'get_all_contracts fuer Vertragslisten, get_contract_by_number fuer Vertragsdetails '
'und get_projects fuer Projektlisten. Nutze create_model_record nur fuer explizite '
'Anlagevorgaenge und nur mit freigegebenen Schreibfeldern. Formuliere Antworten kompakt '
'und nachvollziehbar.'
),
tracking=True,
)
ai_agent_callback_models = fields.Char(
'AI Callback Modelle',
default='dss.contracts,dss.projects',
help='Kommagetrennte Modellnamen, die per get_model_records abgefragt werden duerfen.',
tracking=True,
)
ai_agent_callback_fields_json = fields.Text(
'AI Callback Felder (JSON)',
default='{"dss.contracts": ["contract_auto_id", "contract_name", "client_short_company", "contract_state", "project", "vertragssumme"], "dss.projects": ["projectid", "projektname", "name", "standort_strasse", "standort_plz", "standort_ort", "maps_visitors", "aktstatus"]}',
help='JSON-Objekt mit erlaubten Feldern pro Modell fuer get_model_records.',
tracking=True,
)
ai_agent_callback_aliases_json = fields.Text(
'AI Callback Feld-Aliase (JSON)',
default='{"dss.contracts": {"kundennummer": "contract_auto_id", "vertragsnummer": "contract_auto_id", "vertragsname": "contract_name", "vertragskennung": "contract_auto_name", "kunde": "client_short_company", "firma": "client_short_company", "vorname": "client_short_vorname", "nachname": "client_short_name", "autoverlaengerung": "contract_auto_extend", "auto_verlaengerung": "contract_auto_extend", "verlaengerung_auto": "contract_auto_extend"}}',
help='JSON-Objekt mit Aliasen pro Modell, z.B. {"dss.contracts": {"kundennummer": "contract_auto_id"}}.',
tracking=True,
)
ai_agent_callback_write_fields = fields.Text(
'AI Callback Schreibfelder',
default='',
help='Exakte Erlaubnisliste fuer Schreibzugriffe im Muster model.field, z.B. dss.projects.name oder dss.contracts.remark. Mehrere Eintraege per Komma oder Zeilenumbruch trennen.',
tracking=True,
)
ai_agent_timeout_seconds = fields.Integer('AI Timeout (Sekunden)', default=60, tracking=True)
ai_agent_temperature = fields.Float('AI Temperatur', default=0.3, tracking=True)
ai_agent_max_context_messages = fields.Integer('AI Kontext Nachrichten', default=20, tracking=True)
ai_agent_rag_enabled = fields.Boolean('AI RAG aktiv (LangChain)', default=True, tracking=True)
ai_agent_rag_top_k = fields.Integer('AI RAG Treffer (Top-K)', default=8, tracking=True)
ai_agent_contracts_access = fields.Boolean('AI Zugriff auf Vertraege', default=True, tracking=True)
ai_agent_contracts_limit = fields.Integer('AI Vertragskontext Anzahl (0 = alle)', default=0, tracking=True)
ai_agent_projects_access = fields.Boolean('AI Zugriff auf Projekte', default=True, tracking=True)
ai_agent_projects_limit = fields.Integer('AI Projektkontext Anzahl (0 = alle)', default=0, tracking=True)
phonecenter_company_provider = fields.Selection([
('google_places', 'Google Places'),
('custom_api', 'Custom Firmen-API'),
('mock', 'Mock/Fallback'),
], string='Telefoniecenter Firmenprovider', default='google_places', tracking=True)
phonecenter_company_api_url = fields.Char('Telefoniecenter Firmen-API URL', tracking=True)
phonecenter_company_api_key = fields.Char('Telefoniecenter Firmen-API Key', tracking=True)
phonecenter_company_timeout = fields.Integer('Telefoniecenter Firmen-Timeout (Sekunden)', default=20, tracking=True)
phonecenter_ai_provider = fields.Selection([
('none', 'Keine KI'),
('openai', 'OpenAI kompatibel'),
('custom_api', 'Custom KI-API'),
], string='Telefoniecenter KI-Provider', default='openai', tracking=True)
phonecenter_ai_api_url = fields.Char('Telefoniecenter KI-API URL', tracking=True)
phonecenter_ai_api_key = fields.Char('Telefoniecenter KI-API Key', tracking=True)
phonecenter_ai_model = fields.Char('Telefoniecenter KI Modell', default='gpt-4o-mini', tracking=True)
phonecenter_search_min_entries = fields.Integer('Telefoniecenter Min. Suchergebnisse', default=8, tracking=True)
phonecenter_search_max_results = fields.Integer('Telefoniecenter Max. Ergebnisse', default=30, tracking=True)
phonecenter_search_cache_minutes = fields.Integer('Telefoniecenter Cache (Minuten)', default=30, tracking=True)
showdebug = fields.Boolean('Debug',help='Debug Informationen aktivieren ',tracking=True)
m2mcron = fields.Many2one('ir.cron',string='M2M Cron',tracking=True)
variables = fields.Many2many('dss.settings.vars',string='Einstellungs Variablen',tracking=True)
def _as_bool(self, value):
return str(value or '').strip().lower() in ('1', 'true', 'yes', 'y', 'on')
def _normalize_adportal_graphics_chatgpt_model(self, value):
allowed_models = {'gpt-image-1', 'dall-e-3', 'dall-e-2'}
normalized_value = (value or '').strip()
return normalized_value if normalized_value in allowed_models else 'gpt-image-1'
def _compute_adportal_graphics_config(self):
config = self.env['ir.config_parameter'].sudo()
for record in self:
record.adportal_graphics_base_prompt = config.get_param(
'digitalsignage.adportal_graphics_base_prompt',
default='Erstelle eine hochwertige Werbegrafik im Corporate-Stil fuer die Firma.',
)
record.adportal_graphics_use_chatgpt = self._as_bool(config.get_param('digitalsignage.adportal_graphics_use_chatgpt', default='False'))
record.adportal_graphics_chatgpt_model = self._normalize_adportal_graphics_chatgpt_model(
config.get_param('digitalsignage.adportal_graphics_chatgpt_model', default='gpt-image-1')
)
record.adportal_graphics_api_url = config.get_param('digitalsignage.adportal_graphics_api_url', default='')
record.adportal_graphics_api_key = config.get_param('digitalsignage.adportal_graphics_api_key', default='')
record.adportal_graphics_api_timeout = int(config.get_param('digitalsignage.adportal_graphics_api_timeout', default='90') or 90)
record.adportal_google_company_check = self._as_bool(config.get_param('digitalsignage.adportal_google_company_check', default='False'))
record.adportal_google_api_key = config.get_param('digitalsignage.adportal_google_api_key', default='')
record.adportal_google_cse_id = config.get_param('digitalsignage.adportal_google_cse_id', default='')
def _inverse_adportal_graphics_config(self):
config = self.env['ir.config_parameter'].sudo()
for record in self:
config.set_param('digitalsignage.adportal_graphics_base_prompt', record.adportal_graphics_base_prompt or '')
config.set_param('digitalsignage.adportal_graphics_use_chatgpt', 'True' if record.adportal_graphics_use_chatgpt else 'False')
config.set_param(
'digitalsignage.adportal_graphics_chatgpt_model',
self._normalize_adportal_graphics_chatgpt_model(record.adportal_graphics_chatgpt_model),
)
config.set_param('digitalsignage.adportal_graphics_api_url', record.adportal_graphics_api_url or '')
config.set_param('digitalsignage.adportal_graphics_api_key', record.adportal_graphics_api_key or '')
config.set_param('digitalsignage.adportal_graphics_api_timeout', int(record.adportal_graphics_api_timeout or 90))
config.set_param('digitalsignage.adportal_google_company_check', 'True' if record.adportal_google_company_check else 'False')
config.set_param('digitalsignage.adportal_google_api_key', record.adportal_google_api_key or '')
config.set_param('digitalsignage.adportal_google_cse_id', record.adportal_google_cse_id or '')
def _get_settingvalue(self,valuename):
settings = (self.env['dss.settings'].search([],limit=1))
wert = settings._origin.read([valuename])[0][valuename]
return wert
@api.model
def _get_path_converted(self,path,record):
alls = ''
_logger.info("GetPath with " + str(path)+ " and Record : "+str(record.id))
if path:
for parts in path.split(','):
if parts[0] == '$':
fels = parts[1:]
if fels == 'shortwerbe_feld_selected':
part= ''
for wf in record.werbe_feld_selected:
if wf.Field_notinCloud: # AChtung NOtinCloud = True ist Name anzeigen
part += wf.feldname + '_'
part = part if parts else ''
else:
part = record._origin.read([fels])[0][fels]
_logger.info("Get Path - Anfrage-Teil gefunden : " + str(part))
if parts:
value = str(part)
else:
value = ''
_logger.info("Get Path - Anfrage-Teil gefunden - setze : " + str(value))
alls = alls + value
else:
alls = alls + str(parts)
return alls
@api.onchange('def_project_cloudpath')
def _onchange_def_project_cloudpath(self):
_logger.info("Default Project Cloudpath Setzen")
project_1 = self.env['dss.projects'].search([('uuid','=ilike','%')],limit=1)
if not project_1:
_logger.info("Default Cloudpath Setzen - kein MusterProjekt gefunden")
else:
_logger.info("Default Cloudpath Setzen - MusterProjekt gefunden "+str(project_1.id))
alls = self._get_path_converted(self.def_project_cloudpath,project_1)
self.def_project_cloudpath_sample = alls
@api.onchange('def_contract_cloudpath')
def _onchange_def_contract_cloudpath(self):
_logger.info("Default Contract Cloudpath Setzen")
contract_1 = self.env['dss.contracts'].search([('uuid','=ilike','%')],limit=1)
if not contract_1:
_logger.info("Default Cloudpath Setzen - kein MusterProjekt gefunden")
else:
_logger.info("Default Cloudpath Setzen - MusterProjekt gefunden "+str(contract_1.id))
alls = self._get_path_converted(self.def_contract_cloudpath,contract_1)
self.def_contract_cloudpath_sample = alls
@api.onchange('def_ad_cloudpath')
def _onchange_def_ad_cloudpath(self):
_logger.info("Default ad Cloudpath Setzen")
ad_1 = self.env['dss.ads'].search([('uuid','=ilike','%')],limit=1)
if not ad_1:
_logger.info("Default Cloudpath Setzen - kein MusterProjekt gefunden")
else:
_logger.info("Default Cloudpath Setzen - MusterProjekt gefunden "+str(ad_1.id))
alls = self._get_path_converted(self.def_ad_cloudpath,ad_1)
self.def_ad_cloudpath_sample = alls
def _default_uuid(self):
return str(uuid.uuid4())
@api.model
def saveConfig(self,tableInfo):
_logger.info("Config speichern "+str(tableInfo))
activefields = tableInfo[3]
for field in activefields:
_logger.info("Config speichern : "+str(field)+" / "+str(tableInfo[0])+" -> "+str(tableInfo[1])+" "+str(tableInfo[2]))
myview = self.env['dss.viewsettings'].search(['&',("user","=",self.env.user.name),("model_view","=",tableInfo[1]),("model_datensatz","=",tableInfo[0]),("viewtype","=",tableInfo[2]),("column","=",field[0])])
if not myview:
_logger.info("Config speichern : noch keine gespeicherten Daten")
else:
myview.unlink()
self.env['dss.viewsettings'].create({
'model_view':tableInfo[1],
'model_datensatz':tableInfo[0],
'viewtype':tableInfo[2],
'user':self.env.user.name,
'user_id':self.env.user.id,
'column':field[0],
'column_size':field[1],
'state_visible':field[2],
'state_collapse':field[3]
})
return 0
@api.model
def resetConfig(self,tableInfo):
_logger.info("Config reset "+str(tableInfo))
myview = self.env['dss.viewsettings'].search(['&',("user","=",self.env.user.name),("model_view","=",tableInfo[1]),("model_datensatz","=",tableInfo[0]),("viewtype","=",tableInfo[2])])
if not myview:
_logger.info("Config Reset : noch keine gespeicherten Daten")
else:
myview.unlink()
return 0
@api.model
def getprojectpath(self,project):
_logger.info("Get Project Path for "+str(project.id))
if project:
settings = (self.env['dss.settings'].search([],limit=1))
alls = dssSettings._get_path_converted(self,settings.def_project_cloudpath,project)
_logger.info("Get Project Path - Ergebnis : " + str(alls))
return alls
else:
_logger.info("Get Project Path - kein Projekt gefunden")
return ''
@api.model
def getClientpath(self,client):
_logger.info("Get Client Path for "+str(client.id))
if client:
settings = (self.env['dss.settings'].search([],limit=1))
alls = dssSettings._get_path_converted(self,settings.def_contract_cloudpath,client)
_logger.info("Get Client Path - Ergebnis : " + str(alls))
return alls
else:
_logger.info("Get Client Path - kein Client gefunden")
return ''
@api.model
def readFields(self,tablename):
_logger.info("Project read Fields : "+str(tablename))
model = self.env['ir.model'].search([('model', '=', tablename)])
fields = self.env['ir.model.fields'].search([('model_id', '=', model.id)],order = 'field_description')
_logger.info("Project read Fields - Suche nach Feldern abgeschlossen - "+str(model)+" - "+str(fields))
records = []
for field in fields:
fieldrecord=[]
fieldrecord.append(field.id)
fieldrecord.append(field.field_description)
fieldrecord.append(field.name)
fieldrecord.append(str(field.field_description+' ('+field.name+')'))
records.append(fieldrecord)
return records
@api.model
def _phonecenter_get_settings(self):
settings = self.search([], limit=1)
if not settings:
return {}
ai_api_url = settings.phonecenter_ai_api_url or settings.ai_agent_base_url or 'https://api.openai.com/v1'
ai_api_key = settings.phonecenter_ai_api_key or settings.ai_agent_api_key or ''
ai_model = settings.phonecenter_ai_model or settings.ai_agent_model or 'gpt-4o-mini'
return {
'google_maps_key': settings.google_maps_key or '',
'company_provider': settings.phonecenter_company_provider or 'google_places',
'company_api_url': settings.phonecenter_company_api_url or '',
'company_api_key': settings.phonecenter_company_api_key or '',
'company_timeout': max(5, int(settings.phonecenter_company_timeout or 20)),
'ai_provider': settings.phonecenter_ai_provider or 'openai',
'ai_api_url': ai_api_url,
'ai_api_key': ai_api_key,
'ai_model': ai_model,
'min_entries': max(1, int(settings.phonecenter_search_min_entries or 8)),
'max_results': max(5, int(settings.phonecenter_search_max_results or 30)),
}
@api.model
def phonecenter_get_search_config(self):
cfg = self._phonecenter_get_settings()
return {
'company_provider': cfg.get('company_provider', 'google_places'),
'ai_provider': cfg.get('ai_provider', 'openai'),
'min_entries': cfg.get('min_entries', 8),
'max_results': cfg.get('max_results', 30),
'radius_max': 2000,
}
@api.model
def phonecenter_check_location(self, query):
query = (query or '').strip()
if not query:
return {'found': False, 'message': 'Kein Ort angegeben.'}
cfg = self._phonecenter_get_settings()
google_key = cfg.get('google_maps_key')
if google_key:
try:
response = requests.get(
'https://maps.googleapis.com/maps/api/geocode/json',
params={
'address': query,
'key': google_key,
'language': 'de',
},
timeout=cfg.get('company_timeout', 20),
)
payload = response.json()
if payload.get('status') == 'OK' and payload.get('results'):
first = payload['results'][0]
location = first.get('geometry', {}).get('location', {})
return {
'found': True,
'provider': 'google_geocode',
'query': query,
'lat': location.get('lat'),
'lng': location.get('lng'),
'address': first.get('formatted_address') or query,
}
except Exception as ex:
_logger.warning('Phonecenter location check via Google failed: %s', ex)
try:
response = requests.get(
'https://nominatim.openstreetmap.org/search',
params={
'q': query,
'format': 'json',
'limit': 1,
'addressdetails': 1,
},
headers={
'User-Agent': 'DigitalSignagePhoneCenter/1.0',
},
timeout=cfg.get('company_timeout', 20),
)
payload = response.json()
if isinstance(payload, list) and payload:
first = payload[0]
return {
'found': True,
'provider': 'nominatim',
'query': query,
'lat': float(first.get('lat') or 0.0),
'lng': float(first.get('lon') or 0.0),
'address': first.get('display_name') or query,
}
except Exception as ex:
_logger.warning('Phonecenter location check via Nominatim failed: %s', ex)
return {'found': False, 'query': query, 'message': 'Ort nicht gefunden.'}
@api.model
def _phonecenter_normalize_company_item(self, item):
if not isinstance(item, dict):
return None
name = (item.get('name') or '').strip()
if not name:
return None
return {
'id': str(item.get('id') or item.get('place_id') or item.get('external_id') or name),
'place_id': (item.get('place_id') or '').strip(),
'name': name,
'address': (item.get('address') or item.get('formatted_address') or '').strip(),
'city': (item.get('city') or '').strip(),
'website': (item.get('website') or '').strip(),
'phone': (item.get('phone') or '').strip(),
'email': (item.get('email') or '').strip(),
'rating': item.get('rating') or 0,
'source': (item.get('source') or '').strip(),
'google_maps_url': (item.get('google_maps_url') or '').strip(),
'types': item.get('types') or [],
'lat': item.get('lat'),
'lng': item.get('lng'),
'verified': bool(item.get('verified', False)),
'verified_at': item.get('verified_at') or '',
}
@api.model
def _phonecenter_analyze_company(self, company, cfg):
company = company or {}
name = company.get('name') or ''
website = company.get('website') or ''
address = company.get('address') or ''
phone = company.get('phone') or ''
rating = company.get('rating') or ''
fallback = 'Firma: %s | Telefon: %s | Webseite: %s | Adresse: %s | Rating: %s' % (
name or '-',
phone or '-',
website or '-',
address or '-',
rating or '-',
)
ai_provider = (cfg.get('ai_provider') or 'openai').strip()
if ai_provider not in ('openai', 'custom_api'):
return fallback
api_key = cfg.get('ai_api_key') or ''
api_url = (cfg.get('ai_api_url') or '').strip()
if not api_url:
if ai_provider == 'openai':
api_url = 'https://api.openai.com/v1'
else:
return fallback
if not api_key:
return fallback
try:
if ai_provider == 'openai':
response = requests.post(
'%s/chat/completions' % api_url.rstrip('/'),
headers={
'Authorization': 'Bearer %s' % api_key,
'Content-Type': 'application/json',
},
json={
'model': cfg.get('ai_model') or 'gpt-4o-mini',
'temperature': 0.2,
'messages': [
{'role': 'system', 'content': 'Du analysierst Firmenkontakte fuer ein deutsches Callcenter. Antworte kurz in maximal 4 Saetzen.'},
{'role': 'user', 'content': json.dumps({
'task': 'Analysiere den Firmenkontakt fuer Erstansprache, nenne Nutzenargument, Risiko und naechsten Schritt.',
'company': company,
}, ensure_ascii=False)},
],
},
timeout=cfg.get('company_timeout', 20),
)
payload = response.json() if response else {}
content = (((payload.get('choices') or [{}])[0].get('message') or {}).get('content') or '').strip()
return content or fallback
response = requests.post(
api_url,
headers={
'Authorization': 'Bearer %s' % api_key,
'Content-Type': 'application/json',
},
json={
'task': 'company_analysis',
'company': company,
},
timeout=cfg.get('company_timeout', 20),
)
payload = response.json() if response else {}
if isinstance(payload, dict):
return (payload.get('analysis') or payload.get('summary') or '').strip() or fallback
return fallback
except Exception as ex:
_logger.warning('Phonecenter company analysis failed: %s', ex)
return fallback
@api.model
def phonecenter_review_bad_contact(self, payload):
payload = payload or {}
cfg = self._phonecenter_get_settings()
reason = (payload.get('reason') or '').strip()
contact = payload.get('contact') or {}
note = (payload.get('note') or '').strip()
if not isinstance(contact, dict):
contact = {}
if not reason:
return {
'ok': False,
'message': 'Bitte Grund fuer die Einschaetzung angeben.',
}
fallback_summary = (
'Einschaetzung: %s. Kontakt: %s (%s). Telefon: %s. Nächster Schritt: Wiedervorlage oder weitere Recherche pruefen.'
% (
reason,
contact.get('company') or '-',
contact.get('city') or '-',
contact.get('phone') or '-',
)
)
fallback_actions = ['reschedule', 'research', 'keep', 'discard']
ai_provider = (cfg.get('ai_provider') or 'openai').strip()
api_key = cfg.get('ai_api_key') or ''
api_url = (cfg.get('ai_api_url') or '').strip()
model = cfg.get('ai_model') or 'gpt-4o-mini'
if ai_provider == 'none' or not api_key:
return {
'ok': True,
'summary': fallback_summary,
'actions': fallback_actions,
'source': 'fallback',
}
if not api_url and ai_provider == 'openai':
api_url = 'https://api.openai.com/v1'
try:
if ai_provider == 'openai':
response = requests.post(
'%s/chat/completions' % api_url.rstrip('/'),
headers={
'Authorization': 'Bearer %s' % api_key,
'Content-Type': 'application/json',
},
json={
'model': model,
'temperature': 0.2,
'messages': [
{
'role': 'system',
'content': (
'Du analysierst schlechte Callcenter-Kontakte. '
'Antworte nur als JSON mit keys "summary" und "actions". '
'actions muss eine Teilmenge aus ["reschedule", "discard", "research", "keep"] sein.'
),
},
{
'role': 'user',
'content': json.dumps({
'reason': reason,
'contact': contact,
'classification_note': note,
'task': 'Erzeuge eine kurze Zusammenfassung und passende naechste Schritte.',
}, ensure_ascii=False),
},
],
'response_format': {'type': 'json_object'},
},
timeout=cfg.get('company_timeout', 20),
)
data = response.json() if response else {}
content = (((data.get('choices') or [{}])[0].get('message') or {}).get('content') or '').strip()
parsed = json.loads(content) if content else {}
actions = parsed.get('actions') if isinstance(parsed, dict) else []
if not isinstance(actions, list):
actions = []
cleaned = [a for a in actions if a in fallback_actions]
return {
'ok': True,
'summary': (parsed.get('summary') if isinstance(parsed, dict) else '') or fallback_summary,
'actions': cleaned or fallback_actions,
'source': 'llm',
}
if ai_provider == 'custom_api' and api_url:
response = requests.post(
api_url,
headers={
'Authorization': 'Bearer %s' % api_key,
'Content-Type': 'application/json',
},
json={
'task': 'bad_contact_review',
'reason': reason,
'contact': contact,
'classification_note': note,
},
timeout=cfg.get('company_timeout', 20),
)
data = response.json() if response else {}
actions = data.get('actions', []) if isinstance(data, dict) else []
if not isinstance(actions, list):
actions = []
cleaned = [a for a in actions if a in fallback_actions]
return {
'ok': True,
'summary': (data.get('summary') if isinstance(data, dict) else '') or fallback_summary,
'actions': cleaned or fallback_actions,
'source': 'custom_api',
}
except Exception as ex:
_logger.warning('Phonecenter bad contact review failed: %s', ex)
return {
'ok': True,
'summary': fallback_summary,
'actions': fallback_actions,
'source': 'fallback',
}
@api.model
def _phonecenter_dedupe_companies(self, items):
deduped = []
seen = set()
for raw in items or []:
item = self._phonecenter_normalize_company_item(raw)
if not item:
continue
key = (item.get('name', '').lower().strip(), item.get('address', '').lower().strip())
if key in seen:
continue
seen.add(key)
deduped.append(item)
return deduped
@api.model
def _phonecenter_search_google_places(self, industry, lat, lng, radius_km, cfg):
key = cfg.get('company_api_key') or cfg.get('google_maps_key')
if not key:
return []
query = (industry or '').strip() or 'unternehmen'
radius_m = max(1000, int((radius_km or 10) * 1000))
params = {
'query': query,
'location': f'{lat},{lng}',
'radius': radius_m,
'key': key,
'language': 'de',
}
items = []
try:
response = requests.get(
'https://maps.googleapis.com/maps/api/place/textsearch/json',
params=params,
timeout=cfg.get('company_timeout', 20),
)
payload = response.json()
for row in payload.get('results', [])[:cfg.get('max_results', 30)]:
location = row.get('geometry', {}).get('location', {})
items.append({
'id': row.get('place_id'),
'place_id': row.get('place_id'),
'name': row.get('name'),
'address': row.get('formatted_address') or row.get('vicinity') or '',
'rating': row.get('rating') or 0,
'city': '',
'source': 'google_places',
'lat': location.get('lat'),
'lng': location.get('lng'),
})
except Exception as ex:
_logger.warning('Phonecenter Google Places search failed: %s', ex)
return items
@api.model
def _phonecenter_search_custom_company_api(self, industry, lat, lng, radius_km, cfg):
api_url = (cfg.get('company_api_url') or '').strip()
if not api_url:
return []
headers = {'Content-Type': 'application/json'}
if cfg.get('company_api_key'):
headers['Authorization'] = 'Bearer %s' % cfg.get('company_api_key')
body = {
'industry': industry,
'lat': lat,
'lng': lng,
'radius_km': radius_km,
'limit': cfg.get('max_results', 30),
}
try:
response = requests.post(api_url, headers=headers, json=body, timeout=cfg.get('company_timeout', 20))
payload = response.json()
if isinstance(payload, dict):
return payload.get('companies', []) or []
except Exception as ex:
_logger.warning('Phonecenter custom company API search failed: %s', ex)
return []
@api.model
def _phonecenter_enrich_companies_openai(self, companies, industry, location_query, cfg):
api_url = (cfg.get('ai_api_url') or '').rstrip('/')
api_key = cfg.get('ai_api_key') or ''
model = cfg.get('ai_model') or 'gpt-4o-mini'
if not api_url or not api_key:
return companies
prompt = {
'industry': industry,
'location': location_query,
'companies': companies,
'task': 'Normalisiere, entferne Dubletten und bewerte Relevanz fuer Branchen- und Ortsbezug. Antworte als JSON-Objekt mit key "companies".',
}
try:
response = requests.post(
'%s/chat/completions' % api_url,
headers={
'Authorization': 'Bearer %s' % api_key,
'Content-Type': 'application/json',
},
json={
'model': model,
'temperature': 0.1,
'messages': [
{'role': 'system', 'content': 'Du bist ein Daten-Normalisierer fuer Firmenlisten. Gib nur gueltiges JSON zurueck.'},
{'role': 'user', 'content': json.dumps(prompt, ensure_ascii=False)},
],
'response_format': {'type': 'json_object'},
},
timeout=cfg.get('company_timeout', 20),
)
data = response.json()
content = (((data.get('choices') or [{}])[0].get('message') or {}).get('content') or '').strip()
parsed = json.loads(content) if content else {}
enriched = parsed.get('companies', []) if isinstance(parsed, dict) else []
return self._phonecenter_dedupe_companies(enriched) or companies
except Exception as ex:
_logger.warning('Phonecenter OpenAI enrichment failed: %s', ex)
return companies
@api.model
def _phonecenter_enrich_companies_custom(self, companies, industry, location_query, cfg):
api_url = (cfg.get('ai_api_url') or '').strip()
if not api_url:
return companies
headers = {'Content-Type': 'application/json'}
if cfg.get('ai_api_key'):
headers['Authorization'] = 'Bearer %s' % cfg.get('ai_api_key')
body = {
'industry': industry,
'location': location_query,
'companies': companies,
}
try:
response = requests.post(api_url, headers=headers, json=body, timeout=cfg.get('company_timeout', 20))
payload = response.json()
if isinstance(payload, dict):
enriched = payload.get('companies', []) or []
normalized = self._phonecenter_dedupe_companies(enriched)
return normalized or companies
except Exception as ex:
_logger.warning('Phonecenter custom AI enrichment failed: %s', ex)
return companies
@api.model
def _phonecenter_get_google_place_details(self, place_id, cfg):
key = cfg.get('company_api_key') or cfg.get('google_maps_key')
if not key or not place_id:
return {}
detail_fields = ','.join([
'name',
'formatted_address',
'formatted_phone_number',
'international_phone_number',
'website',
'rating',
'user_ratings_total',
'business_status',
'types',
'url',
'geometry',
])
try:
response = requests.get(
'https://maps.googleapis.com/maps/api/place/details/json',
params={
'place_id': place_id,
'fields': detail_fields,
'key': key,
'language': 'de',
},
timeout=cfg.get('company_timeout', 20),
)
payload = response.json()
if payload.get('status') != 'OK':
return {}
details = payload.get('result') or {}
return {
'id': place_id,
'place_id': place_id,
'name': details.get('name') or '',
'address': details.get('formatted_address') or '',
'phone': details.get('formatted_phone_number') or details.get('international_phone_number') or '',
'website': details.get('website') or '',
'email': '',
'rating': details.get('rating') or 0,
'source': 'google_places',
'google_maps_url': details.get('url') or '',
'types': details.get('types') or [],
'verified': True,
'verified_at': fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'business_status': details.get('business_status') or '',
'lat': (details.get('geometry') or {}).get('location', {}).get('lat'),
'lng': (details.get('geometry') or {}).get('location', {}).get('lng'),
}
except Exception as ex:
_logger.warning('Phonecenter Google Place details failed: %s', ex)
return {}
@api.model
def phonecenter_get_company_details(self, payload):
payload = payload or {}
cfg = self._phonecenter_get_settings()
company = payload.get('company') or {}
if not isinstance(company, dict):
company = {}
provider = (company.get('source') or cfg.get('company_provider') or 'google_places').strip()
place_id = (company.get('place_id') or company.get('id') or '').strip()
details = {}
if provider == 'google_places' and place_id:
details = self._phonecenter_get_google_place_details(place_id, cfg)
if not details:
details = self._phonecenter_normalize_company_item(company) or {}
if details:
details['verified'] = bool(details.get('verified', False))
details['verified_at'] = details.get('verified_at') or ''
if not details:
return {
'ok': False,
'message': 'Keine Unternehmensdetails gefunden.',
}
details.setdefault('source', provider)
details['analysis'] = self._phonecenter_analyze_company(details, cfg)
return {
'ok': True,
'company': details,
}
@api.model
def phonecenter_search_companies(self, payload):
payload = payload or {}
cfg = self._phonecenter_get_settings()
current_options = payload.get('current_options') or []
current_list = self._phonecenter_dedupe_companies(current_options)
min_entries = cfg.get('min_entries', 8)
if current_list and len(current_list) < min_entries:
return {
'companies': current_list,
'added_count': 0,
'search_blocked': True,
'min_entries': min_entries,
'message': 'Naechste Suche erst ab Mindestanzahl moeglich.',
}
industry = (payload.get('industry') or '').strip()
location_query = (payload.get('location_query') or '').strip()
radius_km = int(payload.get('radius_km') or 20)
lat = payload.get('lat')
lng = payload.get('lng')
if (lat is None or lng is None) and location_query:
location_info = self.phonecenter_check_location(location_query)
lat = location_info.get('lat')
lng = location_info.get('lng')
if lat is None or lng is None:
return {
'companies': current_list,
'added_count': 0,
'search_blocked': bool(current_list and len(current_list) < min_entries),
'min_entries': min_entries,
'message': 'Kein gueltiger Ort fuer die Suche vorhanden.',
}
provider = cfg.get('company_provider', 'google_places')
found = []
if provider == 'google_places':
found = self._phonecenter_search_google_places(industry, lat, lng, radius_km, cfg)
elif provider == 'custom_api':
found = self._phonecenter_search_custom_company_api(industry, lat, lng, radius_km, cfg)
if not found:
found = [{
'id': 'fallback-%s-1' % (industry or 'unternehmen'),
'name': '%s %s 1' % (industry or 'Unternehmen', location_query or ''),
'city': location_query,
'source': 'mock',
}]
normalized_new = self._phonecenter_dedupe_companies(found)
ai_provider = cfg.get('ai_provider', 'openai')
if ai_provider == 'openai':
normalized_new = self._phonecenter_enrich_companies_openai(normalized_new, industry, location_query, cfg)
elif ai_provider == 'custom_api':
normalized_new = self._phonecenter_enrich_companies_custom(normalized_new, industry, location_query, cfg)
combined = self._phonecenter_dedupe_companies(current_list + normalized_new)
combined = combined[:cfg.get('max_results', 30)]
return {
'companies': combined,
'added_count': max(0, len(combined) - len(current_list)),
'search_blocked': bool(combined and len(combined) < min_entries),
'min_entries': min_entries,
'radius_km': radius_km,
'provider': provider,
'ai_provider': ai_provider,
}
class dssSettingsSave(models.Model):
_name = "dss.viewsettings"
_description = "DigitalSignage Ansicht Einstellungen"
_inherit = []
uuid = fields.Char(default=lambda self: self._default_uuid(), required=True, readonly=True, copy=False, string='UUID')
user = fields.Char('User')
user_id = fields.Many2one('res.users',string='User_id')
model_view = fields.Char('Model')
model_datensatz = fields.Char('Datensatz')
viewtype = fields.Char('Ansichtstyp')
column = fields.Char('Spalte')
column_size = fields.Char('Spalten Grösse')
state_visible = fields.Char('Status sichtbar')
state_collapse = fields.Char('Status zusammengeklappt')
@api.model
def _default_uuid(self):
return str(uuid.uuid4())
def _default_create_user(self):
return str(self.env.user.name)
def _default_create_user_id(self):
return self.env.user.id