This commit is contained in:
jopster 2026-07-23 08:54:44 +02:00
parent 9c218ba3aa
commit 72cb1ff9c9
736 changed files with 12489 additions and 446 deletions

514
.gitignore vendored Executable file
View File

@ -0,0 +1,514 @@
models/__pycache__/*
static/src/js/ace/info.txt
static/src/js/ace/src-noconflict/ace.js
static/src/js/ace/src-noconflict/ext-beautify.js
static/src/js/ace/src-noconflict/ext-code_lens.js
static/src/js/ace/src-noconflict/ext-command_bar.js
static/src/js/ace/src-noconflict/ext-diff.js
static/src/js/ace/src-noconflict/ext-elastic_tabstops_lite.js
static/src/js/ace/src-noconflict/ext-emmet.js
static/src/js/ace/src-noconflict/ext-error_marker.js
static/src/js/ace/src-noconflict/ext-hardwrap.js
static/src/js/ace/src-noconflict/ext-inline_autocomplete.js
static/src/js/ace/src-noconflict/ext-keybinding_menu.js
static/src/js/ace/src-noconflict/ext-language_tools.js
static/src/js/ace/src-noconflict/ext-linking.js
static/src/js/ace/src-noconflict/ext-modelist.js
static/src/js/ace/src-noconflict/ext-options.js
static/src/js/ace/src-noconflict/ext-prompt.js
static/src/js/ace/src-noconflict/ext-rtl.js
static/src/js/ace/src-noconflict/ext-searchbox.js
static/src/js/ace/src-noconflict/ext-settings_menu.js
static/src/js/ace/src-noconflict/ext-simple_tokenizer.js
static/src/js/ace/src-noconflict/ext-spellcheck.js
static/src/js/ace/src-noconflict/ext-split.js
static/src/js/ace/src-noconflict/ext-static_highlight.js
static/src/js/ace/src-noconflict/ext-statusbar.js
static/src/js/ace/src-noconflict/ext-textarea.js
static/src/js/ace/src-noconflict/ext-themelist.js
static/src/js/ace/src-noconflict/ext-whitespace.js
static/src/js/ace/src-noconflict/info.txt
static/src/js/ace/src-noconflict/keybinding-emacs.js
static/src/js/ace/src-noconflict/keybinding-sublime.js
static/src/js/ace/src-noconflict/keybinding-vim.js
static/src/js/ace/src-noconflict/keybinding-vscode.js
static/src/js/ace/src-noconflict/mode-abap.js
static/src/js/ace/src-noconflict/mode-abc.js
static/src/js/ace/src-noconflict/mode-actionscript.js
static/src/js/ace/src-noconflict/mode-ada.js
static/src/js/ace/src-noconflict/mode-alda.js
static/src/js/ace/src-noconflict/mode-apache_conf.js
static/src/js/ace/src-noconflict/mode-apex.js
static/src/js/ace/src-noconflict/mode-applescript.js
static/src/js/ace/src-noconflict/mode-aql.js
static/src/js/ace/src-noconflict/mode-asciidoc.js
static/src/js/ace/src-noconflict/mode-asl.js
static/src/js/ace/src-noconflict/mode-assembly_arm32.js
static/src/js/ace/src-noconflict/mode-assembly_x86.js
static/src/js/ace/src-noconflict/mode-astro.js
static/src/js/ace/src-noconflict/mode-autohotkey.js
static/src/js/ace/src-noconflict/mode-basic.js
static/src/js/ace/src-noconflict/mode-batchfile.js
static/src/js/ace/src-noconflict/mode-bibtex.js
static/src/js/ace/src-noconflict/mode-c_cpp.js
static/src/js/ace/src-noconflict/mode-c9search.js
static/src/js/ace/src-noconflict/mode-cirru.js
static/src/js/ace/src-noconflict/mode-clojure.js
static/src/js/ace/src-noconflict/mode-clue.js
static/src/js/ace/src-noconflict/mode-cobol.js
static/src/js/ace/src-noconflict/mode-coffee.js
static/src/js/ace/src-noconflict/mode-coldfusion.js
static/src/js/ace/src-noconflict/mode-crystal.js
static/src/js/ace/src-noconflict/mode-csharp.js
static/src/js/ace/src-noconflict/mode-csound_document.js
static/src/js/ace/src-noconflict/mode-csound_orchestra.js
static/src/js/ace/src-noconflict/mode-csound_score.js
static/src/js/ace/src-noconflict/mode-csp.js
static/src/js/ace/src-noconflict/mode-css.js
static/src/js/ace/src-noconflict/mode-csv.js
static/src/js/ace/src-noconflict/mode-curly.js
static/src/js/ace/src-noconflict/mode-cuttlefish.js
static/src/js/ace/src-noconflict/mode-d.js
static/src/js/ace/src-noconflict/mode-dart.js
static/src/js/ace/src-noconflict/mode-diff.js
static/src/js/ace/src-noconflict/mode-django.js
static/src/js/ace/src-noconflict/mode-dockerfile.js
static/src/js/ace/src-noconflict/mode-dot.js
static/src/js/ace/src-noconflict/mode-drools.js
static/src/js/ace/src-noconflict/mode-edifact.js
static/src/js/ace/src-noconflict/mode-eiffel.js
static/src/js/ace/src-noconflict/mode-ejs.js
static/src/js/ace/src-noconflict/mode-elixir.js
static/src/js/ace/src-noconflict/mode-elm.js
static/src/js/ace/src-noconflict/mode-erlang.js
static/src/js/ace/src-noconflict/mode-flix.js
static/src/js/ace/src-noconflict/mode-forth.js
static/src/js/ace/src-noconflict/mode-fortran.js
static/src/js/ace/src-noconflict/mode-fsharp.js
static/src/js/ace/src-noconflict/mode-fsl.js
static/src/js/ace/src-noconflict/mode-ftl.js
static/src/js/ace/src-noconflict/mode-gcode.js
static/src/js/ace/src-noconflict/mode-gherkin.js
static/src/js/ace/src-noconflict/mode-gitignore.js
static/src/js/ace/src-noconflict/mode-glsl.js
static/src/js/ace/src-noconflict/mode-gobstones.js
static/src/js/ace/src-noconflict/mode-golang.js
static/src/js/ace/src-noconflict/mode-graphqlschema.js
static/src/js/ace/src-noconflict/mode-groovy.js
static/src/js/ace/src-noconflict/mode-haml.js
static/src/js/ace/src-noconflict/mode-handlebars.js
static/src/js/ace/src-noconflict/mode-haskell_cabal.js
static/src/js/ace/src-noconflict/mode-haskell.js
static/src/js/ace/src-noconflict/mode-haxe.js
static/src/js/ace/src-noconflict/mode-hjson.js
static/src/js/ace/src-noconflict/mode-html_elixir.js
static/src/js/ace/src-noconflict/mode-html_ruby.js
static/src/js/ace/src-noconflict/mode-html.js
static/src/js/ace/src-noconflict/mode-ini.js
static/src/js/ace/src-noconflict/mode-io.js
static/src/js/ace/src-noconflict/mode-ion.js
static/src/js/ace/src-noconflict/mode-jack.js
static/src/js/ace/src-noconflict/mode-jade.js
static/src/js/ace/src-noconflict/mode-java.js
static/src/js/ace/src-noconflict/mode-javascript.js
static/src/js/ace/src-noconflict/mode-jexl.js
static/src/js/ace/src-noconflict/mode-json.js
static/src/js/ace/src-noconflict/mode-json5.js
static/src/js/ace/src-noconflict/mode-jsoniq.js
static/src/js/ace/src-noconflict/mode-jsp.js
static/src/js/ace/src-noconflict/mode-jssm.js
static/src/js/ace/src-noconflict/mode-jsx.js
static/src/js/ace/src-noconflict/mode-julia.js
static/src/js/ace/src-noconflict/mode-kotlin.js
static/src/js/ace/src-noconflict/mode-latex.js
static/src/js/ace/src-noconflict/mode-latte.js
static/src/js/ace/src-noconflict/mode-less.js
static/src/js/ace/src-noconflict/mode-liquid.js
static/src/js/ace/src-noconflict/mode-lisp.js
static/src/js/ace/src-noconflict/mode-livescript.js
static/src/js/ace/src-noconflict/mode-logiql.js
static/src/js/ace/src-noconflict/mode-logtalk.js
static/src/js/ace/src-noconflict/mode-lsl.js
static/src/js/ace/src-noconflict/mode-lua.js
static/src/js/ace/src-noconflict/mode-luapage.js
static/src/js/ace/src-noconflict/mode-lucene.js
static/src/js/ace/src-noconflict/mode-makefile.js
static/src/js/ace/src-noconflict/mode-markdown.js
static/src/js/ace/src-noconflict/mode-mask.js
static/src/js/ace/src-noconflict/mode-matlab.js
static/src/js/ace/src-noconflict/mode-maze.js
static/src/js/ace/src-noconflict/mode-mediawiki.js
static/src/js/ace/src-noconflict/mode-mel.js
static/src/js/ace/src-noconflict/mode-mips.js
static/src/js/ace/src-noconflict/mode-mixal.js
static/src/js/ace/src-noconflict/mode-mushcode.js
static/src/js/ace/src-noconflict/mode-mysql.js
static/src/js/ace/src-noconflict/mode-nasal.js
static/src/js/ace/src-noconflict/mode-nginx.js
static/src/js/ace/src-noconflict/mode-nim.js
static/src/js/ace/src-noconflict/mode-nix.js
static/src/js/ace/src-noconflict/mode-nsis.js
static/src/js/ace/src-noconflict/mode-nunjucks.js
static/src/js/ace/src-noconflict/mode-objectivec.js
static/src/js/ace/src-noconflict/mode-ocaml.js
static/src/js/ace/src-noconflict/mode-odin.js
static/src/js/ace/src-noconflict/mode-partiql.js
static/src/js/ace/src-noconflict/mode-pascal.js
static/src/js/ace/src-noconflict/mode-perl.js
static/src/js/ace/src-noconflict/mode-pgsql.js
static/src/js/ace/src-noconflict/mode-php_laravel_blade.js
static/src/js/ace/src-noconflict/mode-php.js
static/src/js/ace/src-noconflict/mode-pig.js
static/src/js/ace/src-noconflict/mode-plain_text.js
static/src/js/ace/src-noconflict/mode-plsql.js
static/src/js/ace/src-noconflict/mode-powershell.js
static/src/js/ace/src-noconflict/mode-praat.js
static/src/js/ace/src-noconflict/mode-prisma.js
static/src/js/ace/src-noconflict/mode-prolog.js
static/src/js/ace/src-noconflict/mode-properties.js
static/src/js/ace/src-noconflict/mode-protobuf.js
static/src/js/ace/src-noconflict/mode-prql.js
static/src/js/ace/src-noconflict/mode-puppet.js
static/src/js/ace/src-noconflict/mode-python.js
static/src/js/ace/src-noconflict/mode-qml.js
static/src/js/ace/src-noconflict/mode-r.js
static/src/js/ace/src-noconflict/mode-raku.js
static/src/js/ace/src-noconflict/mode-razor.js
static/src/js/ace/src-noconflict/mode-rdoc.js
static/src/js/ace/src-noconflict/mode-red.js
static/src/js/ace/src-noconflict/mode-redshift.js
static/src/js/ace/src-noconflict/mode-rhtml.js
static/src/js/ace/src-noconflict/mode-robot.js
static/src/js/ace/src-noconflict/mode-rst.js
static/src/js/ace/src-noconflict/mode-ruby.js
static/src/js/ace/src-noconflict/mode-rust.js
static/src/js/ace/src-noconflict/mode-sac.js
static/src/js/ace/src-noconflict/mode-sass.js
static/src/js/ace/src-noconflict/mode-scad.js
static/src/js/ace/src-noconflict/mode-scala.js
static/src/js/ace/src-noconflict/mode-scheme.js
static/src/js/ace/src-noconflict/mode-scrypt.js
static/src/js/ace/src-noconflict/mode-scss.js
static/src/js/ace/src-noconflict/mode-sh.js
static/src/js/ace/src-noconflict/mode-sjs.js
static/src/js/ace/src-noconflict/mode-slim.js
static/src/js/ace/src-noconflict/mode-smarty.js
static/src/js/ace/src-noconflict/mode-smithy.js
static/src/js/ace/src-noconflict/mode-snippets.js
static/src/js/ace/src-noconflict/mode-soy_template.js
static/src/js/ace/src-noconflict/mode-space.js
static/src/js/ace/src-noconflict/mode-sparql.js
static/src/js/ace/src-noconflict/mode-sql.js
static/src/js/ace/src-noconflict/mode-sqlserver.js
static/src/js/ace/src-noconflict/mode-stylus.js
static/src/js/ace/src-noconflict/mode-svg.js
static/src/js/ace/src-noconflict/mode-swift.js
static/src/js/ace/src-noconflict/mode-tcl.js
static/src/js/ace/src-noconflict/mode-terraform.js
static/src/js/ace/src-noconflict/mode-tex.js
static/src/js/ace/src-noconflict/mode-text.js
static/src/js/ace/src-noconflict/mode-textile.js
static/src/js/ace/src-noconflict/mode-toml.js
static/src/js/ace/src-noconflict/mode-tsv.js
static/src/js/ace/src-noconflict/mode-tsx.js
static/src/js/ace/src-noconflict/mode-turtle.js
static/src/js/ace/src-noconflict/mode-twig.js
static/src/js/ace/src-noconflict/mode-typescript.js
static/src/js/ace/src-noconflict/mode-vala.js
static/src/js/ace/src-noconflict/mode-vbscript.js
static/src/js/ace/src-noconflict/mode-velocity.js
static/src/js/ace/src-noconflict/mode-verilog.js
static/src/js/ace/src-noconflict/mode-vhdl.js
static/src/js/ace/src-noconflict/mode-visualforce.js
static/src/js/ace/src-noconflict/mode-vue.js
static/src/js/ace/src-noconflict/mode-wollok.js
static/src/js/ace/src-noconflict/mode-xml.js
static/src/js/ace/src-noconflict/mode-xquery.js
static/src/js/ace/src-noconflict/mode-yaml.js
static/src/js/ace/src-noconflict/mode-zeek.js
static/src/js/ace/src-noconflict/mode-zig.js
static/src/js/ace/src-noconflict/theme-ambiance.js
static/src/js/ace/src-noconflict/theme-chaos.js
static/src/js/ace/src-noconflict/theme-chrome.js
static/src/js/ace/src-noconflict/theme-cloud_editor_dark.js
static/src/js/ace/src-noconflict/theme-cloud_editor.js
static/src/js/ace/src-noconflict/theme-cloud9_day.js
static/src/js/ace/src-noconflict/theme-cloud9_night_low_color.js
static/src/js/ace/src-noconflict/theme-cloud9_night.js
static/src/js/ace/src-noconflict/theme-clouds_midnight.js
static/src/js/ace/src-noconflict/theme-clouds.js
static/src/js/ace/src-noconflict/theme-cobalt.js
static/src/js/ace/src-noconflict/theme-crimson_editor.js
static/src/js/ace/src-noconflict/theme-dawn.js
static/src/js/ace/src-noconflict/theme-dracula.js
static/src/js/ace/src-noconflict/theme-dreamweaver.js
static/src/js/ace/src-noconflict/theme-eclipse.js
static/src/js/ace/src-noconflict/theme-github_dark.js
static/src/js/ace/src-noconflict/theme-github_light_default.js
static/src/js/ace/src-noconflict/theme-github.js
static/src/js/ace/src-noconflict/theme-gob.js
static/src/js/ace/src-noconflict/theme-gruvbox_dark_hard.js
static/src/js/ace/src-noconflict/theme-gruvbox_light_hard.js
static/src/js/ace/src-noconflict/theme-gruvbox.js
static/src/js/ace/src-noconflict/theme-idle_fingers.js
static/src/js/ace/src-noconflict/theme-iplastic.js
static/src/js/ace/src-noconflict/theme-katzenmilch.js
static/src/js/ace/src-noconflict/theme-kr_theme.js
static/src/js/ace/src-noconflict/theme-kuroir.js
static/src/js/ace/src-noconflict/theme-merbivore_soft.js
static/src/js/ace/src-noconflict/theme-merbivore.js
static/src/js/ace/src-noconflict/theme-mono_industrial.js
static/src/js/ace/src-noconflict/theme-monokai.js
static/src/js/ace/src-noconflict/theme-nord_dark.js
static/src/js/ace/src-noconflict/theme-one_dark.js
static/src/js/ace/src-noconflict/theme-pastel_on_dark.js
static/src/js/ace/src-noconflict/theme-solarized_dark.js
static/src/js/ace/src-noconflict/theme-solarized_light.js
static/src/js/ace/src-noconflict/theme-sqlserver.js
static/src/js/ace/src-noconflict/theme-terminal.js
static/src/js/ace/src-noconflict/theme-textmate.js
static/src/js/ace/src-noconflict/theme-tomorrow_night_blue.js
static/src/js/ace/src-noconflict/theme-tomorrow_night_bright.js
static/src/js/ace/src-noconflict/theme-tomorrow_night_eighties.js
static/src/js/ace/src-noconflict/theme-tomorrow_night.js
static/src/js/ace/src-noconflict/theme-tomorrow.js
static/src/js/ace/src-noconflict/theme-twilight.js
static/src/js/ace/src-noconflict/theme-vibrant_ink.js
static/src/js/ace/src-noconflict/theme-xcode.js
static/src/js/ace/src-noconflict/worker-base.js
static/src/js/ace/src-noconflict/worker-coffee.js
static/src/js/ace/src-noconflict/worker-css.js
static/src/js/ace/src-noconflict/worker-html.js
static/src/js/ace/src-noconflict/worker-javascript.js
static/src/js/ace/src-noconflict/worker-json.js
static/src/js/ace/src-noconflict/worker-lua.js
static/src/js/ace/src-noconflict/worker-php.js
static/src/js/ace/src-noconflict/worker-xml.js
static/src/js/ace/src-noconflict/worker-xquery.js
static/src/js/ace/src-noconflict/worker-yaml.js
static/src/js/ace/src-noconflict/snippets/abap.js
static/src/js/ace/src-noconflict/snippets/abc.js
static/src/js/ace/src-noconflict/snippets/actionscript.js
static/src/js/ace/src-noconflict/snippets/ada.js
static/src/js/ace/src-noconflict/snippets/alda.js
static/src/js/ace/src-noconflict/snippets/apache_conf.js
static/src/js/ace/src-noconflict/snippets/apex.js
static/src/js/ace/src-noconflict/snippets/applescript.js
static/src/js/ace/src-noconflict/snippets/aql.js
static/src/js/ace/src-noconflict/snippets/asciidoc.js
static/src/js/ace/src-noconflict/snippets/asl.js
static/src/js/ace/src-noconflict/snippets/assembly_arm32.js
static/src/js/ace/src-noconflict/snippets/assembly_x86.js
static/src/js/ace/src-noconflict/snippets/astro.js
static/src/js/ace/src-noconflict/snippets/autohotkey.js
static/src/js/ace/src-noconflict/snippets/basic.js
static/src/js/ace/src-noconflict/snippets/batchfile.js
static/src/js/ace/src-noconflict/snippets/bibtex.js
static/src/js/ace/src-noconflict/snippets/c_cpp.js
static/src/js/ace/src-noconflict/snippets/c9search.js
static/src/js/ace/src-noconflict/snippets/cirru.js
static/src/js/ace/src-noconflict/snippets/clojure.js
static/src/js/ace/src-noconflict/snippets/clue.js
static/src/js/ace/src-noconflict/snippets/cobol.js
static/src/js/ace/src-noconflict/snippets/coffee.js
static/src/js/ace/src-noconflict/snippets/coldfusion.js
static/src/js/ace/src-noconflict/snippets/crystal.js
static/src/js/ace/src-noconflict/snippets/csharp.js
static/src/js/ace/src-noconflict/snippets/csound_document.js
static/src/js/ace/src-noconflict/snippets/csound_orchestra.js
static/src/js/ace/src-noconflict/snippets/csound_score.js
static/src/js/ace/src-noconflict/snippets/csp.js
static/src/js/ace/src-noconflict/snippets/css.js
static/src/js/ace/src-noconflict/snippets/csv.js
static/src/js/ace/src-noconflict/snippets/curly.js
static/src/js/ace/src-noconflict/snippets/cuttlefish.js
static/src/js/ace/src-noconflict/snippets/d.js
static/src/js/ace/src-noconflict/snippets/dart.js
static/src/js/ace/src-noconflict/snippets/diff.js
static/src/js/ace/src-noconflict/snippets/django.js
static/src/js/ace/src-noconflict/snippets/dockerfile.js
static/src/js/ace/src-noconflict/snippets/dot.js
static/src/js/ace/src-noconflict/snippets/drools.js
static/src/js/ace/src-noconflict/snippets/edifact.js
static/src/js/ace/src-noconflict/snippets/eiffel.js
static/src/js/ace/src-noconflict/snippets/ejs.js
static/src/js/ace/src-noconflict/snippets/elixir.js
static/src/js/ace/src-noconflict/snippets/elm.js
static/src/js/ace/src-noconflict/snippets/erlang.js
static/src/js/ace/src-noconflict/snippets/flix.js
static/src/js/ace/src-noconflict/snippets/forth.js
static/src/js/ace/src-noconflict/snippets/fortran.js
static/src/js/ace/src-noconflict/snippets/fsharp.js
static/src/js/ace/src-noconflict/snippets/fsl.js
static/src/js/ace/src-noconflict/snippets/ftl.js
static/src/js/ace/src-noconflict/snippets/gcode.js
static/src/js/ace/src-noconflict/snippets/gherkin.js
static/src/js/ace/src-noconflict/snippets/gitignore.js
static/src/js/ace/src-noconflict/snippets/glsl.js
static/src/js/ace/src-noconflict/snippets/gobstones.js
static/src/js/ace/src-noconflict/snippets/golang.js
static/src/js/ace/src-noconflict/snippets/graphqlschema.js
static/src/js/ace/src-noconflict/snippets/groovy.js
static/src/js/ace/src-noconflict/snippets/haml.js
static/src/js/ace/src-noconflict/snippets/handlebars.js
static/src/js/ace/src-noconflict/snippets/haskell_cabal.js
static/src/js/ace/src-noconflict/snippets/haskell.js
static/src/js/ace/src-noconflict/snippets/haxe.js
static/src/js/ace/src-noconflict/snippets/hjson.js
static/src/js/ace/src-noconflict/snippets/html_elixir.js
static/src/js/ace/src-noconflict/snippets/html_ruby.js
static/src/js/ace/src-noconflict/snippets/html.js
static/src/js/ace/src-noconflict/snippets/ini.js
static/src/js/ace/src-noconflict/snippets/io.js
static/src/js/ace/src-noconflict/snippets/ion.js
static/src/js/ace/src-noconflict/snippets/jack.js
static/src/js/ace/src-noconflict/snippets/jade.js
static/src/js/ace/src-noconflict/snippets/java.js
static/src/js/ace/src-noconflict/snippets/javascript.js
static/src/js/ace/src-noconflict/snippets/jexl.js
static/src/js/ace/src-noconflict/snippets/json.js
static/src/js/ace/src-noconflict/snippets/json5.js
static/src/js/ace/src-noconflict/snippets/jsoniq.js
static/src/js/ace/src-noconflict/snippets/jsp.js
static/src/js/ace/src-noconflict/snippets/jssm.js
static/src/js/ace/src-noconflict/snippets/jsx.js
static/src/js/ace/src-noconflict/snippets/julia.js
static/src/js/ace/src-noconflict/snippets/kotlin.js
static/src/js/ace/src-noconflict/snippets/latex.js
static/src/js/ace/src-noconflict/snippets/latte.js
static/src/js/ace/src-noconflict/snippets/less.js
static/src/js/ace/src-noconflict/snippets/liquid.js
static/src/js/ace/src-noconflict/snippets/lisp.js
static/src/js/ace/src-noconflict/snippets/livescript.js
static/src/js/ace/src-noconflict/snippets/logiql.js
static/src/js/ace/src-noconflict/snippets/logtalk.js
static/src/js/ace/src-noconflict/snippets/lsl.js
static/src/js/ace/src-noconflict/snippets/lua.js
static/src/js/ace/src-noconflict/snippets/luapage.js
static/src/js/ace/src-noconflict/snippets/lucene.js
static/src/js/ace/src-noconflict/snippets/makefile.js
static/src/js/ace/src-noconflict/snippets/markdown.js
static/src/js/ace/src-noconflict/snippets/mask.js
static/src/js/ace/src-noconflict/snippets/matlab.js
static/src/js/ace/src-noconflict/snippets/maze.js
static/src/js/ace/src-noconflict/snippets/mediawiki.js
static/src/js/ace/src-noconflict/snippets/mel.js
static/src/js/ace/src-noconflict/snippets/mips.js
static/src/js/ace/src-noconflict/snippets/mixal.js
static/src/js/ace/src-noconflict/snippets/mushcode.js
static/src/js/ace/src-noconflict/snippets/mysql.js
static/src/js/ace/src-noconflict/snippets/nasal.js
static/src/js/ace/src-noconflict/snippets/nginx.js
static/src/js/ace/src-noconflict/snippets/nim.js
static/src/js/ace/src-noconflict/snippets/nix.js
static/src/js/ace/src-noconflict/snippets/nsis.js
static/src/js/ace/src-noconflict/snippets/nunjucks.js
static/src/js/ace/src-noconflict/snippets/objectivec.js
static/src/js/ace/src-noconflict/snippets/ocaml.js
static/src/js/ace/src-noconflict/snippets/odin.js
static/src/js/ace/src-noconflict/snippets/partiql.js
static/src/js/ace/src-noconflict/snippets/pascal.js
static/src/js/ace/src-noconflict/snippets/perl.js
static/src/js/ace/src-noconflict/snippets/pgsql.js
static/src/js/ace/src-noconflict/snippets/php_laravel_blade.js
static/src/js/ace/src-noconflict/snippets/php.js
static/src/js/ace/src-noconflict/snippets/pig.js
static/src/js/ace/src-noconflict/snippets/plain_text.js
static/src/js/ace/src-noconflict/snippets/plsql.js
static/src/js/ace/src-noconflict/snippets/powershell.js
static/src/js/ace/src-noconflict/snippets/praat.js
static/src/js/ace/src-noconflict/snippets/prisma.js
static/src/js/ace/src-noconflict/snippets/prolog.js
static/src/js/ace/src-noconflict/snippets/properties.js
static/src/js/ace/src-noconflict/snippets/protobuf.js
static/src/js/ace/src-noconflict/snippets/prql.js
static/src/js/ace/src-noconflict/snippets/puppet.js
static/src/js/ace/src-noconflict/snippets/python.js
static/src/js/ace/src-noconflict/snippets/qml.js
static/src/js/ace/src-noconflict/snippets/r.js
static/src/js/ace/src-noconflict/snippets/raku.js
static/src/js/ace/src-noconflict/snippets/razor.js
static/src/js/ace/src-noconflict/snippets/rdoc.js
static/src/js/ace/src-noconflict/snippets/red.js
static/src/js/ace/src-noconflict/snippets/redshift.js
static/src/js/ace/src-noconflict/snippets/rhtml.js
static/src/js/ace/src-noconflict/snippets/robot.js
static/src/js/ace/src-noconflict/snippets/rst.js
static/src/js/ace/src-noconflict/snippets/ruby.js
static/src/js/ace/src-noconflict/snippets/rust.js
static/src/js/ace/src-noconflict/snippets/sac.js
static/src/js/ace/src-noconflict/snippets/sass.js
static/src/js/ace/src-noconflict/snippets/scad.js
static/src/js/ace/src-noconflict/snippets/scala.js
static/src/js/ace/src-noconflict/snippets/scheme.js
static/src/js/ace/src-noconflict/snippets/scrypt.js
static/src/js/ace/src-noconflict/snippets/scss.js
static/src/js/ace/src-noconflict/snippets/sh.js
static/src/js/ace/src-noconflict/snippets/sjs.js
static/src/js/ace/src-noconflict/snippets/slim.js
static/src/js/ace/src-noconflict/snippets/smarty.js
static/src/js/ace/src-noconflict/snippets/smithy.js
static/src/js/ace/src-noconflict/snippets/snippets.js
static/src/js/ace/src-noconflict/snippets/soy_template.js
static/src/js/ace/src-noconflict/snippets/space.js
static/src/js/ace/src-noconflict/snippets/sparql.js
static/src/js/ace/src-noconflict/snippets/sql.js
static/src/js/ace/src-noconflict/snippets/sqlserver.js
static/src/js/ace/src-noconflict/snippets/stylus.js
static/src/js/ace/src-noconflict/snippets/svg.js
static/src/js/ace/src-noconflict/snippets/swift.js
static/src/js/ace/src-noconflict/snippets/tcl.js
static/src/js/ace/src-noconflict/snippets/terraform.js
static/src/js/ace/src-noconflict/snippets/tex.js
static/src/js/ace/src-noconflict/snippets/text.js
static/src/js/ace/src-noconflict/snippets/textile.js
static/src/js/ace/src-noconflict/snippets/toml.js
static/src/js/ace/src-noconflict/snippets/tsv.js
static/src/js/ace/src-noconflict/snippets/tsx.js
static/src/js/ace/src-noconflict/snippets/turtle.js
static/src/js/ace/src-noconflict/snippets/twig.js
static/src/js/ace/src-noconflict/snippets/typescript.js
static/src/js/ace/src-noconflict/snippets/vala.js
static/src/js/ace/src-noconflict/snippets/vbscript.js
static/src/js/ace/src-noconflict/snippets/velocity.js
static/src/js/ace/src-noconflict/snippets/verilog.js
static/src/js/ace/src-noconflict/snippets/vhdl.js
static/src/js/ace/src-noconflict/snippets/visualforce.js
static/src/js/ace/src-noconflict/snippets/vue.js
static/src/js/ace/src-noconflict/snippets/wollok.js
static/src/js/ace/src-noconflict/snippets/xml.js
static/src/js/ace/src-noconflict/snippets/xquery.js
static/src/js/ace/src-noconflict/snippets/yaml.js
static/src/js/ace/src-noconflict/snippets/zeek.js
static/src/js/ace/src-noconflict/snippets/zig.js
models/__pycache__/__init__.cpython-311.pyc
models/__pycache__/company.cpython-311.pyc
models/__pycache__/dss_ads.cpython-311.pyc
models/__pycache__/dss_advertisefields.cpython-311.pyc
models/__pycache__/dss_binaries.cpython-311.pyc
models/__pycache__/dss_contract_uservar.cpython-311.pyc
models/__pycache__/dss_contract.cpython-311.pyc
models/__pycache__/dss_crm_extended.cpython-311.pyc
models/__pycache__/dss_display_templates.cpython-311.pyc
models/__pycache__/dss_eventdays.cpython-311.pyc
models/__pycache__/dss_kileads_requests.cpython-311.pyc
models/__pycache__/dss_kileads.cpython-311.pyc
models/__pycache__/dss_linkmail.cpython-311.pyc
models/__pycache__/dss_m2mmail.cpython-311.pyc
models/__pycache__/dss_maintains.cpython-311.pyc
models/__pycache__/dss_marker.cpython-311.pyc
models/__pycache__/dss_onlinestate.cpython-311.pyc
models/__pycache__/dss_projects.cpython-311.pyc
models/__pycache__/dss_res_users.cpython-311.pyc
models/__pycache__/dss_settings_variables.cpython-311.pyc
models/__pycache__/dss_settings.cpython-311.pyc
models/__pycache__/dss_systemtypen.cpython-311.pyc
models/__pycache__/dss_trigger.cpython-311.pyc
models/__pycache__/dss_web_contracts_all.cpython-311.pyc
models/__pycache__/dss_web_contracts_all2.cpython-311.pyc
models/__pycache__/dss_web_contracts_entry.cpython-311.pyc
models/__pycache__/dss_web_contracts_stream.cpython-311.pyc
models/__pycache__/dss_web_contracts.cpython-311.pyc
models/__pycache__/dss.cpython-311.pyc
models/__pycache__/dsslogger.cpython-311.pyc
models/__pycache__/utils.cpython-311.pyc

View File

@ -21,6 +21,7 @@
'views/dss_display_templates.xml',
'views/dss_advertisementfields_templates.xml',
'views/dss_advertisementfields.xml',
'views/dss_advertisementfields_create_from_template_wizard.xml',
'views/dss_projects.xml',
'views/dss_import.xml',
'views/dss_ads.xml',
@ -50,6 +51,11 @@
'views/dss_binaries.xml',
'views/mainsystem_view.xml',
'views/dss_importinvoicelist.xml',
'views/dss_company_type_views.xml',
'views/dss_csvs.xml',
'views/dss_phone_center.xml',
'views/dss_phonecenter_query_views.xml',
'views/menu.xml',
'views/company_view.xml',
'views/google_map.xml',
@ -62,6 +68,7 @@
'views/dss_web_contracts_all_templates.xml',
'views/dss_contract_uservars.xml',
'views/dss_portal_add.xml',
'views/adportal_dashboard.xml',
#'views/dss_screendesign.xml',
],
'demo': [],
@ -72,6 +79,9 @@
'assets': {
'web.assets_backend': [
'DigitalSignage/static/src/img/*',
'DigitalSignage/static/src/xml/navbar_ai_button.xml',
'DigitalSignage/static/src/js/navbar_ai_button.js',
'DigitalSignage/static/src/scss/navbar_ai_button.scss',
'DigitalSignage/static/src/xml/screenDesignerView.xml',
'DigitalSignage/static/src/xml/dss_screenview_controller.xml',
'DigitalSignage/static/src/xml/dss_screenview_renderer.xml',
@ -100,9 +110,12 @@
'DigitalSignage/static/src/js/kanban_button.js',
'DigitalSignage/static/src/js/dss_crm_extended_ki_leads.js',
'DigitalSignage/static/src/js/kilead_hook.js',
'DigitalSignage/static/src/js/phone_center_action.js',
'DigitalSignage/static/src/xml/form_button.xml',
'DigitalSignage/static/src/xml/kanban_button.xml',
'DigitalSignage/static/src/xml/form_label.xml',
'DigitalSignage/static/src/xml/phone_center_action.xml',
'DigitalSignage/static/src/scss/phone_center_action.scss',
'DigitalSignage/static/src/js/test.php',
],
'web.assets_common': [
@ -117,6 +130,7 @@
],
'web.assets_qweb': [
'DigitalSignage/static/src/xml/kanban_button.xml',
'DigitalSignage/static/src/xml/phone_center_action.xml',
],
},
'license': 'LGPL-3',

View File

@ -6,3 +6,5 @@ from . import dss_screendesigner_controller
from . import dss_screenview_controller
from . import dss_nextcloudwidget
from . import dss_form_input_controller
from . import ai_agent_controller
from . import adportal

BIN
controllers/__pycache__/__init__.cpython-311.pyc Normal file → Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

1161
controllers/adportal.py Executable file

File diff suppressed because it is too large Load Diff

2328
controllers/ai_agent_controller.py Executable file

File diff suppressed because it is too large Load Diff

248
controllers/dss_form_input_controller.py Normal file → Executable file
View File

@ -61,6 +61,14 @@ class CustomerPortal(portal.CustomerPortal):
contract_name = str(tempid)+' - Vertrag ' + str(project.lastkdnr).zfill(2)
contract_id = str(project.projectid) + str(project.lastrealkdnr).zfill(2)
grundsystem_webinput_template=project.web_input_template
felder = request.env['dss.advertisefields'].search(['&','|',('contract','=',False),('isblocked','=',False),('project','=',project.id)])
comptyps = request.env['dss.company.type'].search([])
fo =""
for feld in felder:
fo = fo+"<option value=''>"+feld.feldname+"</option>"
ctn = ""
for comptyp in comptyps:
ctn = ctn+"<option value='"+comptyp.uuid+"'>"+comptyp.Firmetyp_name+"</option>"
if not grundsystem_webinput_template:
grundsystem_webinput_template = project.grundsystem_webinput_template
if not grundsystem_webinput_template:
@ -68,8 +76,27 @@ class CustomerPortal(portal.CustomerPortal):
else:
new_contract = request.env['dss.contracts'].create({'project_id':project.projectid,'project':project.id,'client_id':str(project.lastkdnr).zfill(2),'contract_auto_id':str(tempid),'contract_auto_name':str(tempname),'contract_state':39,'contract_name':contract_name,'web_contract_input_notfinished':True,'contract_creator':int(user_id.id)})
contract_data_id=new_contract.id
formdata = {
'websideusername':name,
'user_id':str(user_id.id),
'projectid':str(project.projectid),
'project':str(projekt_id),
'contract_id':str(contract_id),
'kd_id':str(str(project.lastrealkdnr).zfill(2)),
'web_contract_paysum_gfx':'249',
'contract_data_id':str(contract_data_id),
'company_type_options':str(ctn),
'client_short_company_type_options':str(ctn),
'feldoptions':fo,
'lastschrift': 'off',
'satzmail': 'off',
'rgmail': 'off',
'kamail': 'off',
'satzgiven': 'off',
'showimage' : project.grundsystem_webinput_showimage_url
}
_logger.info("inside start: "+str(name)+'--'+str(projekt_id)+'--'+str(grundsystem_webinput_template))
return request.render("website."+grundsystem_webinput_template,{'websideusername':name,'user_id':str(user_id.id),'projectid':str(project.projectid),'project':str(projekt_id),'contract_id':str(contract_id),'kd_id':str(str(project.lastrealkdnr).zfill(2)),'web_contract_paysum_gfx':'249','contract_data_id':str(contract_data_id)})
return request.render("website."+grundsystem_webinput_template,formdata)
@route(['/eingabe/continue'],type='http', auth='user', website=True, csrf=False)
def inputcontinue(self, **kwargs):
@ -81,63 +108,116 @@ class CustomerPortal(portal.CustomerPortal):
grundsystem_webinput_template=project.web_input_template
if not grundsystem_webinput_template:
grundsystem_webinput_template = project.grundsystem_webinput_template
_logger.info("inside continue: "+str(contract.contract_auto_name)+'--'+str(project.id)+'--'+str(grundsystem_webinput_template))
_logger.info("inside continue: "+str(contract.contract_auto_name)+'--'+str(project.id)+'--'+str(grundsystem_webinput_template)+'--'+str(str(contract.client_short_company_type.uuid)))
felder = request.env['dss.advertisefields'].search(['&','|',('contract','=',False),('isblocked','=',False),('project','=',project.id)])
comptyps = request.env['dss.company.type'].search([])
fo =""
sfeld =""
ctn = ""
for feld in felder:
fo = fo+"<option value=''>"+feld.feldname+"</option>"
for feld in contract.werbe_feld_selected:
sfeld = sfeld+feld.auto_feldname+", "
for comptyp in comptyps:
ctn = ctn+"<option value='"+comptyp.uuid+"' "+("selected" if comptyp.uuid == contract.client_short_company_type.uuid else "")+">"+comptyp.Firmetyp_name+"</option>"
evtlcontract_id = str(contract.project.projectid) + str(contract.project.lastrealkdnr).zfill(2)
evtlcontract_name = str(evtlcontract_id)+'_'+str(contract.client_short_company) if contract.client_short_company else str(contract.client_short_name)+' '+str(contract.client_short_vorname)
olddata = {
olddata = {
'user_id':str(user_id),
'projectid':str(project.projectid),
'project':str(project.id),
'company_type_options':str(ctn),
'contract_id':str(evtlcontract_id),
'client_short_company':str(contract.client_short_company),
'client_short_strasse':str(contract.client_short_strasse),
'client_short_plz':str(contract.client_short_plz),
'client_short_telefon':str(contract.client_short_telefon),
'client_short_email':str(contract.client_short_email),
'client_short_rgemail':str(contract.client_short_rgemail),
'client_short_ort':str(contract.client_short_ort),
'client_short_mobil':str(contract.client_short_mobil),
'client_short_website':str(contract.client_short_website),
'client_short_vorname':str(contract.client_short_vorname),
'client_short_name':str(contract.client_short_name),
'web_contract_paysum_lz':contract.web_contract_paysum_lz,
'web_contract_paysum_all':contract.web_contract_paysum_all,
'web_contract_paysum_gfx':contract.web_contract_paysum_gfx,
'client_short_company':str(contract.client_short_company) if contract.client_short_company else '',
'client_short_company_type':str(contract.client_short_company_type.uuid),
'client_short_company_type_options':str(ctn) ,
'client_short_strasse':str(contract.client_short_strasse) if contract.client_short_strasse else '',
'client_short_plz':str(contract.client_short_plz) if contract.client_short_plz else '',
'client_short_telefon':str(contract.client_short_telefon) if contract.client_short_telefon else '',
'client_short_email':str(contract.client_short_email) if contract.client_short_email else '',
'client_short_rgemail':str(contract.client_short_rgemail) if contract.client_short_rgemail else '',
'client_short_ort':str(contract.client_short_ort) if contract.client_short_ort else '',
'client_short_mobil':str(contract.client_short_mobil) if contract.client_short_mobil else '',
'client_short_website':str(contract.client_short_website) if contract.client_short_website else '',
'client_short_vorname':str(contract.client_short_vorname) if contract.client_short_vorname else '',
'client_short_name':str(contract.client_short_name) if contract.client_short_name else '',
'web_contract_paysum_lz':contract.web_contract_paysum_lz if contract.web_contract_paysum_lz else '0',
'web_contract_paysum_all':contract.web_contract_paysum_all if contract.web_contract_paysum_all else '0',
'web_contract_paysum_gfx':contract.web_contract_paysum_gfx if contract.web_contract_paysum_gfx else '0',
'web_contract_full_duration':contract.main_runtime if contract.main_runtime else '0',
'web_contract_spot_count':contract.split_runtime_count if contract.split_runtime_count else '0',
'web_contract_spot_duration':contract.split_runtime_time if contract.split_runtime_time else '0',
'web_tv_reach_PLZ':contract.tv_reach_PLZ if contract.tv_reach_PLZ else '',
'web_ads_last_date_start_planed':contract.web_contract_start_planed if contract.web_contract_start_planed else '',
'web_ads_last_date_remove_planed':contract.web_contract_remove_planed if contract.web_contract_remove_planed else '',
'web_bande_spieltage':contract.web_bande_spieltage if contract.web_bande_spieltage else '',
'contract_data_id':str(contract.id),
'runtime_m':contract.runtime_m if contract.runtime_m else '60',
'lastschrift': 'on' if contract.web_contract_is_lastschrift else 'off',
'zahlsonst': contract.web_constract_zahlstonst if contract.web_constract_zahlstonst else '',
'feldoptions':fo,
'feldselected':sfeld,'kd_id':evtlcontract_name,
'Datum': contract.web_contract_sign_datum,
'Ort': contract.web_contract_sign_ort,
'NameVorname': contract.web_contract_sign_name,
'bic': contract.client_short_BIC,
'iban': contract.client_short_IBAN,
'kapay': contract.web_contract_kapayment_info,
'alpay': contract.web_contract_alpayment_info,
'sonder_0': contract.web_contract_sonder_0,
'sonder_1': contract.web_contract_sonder_1,
'sonder_2': contract.web_contract_sonder_2,
'feldselected':sfeld if sfeld else '',
'kd_id':evtlcontract_name,
'ort': contract.web_contract_sign_ort if contract.web_contract_sign_ort else '',
'namevorname': contract.web_contract_sign_name if contract.web_contract_sign_name else '',
'bic': contract.client_short_BIC if contract.client_short_BIC else '',
'iban': contract.client_short_IBAN if contract.client_short_IBAN else '',
'kapay': contract.web_contract_kapayment_info if contract.web_contract_kapayment_info else '',
'alpay': contract.web_contract_alpayment_info if contract.web_contract_alpayment_info else '',
'sonder_0': contract.web_contract_sonder_0 if contract.web_contract_sonder_0 else '',
'sonder_1': contract.web_contract_sonder_1 if contract.web_contract_sonder_1 else '',
'sonder_2': contract.web_contract_sonder_2 if contract.web_contract_sonder_2 else '',
'satzmail': 'on' if contract.web_contract_satzmail else 'off',
'rgmail': 'on' if contract.web_contract_rgmail else 'off' ,
'kamail': 'on' if contract.web_contract_kamail else 'off',
'satzgiven': 'on' if contract.web_contract_satzgiven else 'off',
'web_ads_count_per_year2': contract.ads_count_perYear2 if contract.ads_count_perYear2 else '0',
'contract_auto_extend' : 'on' if contract.contract_auto_extend else 'off',
'contract_auto_extend_time': contract.contract_auto_extend_time if contract.contract_auto_extend_time else '0',
'web_contract_cancel_date': contract.contract_cancel_date.strftime("%d.%m.%Y") if contract.contract_cancel_date else '',
'edited': "1" # damit im Template erkannt wird, dass es sich um eine Fortsetzung handelt und nicht um einen neuen Vertrag
}
return request.render("website."+grundsystem_webinput_template, olddata)
@route(['/eingabe/upload'],type='http', auth='user', website=True, csrf=False)
def inputcontinueupload(self, **kwargs):
_logger.info("inside upload continue : " + str(request.params)+' --'+str(kwargs)+' --'+str(request.httprequest.data))
user_id = request.env.user
contract = request.env['dss.contracts'].search([('uuid','=',kwargs.get('uuid'))],limit=1)
contract_id = kwargs.get('contract_id')
contract_data_id = contract.id
kd_id = kwargs.get('kd_id')
#contract = self.setcontract(request.env['dss.contracts'].search([('id','=',contract_data_id)],limit=1),kwargs)
uploads=request.env['dss.mediatypes'].search([('weboptionupload_show','=',True)])
return request.render("website.eingabe-phase3-upload",{'user_id':str(user_id),'contract_id':str(contract_id),'kd_id':str(kd_id),'contract_data_id':str(contract_data_id),'uploadfiles':uploads})
@route(['/eingabe/finish'],type='http', auth='user', website=True, csrf=False)
def finish_contract(self, **kwargs):
_logger.info("inside: start changeScreenTemplateView" + str(request.params)+' --'+str(kwargs)+' --'+str(request.httprequest.data))
uuid = kwargs.get('uuid')
contract = request.env['dss.contracts'].search([('uuid','=',uuid)],limit=1)
_logger.info("inside finish_contract: "+str(contract.id)+'--'+str(contract.contract_auto_id)+'--'+str(uuid))
_logger.info("inside finish_contract: "+str(contract.id)+'--'+str(contract.contract_auto_id)+'--'+str(uuid))
if not contract.web_contract_input_finished:
if contract.project.lostkdnr:
if len(contract.project.lostkdnr)>0:
nextnr = contract.project.lostkdnr.split(',')[0]
contract.project.lostkdnr = ','.join(contract.project.lostkdnr.split(',')[1:])
else:
contract.project.lastrealkdnr += 1
nextnr = str(contract.project.lastrealkdnr).zfill(2)
else:
contract.project.lastrealkdnr += 1
nextnr = str(contract.project.lastrealkdnr).zfill(2)
contract_id = str(contract.project.projectid) + str(nextnr)
contract.contract_id = str(nextnr)
contract.client_id = str(nextnr)
contract.contract_auto_id = contract_id
contract.contract_name = str(contract.client_short_company) if contract.client_short_company else str(contract.client_short_name)+' '+str(contract.client_short_vorname)
contract.contract_auto_name=str(contract_id)+'_'+str(contract.client_short_company) if contract.client_short_company else str(contract.client_short_name)+' '+str(contract.client_short_vorname)
dsscontracts.tokampagne(contract)
contract.web_contract_input_finished = True
contract.web_contract_rejected = False
contract.web_contract_input_notfinished=False
contract_id = str(contract.project.projectid) + str(contract.project.lastrealkdnr).zfill(2)
contract.contract_auto_id = contract_id
contract.contract_name = str(contract_id)+' - '+str(contract.client_short_company) if contract.client_short_company else str(contract.client_short_name)+' '+str(contract.client_short_vorname)
contract.contract_auto_name=str(contract_id)+'_'+str(contract.client_short_company) if contract.client_short_company else str(contract.client_short_name)+' '+str(contract.client_short_vorname)
dsscontracts.tokampagne(contract)
return request.render("website.eingabe-finished",{'contract':contract,'contract_auto_id':contract.contract_auto_id})
@route(['/eingabe/cancel'],type='http', auth='user', website=True, csrf=False)
@ -151,7 +231,8 @@ class CustomerPortal(portal.CustomerPortal):
def setcontract(self,contract,kwargs):
projekt_id = kwargs.get('project')
contract.client_short_company = kwargs.get('client_short_company') if kwargs.get('client_short_company') else ''
contract.client_short_company = kwargs.get('client_short_company') if kwargs.get('client_short_company') else ''
contract.client_short_company_type = request.env['dss.company.type'].search([('uuid','=',kwargs.get('client_short_company_type'))],limit=1) if kwargs.get('client_short_company_type') else ''
contract.client_short_vorname = kwargs.get('client_short_vorname') if kwargs.get('client_short_vorname') else ''
contract.client_short_name = kwargs.get('client_short_name') if kwargs.get('client_short_name') else ''
contract.client_short_telefon = kwargs.get('client_short_telefon') if kwargs.get('client_short_telefon') else ''
@ -162,24 +243,62 @@ class CustomerPortal(portal.CustomerPortal):
contract.client_short_ort = kwargs.get('client_short_ort') if kwargs.get('client_short_ort') else ''
contract.client_short_mobil = kwargs.get('client_short_mobil') if kwargs.get('client_short_mobil') else ''
contract.client_short_website = kwargs.get('client_short_website') if kwargs.get('client_short_website') else ''
contract.runtimesystem = "M"
contract.runtime_m = kwargs.get('contract_time') if kwargs.get('contract_time') else '64'
contract.runtimesystem = "M"
try:
laufzeit = int(kwargs.get('contract_time'))
except:
laufzeit = 60
contract.runtime_m = laufzeit if laufzeit > 0 else kwargs.get('runtime_m') if laufzeit == -1 else 60
contract.web_contract_paysum_lz = kwargs.get('web_contract_paysum_lz') if kwargs.get('web_contract_paysum_lz') else 0.0
contract.web_contract_paysum_gfx = kwargs.get('web_contract_paysum_gfx') if kwargs.get('web_contract_paysum_gfx') else 0.0
contract.web_contract_paysum_all = kwargs.get('web_contract_paysum_all') if kwargs.get('web_contract_paysum_all') else 0.0
contract.web_contract_sign_datum = kwargs.get('Datum') if kwargs.get('Datum') else ''
contract.web_contract_sign_ort = kwargs.get('Ort') if kwargs.get('Ort') else ''
contract.contract_date = datetime.datetime.strptime(contract.web_contract_sign_datum, "%d.%m.%Y").date()
contract.web_contract_sign_name = kwargs.get('NameVorname') if kwargs.get('NameVorname') else ''
contract.client_short_BIC = kwargs.get('bic') if kwargs.get('bic') else ''
contract.client_short_IBAN = kwargs.get('iban') if kwargs.get('iban') else ''
contract.web_contract_is_lastschrift = kwargs.get('lastschrift') == 'on' if kwargs.get('lastschrift') else False
contract.web_contract_is_lastschrift = True if kwargs.get('lastschrift') else False
contract.web_constract_zahlstonst = kwargs.get('zahlsonst') if kwargs.get('zahlsonst') else ''
contract.work_state_info = kwargs.get('sonder_2') if kwargs.get('sonder_2') else ''
contract.web_contract_kapayment_info = kwargs.get('kapay') if kwargs.get('kapay') else ''
contract.web_contract_alpayment_info = kwargs.get('alpay') if kwargs.get('alpay') else ''
contract.web_contract_satzmail = True if kwargs.get('satzmail') else False
contract.web_contract_satzgiven = True if kwargs.get('satzgiven') else False
contract.web_contract_rgmail = True if kwargs.get('rgmail') else False
contract.web_contract_kamail = True if kwargs.get('kamail') else False
contract.web_contract_sonder_0 = kwargs.get('sonder_0') if kwargs.get('sonder_0') else ''
contract.web_contract_sonder_1 = kwargs.get('sonder_1') if kwargs.get('sonder_1') else ''
contract.web_contract_sonder_2 = kwargs.get('sonder_2') if kwargs.get('sonder_2') else ''
contract.main_runtime = kwargs.get('web_contract_full_duration') if kwargs.get('web_contract_full_duration') else '0'
contract.split_runtime_count = kwargs.get('web_contract_spot_count') if kwargs.get('web_contract_spot_count') else '0'
contract.split_runtime_time = kwargs.get('web_contract_spot_duration') if kwargs.get('web_contract_spot_duration') else '0'
contract.ads_count_perYear2 = kwargs.get('web_ads_count_per_year2') if kwargs.get('web_ads_count_per_year2') else '0'
contract.tv_reach_PLZ = kwargs.get('web_tv_reach_PLZ') if kwargs.get('web_tv_reach_PLZ') else ''
contract.web_contract_start_planed = kwargs.get('web_ads_last_date_start_planed') if kwargs.get('web_ads_last_date_start_planed') else ''
contract.web_contract_remove_planed = kwargs.get('web_ads_last_date_remove_planed') if kwargs.get('web_ads_last_date_remove_planed') else ''
contract.web_bande_spieltage = kwargs.get('web_bande_spieltage') if kwargs.get('web_bande_spieltage') else ''
contract.contract_auto_extend = True if kwargs.get('contract_auto_extend') else False
contract.contract_auto_extend_time = kwargs.get('contract_auto_extend_time') if kwargs.get('contract_auto_extend_time') else '0'
cancel_date = kwargs.get('web_contract_cancel_date') if kwargs.get('web_contract_cancel_date') else ''
if cancel_date:
try:
datetime.datetime.strptime(cancel_date, "%Y-%m-%d")
contract.contract_cancel_date = cancel_date
except ValueError:
try:
converted_date = datetime.datetime.strptime(cancel_date, "%d.%m.%Y").strftime("%Y-%m-%d")
contract.contract_cancel_date = converted_date
except ValueError:
contract.contract_cancel_date = ''
else:
contract.contract_cancel_date = ''
try:
contract.contract_date = datetime.datetime.strptime(contract.web_contract_sign_datum, "%d.%m.%Y").date()
except:
# ignore error and do not set date if format is wrong
pass
gesamts = kwargs.get('sonder_0') if kwargs.get('sonder_0') else ''
gesamts = gesamts+' //// '+kwargs.get('sonder_1') if kwargs.get('sonder_1') else ''
contract.remark = gesamts
@ -189,11 +308,15 @@ class CustomerPortal(portal.CustomerPortal):
for wfa in wfarray:
if wfa:
if '_' in wfa:
wfa = wfa.split('_')[1]
wfa = wfa.split('_')[1]
wfaid = request.env['dss.advertisefields'].search([('feldname','=',wfa.strip()),('project','=',contract.project.id)],limit=1)
_logger.info("inside update : "+str(projekt_id)+'/'+str(contract.project.id)+'--'+str(wfields)+'--'+str(wfa.strip()+'--'+str(wfaid)))
if wfaid:
contract.werbe_feld_selected = [(4, wfaid.id)]
else:
wfaid = request.env['dss.advertisefields'].create({'feldname':wfa.strip(),'project':contract.project.id,'auto_feldname':str(str(contract.project.projectid)+'_'+wfa.strip())})
if wfaid:
contract.werbe_feld_selected = [(4, wfaid.id)]
return contract
@route(['/eingabe/submit_lcdtouch'],type='http', auth='user', website=True, csrf=False)
@ -206,11 +329,34 @@ class CustomerPortal(portal.CustomerPortal):
contract_id = kwargs.get('contract_id')
kd_id = kwargs.get('kd_id')
contract_data_id = kwargs.get('contract_data_id')
_logger.info("inside: "+str(name)+'--'+str(projekt_id)+'--'+str(kd_id)+'--'+str(contract_id)+'--'+str(contract_data_id)+'--'+str(projectid)+'--'+str(kwargs.get('kamail')))
_logger.info("inside: "+str(name)+'--'+str(projekt_id)+'--'+str(kd_id)+'--'+str(contract_id)+'--'+str(contract_data_id)+'--'+str(projectid)+'--'+str(kwargs.get('kamail')))
contract = self.setcontract(request.env['dss.contracts'].search([('id','=',contract_data_id)],limit=1),kwargs)
return request.render("website.eingabe-phase3-upload",{'websideusername':name,'user_id':str(user_id),'projectid':str(projectid),'project':str(projekt_id),'contract_id':str(contract_id),'kd_id':str(kd_id),'contract_data_id':str(contract_data_id)})
uploads=request.env['dss.mediatypes'].search([('weboptionupload_show','=',True)])
return request.render("website.eingabe-phase3-upload",{'websideusername':name,'user_id':str(user_id),'projectid':str(projectid),'project':str(projekt_id),'contract_id':str(contract_id),'kd_id':str(kd_id),'contract_data_id':str(contract_data_id),'uploadfiles':uploads})
@route(['/eingabe/submit_kfz'],type='http', auth='user', website=True, csrf=False)
@route(['/eingabe/submit_save'],type='json', auth='user', methods=['POST'], csrf=True)
def submit_save(self, **kwargs):
_logger.info("inside: Save Received Data " + str(request.params)+' --'+str(kwargs)+' --'+str(request.httprequest.data))
kwargs = json.loads(request.httprequest.data.decode('utf-8'))
_logger.info("inside: Save Received Data " + str(request.params)+' --'+str(kwargs)+' --'+str(request.httprequest.data))
name = kwargs.get('websideusername')
projekt_id = kwargs.get('project')
projectid = kwargs.get('projectid')
user_id = kwargs.get('user_id')
contract_id = kwargs.get('contract_id')
contract_data_id = kwargs.get('contract_data_id')
kd_id = kwargs.get('kd_id')
contract = self.setcontract(request.env['dss.contracts'].search([('id','=',contract_data_id)],limit=1),kwargs)
_logger.info("inside: Save Received Data "+str(name)+'--'+str(projekt_id)+'--'+str(kd_id)+'--'+str(contract_id)+'--'+str(contract_data_id))
output = {
'results':{
'code':200,
'message':'OK'
}
}
return json.dumps(output)
@route(['/eingabe/submit_all'],type='http', auth='user', website=True, csrf=False)
def submit_kfz(self, **kwargs):
_logger.info("inside: start changeScreenTemplateView" + str(request.params)+' --'+str(kwargs)+' --'+str(request.httprequest.data))
name = kwargs.get('websideusername')
@ -222,7 +368,8 @@ class CustomerPortal(portal.CustomerPortal):
kd_id = kwargs.get('kd_id')
contract = self.setcontract(request.env['dss.contracts'].search([('id','=',contract_data_id)],limit=1),kwargs)
_logger.info("inside: "+str(name)+'--'+str(projekt_id)+'--'+str(kd_id)+'--'+str(contract_id)+'--'+str(contract_data_id))
return request.render("website.eingabe-phase3-upload",{'websideusername':name,'user_id':str(user_id),'projectid':str(projectid),'project':str(projekt_id),'contract_id':str(contract_id),'kd_id':str(kd_id),'contract_data_id':str(contract_data_id)})
uploads=request.env['dss.mediatypes'].search([('weboptionupload_show','=',True)])
return request.render("website.eingabe-phase3-upload",{'websideusername':name,'user_id':str(user_id),'projectid':str(projectid),'project':str(projekt_id),'contract_id':str(contract_id),'kd_id':str(kd_id),'contract_data_id':str(contract_data_id),'uploadfiles':uploads})
@route(['/eingabe/submit_stream'],type='http', auth='user', website=True, csrf=False)
def submit_stream(self, **kwargs):
@ -233,6 +380,8 @@ class CustomerPortal(portal.CustomerPortal):
user_id = kwargs.get('user_id')
contract_id = kwargs.get('contract_id')
kd_id = kwargs.get('kd_id')
a=2
contract_data_id = kwargs.get('contract_data_id')
contract = self.setcontract(request.env['dss.contracts'].search([('id','=',contract_data_id)],limit=1),kwargs)
_logger.info("inside: "+str(name)+'--'+str(projekt_id)+'--'+str(kd_id)+'--'+str(contract_id)+'--'+str(contract_data_id))
@ -247,8 +396,15 @@ class CustomerPortal(portal.CustomerPortal):
user_id = kwargs.get('user_id')
contract_id = kwargs.get('contract_id')
kd_id = kwargs.get('kd_id')
filetype = kwargs.get('filetype')
file = kwargs.get('filename')
b64file = base64.b64encode(file.read())
contract_data_id = kwargs.get('contract_data_id')
_logger.info("inside: "+str(name)+'--'+str(projekt_id)+'--'+str(kd_id)+'--'+str(contract_id)+'--'+str(contract_data_id))
contract=request.env['dss.contracts'].search([('id','=',contract_data_id)],limit=1)
if filetype=="1":
_logger.info("inside: set Contract "+str(b64file))
contract.scan_vertrag=b64file
_logger.info("inside: "+str(name)+'--'+str(projekt_id)+'--'+str(kd_id)+'--'+str(contract_id)+'--'+str(contract_data_id)+'--'+str(file)+'---'+str(b64file))
return request.render("website.eingabe-phase4-finish",{'websideusername':name,'user_id':str(user_id),'projectid':str(projectid),'project':str(projekt_id),'contract_id':str(contract_id),'kd_id':str(kd_id),'contract_data_id':str(contract_data_id)})
@route(['/eingabe/submit_finish'],type='http', auth='user', website=True, csrf=False)
@ -264,7 +420,7 @@ class CustomerPortal(portal.CustomerPortal):
submit_finish = kwargs.get('submit')
_logger.info("inside finish : "+str(name)+'--'+str(projekt_id)+'--'+str(kd_id)+'--'+str(contract_id)+'--'+str(contract_data_id)+'--'+str(submit_finish))
if submit_finish == 'Eingabe beenden':
return request.render("website.eingabe-phase1")
return self.ad_portal_my_documents(self, **kwargs)
if submit_finish == 'Neuen Vertrag im gleichen Projekt':
return self.changeScreenView(**kwargs)
if submit_finish == 'Neuen Vertrag in anderen Projekt':

0
controllers/dss_webhook_controller.py Normal file → Executable file
View File

View File

@ -5,6 +5,7 @@ from . import dss_settings
from . import dss_trigger
from . import dss_ads
from . import dss_contract
from . import dss_contracts_move_field_wizard
from . import dss_contract_uservar
from . import dss_projects
from . import dss_geraetetypen
@ -15,6 +16,9 @@ from . import dss_eventdays
from . import dss_display_templates
from . import dss_advertisefields_templates
from . import dss_advertisefields
from . import dss_advertisefields_create_from_template_wizard
from . import dss_advertisefields_create_from_project_wizard
from . import dss_advertisefields_save_as_template_wizard
from . import dss_trigger
from . import dss_triggerexecute
from . import dss_triggervalues
@ -23,6 +27,7 @@ from . import dss
from . import dss_maintains
from . import dss_provisionstypen
from . import dss_provision
from . import dss_company_type
from . import company
from . import dss_invoices
from . import dss_importinvoicelist
@ -35,6 +40,10 @@ from . import dss_crm_extended
from . import dss_kileads_requests
from . import dss_kileads
from . import dss_res_users
from . import dss_portal_user_profile
from . import dss_web_contracts_stream
from . import dss_settings_variables
from . import dss_csvs
from . import dss_ai_chat_history
from . import dss_phonecenter_query

BIN
models/__pycache__/__init__.cpython-311.pyc Normal file → Executable file

Binary file not shown.

BIN
models/__pycache__/company.cpython-311.pyc Normal file → Executable file

Binary file not shown.

BIN
models/__pycache__/dss.cpython-311.pyc Normal file → Executable file

Binary file not shown.

BIN
models/__pycache__/dss_ads.cpython-311.pyc Normal file → Executable file

Binary file not shown.

BIN
models/__pycache__/dss_advertisefields.cpython-311.pyc Normal file → Executable file

Binary file not shown.

BIN
models/__pycache__/dss_binaries.cpython-311.pyc Normal file → Executable file

Binary file not shown.

BIN
models/__pycache__/dss_contract.cpython-311.pyc Normal file → Executable file

Binary file not shown.

BIN
models/__pycache__/dss_contract_uservar.cpython-311.pyc Normal file → Executable file

Binary file not shown.

0
models/__pycache__/dss_crm_extended.cpython-311.pyc Normal file → Executable file
View File

0
models/__pycache__/dss_eventdays.cpython-311.pyc Normal file → Executable file
View File

0
models/__pycache__/dss_kileads.cpython-311.pyc Normal file → Executable file
View File

View File

0
models/__pycache__/dss_linkmail.cpython-311.pyc Normal file → Executable file
View File

0
models/__pycache__/dss_m2mmail.cpython-311.pyc Normal file → Executable file
View File

BIN
models/__pycache__/dss_maintains.cpython-311.pyc Normal file → Executable file

Binary file not shown.

0
models/__pycache__/dss_marker.cpython-311.pyc Normal file → Executable file
View File

0
models/__pycache__/dss_onlinestate.cpython-311.pyc Normal file → Executable file
View File

BIN
models/__pycache__/dss_projects.cpython-311.pyc Normal file → Executable file

Binary file not shown.

BIN
models/__pycache__/dss_res_users.cpython-311.pyc Normal file → Executable file

Binary file not shown.

BIN
models/__pycache__/dss_settings.cpython-311.pyc Normal file → Executable file

Binary file not shown.

View File

BIN
models/__pycache__/dss_systemtypen.cpython-311.pyc Normal file → Executable file

Binary file not shown.

BIN
models/__pycache__/dss_trigger.cpython-311.pyc Normal file → Executable file

Binary file not shown.

0
models/__pycache__/dss_web_contracts.cpython-311.pyc Normal file → Executable file
View File

View File

View File

View File

View File

0
models/__pycache__/dsslogger.cpython-311.pyc Normal file → Executable file
View File

0
models/__pycache__/utils.cpython-311.pyc Normal file → Executable file
View File

View File

@ -28,10 +28,17 @@ class ResCompany(models.Model):
dssinternpartner_grafik = fields.Boolean('Grafiker', default=False)
dssinternpartner_technik = fields.Boolean('Techniker', default=False)
dssportaluser = fields.Many2one('res.users', 'Portal User', domain=[('share','=',True)], help='Verknüpfter Portal Benutzer fuer diesen Partner')
dsspartner_Company_type = fields.Many2one('dss.company.type',string='Firmentyp Kunde',tracking=True)
# dss_uuid = fields.Char('uuid')
dss_uuid = fields.Char(default=lambda self: self._default_uuid(), required=True, readonly=True, copy=False, string='UUID')
rgemail = fields.Char('Rechnungs-Email', default=False)
leitwegid = fields.Char('Leitweg-ID', default=False)
google_calendar_id = fields.Char('Google Kalender ID', default=False)
google_calendar_client_id = fields.Char('Google Client ID', default=False)
google_calendar_client_secret = fields.Char('Google Client Secret', default=False)
google_calendar_refresh_token = fields.Char('Google Refresh Token', default=False)
google_calendar_access_token = fields.Char('Google Access Token', default=False)
google_calendar_token_expires_at = fields.Datetime('Google Token gueltig bis', default=False)
@api.model
def _default_uuid(self):

View File

@ -25,6 +25,7 @@ from pyffmpeg import FFmpeg
from tuya_iot import TuyaOpenAPI, TUYA_LOGGER
from tuya_connector import TuyaOpenAPI, TUYA_LOGGER
from . import dss_contract
from . import dss_projects
import sys
@ -99,7 +100,28 @@ class dsscontractstatus(models.Model):
systemtypdefault = fields.Boolean(string='Default Typ',tracking=True)
systemtyp = fields.Many2one('dss.systemtypen',tracking=True)
systemtypuuid = fields.Char(related='systemtyp.uuid',tracking=True)
kanban_display_minimal = fields.Boolean(string='Kanban minimalisiert')
kanban_display_minimal = fields.Boolean(string='Kanban minimieren')
kanban_collapsed_choice = fields.Selection(
[('no', 'Nein'), ('yes', 'Ja')],
string='Kanban eingeklappt',
default='no',
store=True,
)
fold = fields.Boolean(
string='Kanban-Spalte eingeklappt',
compute='_compute_fold',
inverse='_inverse_fold',
store=True,
)
@api.depends('kanban_collapsed_choice')
def _compute_fold(self):
for record in self:
record.fold = record.kanban_collapsed_choice == 'yes'
def _inverse_fold(self):
for record in self:
record.kanban_collapsed_choice = 'yes' if record.fold else 'no'
@api.model
def _default_uuid(self):
@ -196,7 +218,12 @@ class dssmediatypes(models.Model):
maxsize_kb = fields.Integer('Maximale Größe KB')
maxsize_w = fields.Integer('Maximale Pixel W')
maxsize_h = fields.Integer('Maximale Pixel H')
adportal_visible = fields.Boolean('Ad Portal sichtbar', default=False)
adportal_ki_prompt = fields.Text('KI Prompt')
standard_image = fields.Image()
weboptionupload_show = fields.Boolean('Im Uploadformular Anzeigen ?')
weboptionupload_name = fields.Char('Anzeigename für Webformular')
weboptionupload_id = fields.Integer('UploadID für Webformular')
default_filename = fields.Char('Default Dateiname', help='Standard-Dateiname für diesen Mediatyp. Wird automatisch aus dem Dateinamen der hochgeladenen Datei abgerufen')
can_gen_stamp = fields.Boolean('Kann Stempel generieren', help='Darf durch Stempelgenerator gefüllt werden ja/nein')
@ -360,6 +387,14 @@ class NewBinaryWizard(models.TransientModel):
dss_contract.dsscontracts.pyopen_dss_contract_add_binary_form_view_from_wizzard(contract,self.field_ids)
return context
def pyopen_dss_project_add_binary_form_view_from_wizzard(self):
context = dict(self.env.context or {})
default_contract_id = context.get('active_id', False)
_logger.info('Open Add Binary Form '+str(context)+' - '+str(default_contract_id)+' / '+str(self.field_ids))
project= self.env['dss.projects'].browse(default_contract_id)
dss_projects.dssprojects.pyopen_dss_project_add_binary_form_view_from_wizzard(project,self.field_ids)
return context
class View(models.Model):
"""
Extends the base 'ir.ui.view' model to include a new type of view

View File

@ -144,6 +144,7 @@ class dsscontractads(models.Model):
date_zuarbeit = fields.Date(string='Zuarbeit Datum', help='Zuarbeit gesendet am',
tracking=True)
korrekturabzug_cloudlink = fields.Char('Freigabelink zu den fertigen Dateien ',tracking=True)
date_korrekturabzug = fields.Date(string='K.Abzug Datum', help='Korrekturabzug gesendet am',
tracking=True)
date_korrekturfreigabe = fields.Date(string='K.Freigabe Datum', help='Korrekturfreigabe erhalten am',
@ -246,6 +247,24 @@ class dsscontractads(models.Model):
autodays=self.env['dss.settings'].search([],limit=1).freigabe_auto_time
record.date_korrekturfreigabe_ablauf = record.date_korrekturabzug + relativedelta(days=autodays)
def pysendKA(self):
contract = self.contract
grundsystemname = contract.project.grundsystemname
_logger.info('Korrekturabzug senden : C_' + str(contract.id) + 'GS_' + str(grundsystemname.id)+' / '+str(grundsystemname.email_template_abzug))
if grundsystemname.email_template_abzug:
_logger.info('Korrekturabzug senden in : C_' + str(contract.id) + 'GS_' + str(grundsystemname.id)+' / '+str(grundsystemname.email_template_abzug))
newmail = self.env['mail.template'].browse(grundsystemname.email_template_abzug.id).copy()
newmail.email_to = contract.client_short_email
newmail.email_bcc = "projekt@logumedia.de"
newmail.send_mail(contract.id, force_send = True)
self.date_korrekturabzug = date.today()
_logger.info('Korrekturabzug gesendet : C_' + str(contract.id) + 'GS_' + str(grundsystemname.id)+' / '+str(grundsystemname.email_template_abzug))
else:
_logger.info('Keine Korrekturabzug Mailvorlage hinterlegt : C_' + str(contract.id) + 'GS_' + str(grundsystemname.id)+' / '+str(grundsystemname.email_template_abzug))
raise ValidationError(_("Keine Korrekturabzug Mailvorlage hinterlegt ! Bitte im Systemtyp eine Mailvorlage für Korrekturabzug hinterlegen !"))
def pyaction_view_ad_details(self):
action = self.env['ir.actions.act_window'].with_context({'default_adid': self.id})._for_xml_id(
'DigitalSignage.action_dss_ads_view')

View File

@ -183,6 +183,13 @@ class dssadvertisefields(models.Model):
if not record.contract:
record.contract = record.contract_save
def action_remove_contract_links(self):
for record in self:
record.write({
'contract': [(5, 0, 0)],
'contract_save': False,
})
def pyactiontaketemplate(self):
action = (self.env["confirmation.wizard"].confirm_message(_("Das Template wirklich übernehmen ?"),title="Bitte bestätigen",method="taketemplate",records=self,callback_params={"template":self.id})
)
@ -236,6 +243,45 @@ class dssadvertisefields(models.Model):
action['context']='{"active_id":'+str(self.id)+'}'
action['res_id']=self.id
return action
@api.model
def action_open_create_from_template_wizard(self):
view = self.env.ref('DigitalSignage.dss_advertisefields_create_from_template_wizard_view_form')
return {
'type': 'ir.actions.act_window',
'name': _('Felder aus Vorlage erstellen'),
'res_model': 'dss.advertisefields.create.from.template.wizard',
'view_mode': 'form',
'view_id': view.id,
'views': [(view.id, 'form')],
'target': 'new',
}
@api.model
def action_open_create_from_project_wizard(self):
view = self.env.ref('DigitalSignage.dss_advertisefields_create_from_project_wizard_view_form')
return {
'type': 'ir.actions.act_window',
'name': _('Felder aus vorh. Projekt erstellen'),
'res_model': 'dss.advertisefields.create.from.project.wizard',
'view_mode': 'form',
'view_id': view.id,
'views': [(view.id, 'form')],
'target': 'new',
}
@api.model
def action_open_save_as_template_wizard(self):
view = self.env.ref('DigitalSignage.dss_advertisefields_save_as_template_wizard_view_form')
return {
'type': 'ir.actions.act_window',
'name': _('Felder in Vorlage speichern'),
'res_model': 'dss.advertisefields.save.as.template.wizard',
'view_mode': 'form',
'view_id': view.id,
'views': [(view.id, 'form')],
'target': 'new',
}
def btn_image_import(self):
action = self.env["confirmation.wizard"].confirm_message(_("Das Bild wirklich importieren ?"),title="Bitte bestätigen",method="btn_image_import_do",records=self,callback_params={"template":self.id})

View File

@ -0,0 +1,52 @@
from odoo import fields, models, _
from odoo.exceptions import ValidationError
class DssAdvertisefieldsCreateFromProjectWizard(models.TransientModel):
_name = "dss.advertisefields.create.from.project.wizard"
_description = "Werbefelder aus vorhandenem Projekt erstellen"
source_project_id = fields.Many2one(
'dss.projects',
string='Altes Projekt',
required=True,
)
project_id = fields.Many2one(
'dss.projects',
string='Neues Projekt',
required=True,
)
def action_create_fields(self):
self.ensure_one()
if self.source_project_id.id == self.project_id.id:
raise ValidationError(_("Altes Projekt und neues Projekt muessen unterschiedlich sein."))
source_fields = self.env['dss.advertisefields'].search([
('project', '=', self.source_project_id.id)
])
if not source_fields:
raise ValidationError(_("Im gewaehlten alten Projekt wurden keine Werbefelder gefunden."))
create_vals = []
project_identifier = self.project_id.projectid or self.project_id.id
for source_field in source_fields:
vals = source_field.copy_data()[0]
feldname = vals.get('feldname') or source_field.feldname
vals.update({
'project': self.project_id.id,
'auto_feldname': f"{project_identifier}_{feldname}",
'contract': [(6, 0, [])],
'contract_save': False,
})
create_vals.append(vals)
created_fields = self.env['dss.advertisefields'].create(create_vals)
return {
'type': 'ir.actions.act_window',
'name': _('Erstellte Werbefelder'),
'res_model': 'dss.advertisefields',
'view_mode': 'tree,form',
'domain': [('id', 'in', created_fields.ids)],
}

View File

@ -0,0 +1,94 @@
from odoo import fields, models, _
from odoo.exceptions import ValidationError
class DssAdvertisefieldsCreateFromTemplateWizard(models.TransientModel):
_name = "dss.advertisefields.create.from.template.wizard"
_description = "Werbefelder aus Vorlage erstellen"
display_template_id = fields.Many2one(
'dss.display.templates',
string='Vorlage',
required=True,
)
project_id = fields.Many2one(
'dss.projects',
string='Neues Projekt',
required=True,
)
def action_create_fields(self):
self.ensure_one()
template_fields = self.env['dss.advertisefields.templates'].search([
('displaytemplate', '=', self.display_template_id.id)
])
if not template_fields:
raise ValidationError(_("Für die gewählte Vorlage wurden keine Werbefelder gefunden."))
create_vals = []
project_identifier = self.project_id.projectid or self.project_id.id
for template_field in template_fields:
feldname = template_field.feldname or template_field.templatefeldname
create_vals.append({
'feldname': feldname,
'auto_feldname': f"{project_identifier}_{feldname}",
'project': self.project_id.id,
'display': template_field.display,
'color_used': template_field.color_used,
'color_unused': template_field.color_unused,
'mediastructure': template_field.mediastructure.id,
'is_btn': template_field.is_btn,
'btn_displaytemplate': self.display_template_id.id,
'btn_fromtemplate': template_field.id,
'btn_pos_x': template_field.btn_pos_x,
'btn_pos_y': template_field.btn_pos_y,
'btn_pos_w': template_field.btn_pos_w,
'btn_pos_h': template_field.btn_pos_h,
'btn_name': template_field.btn_name,
'btn_image': template_field.btn_image,
'btn_fieldname': template_field.btn_fieldname,
'btn_visible': template_field.btn_visible,
'btn_active': template_field.btn_active,
'btn_action_inactive': template_field.btn_action_inactive,
'btn_action_active': template_field.btn_action_active,
'btn_playlist_zuordnung': template_field.btn_playlist_zuordnung,
'btn_gallery_rows': template_field.btn_gallery_rows,
'btn_gallery_cols': template_field.btn_gallery_cols,
'btn_gallery_prv_w': template_field.btn_gallery_prv_w,
'btn_gallery_prv_h': template_field.btn_gallery_prv_h,
'btn_has_timer': template_field.btn_has_timer,
'btn_has_timer_time': template_field.btn_has_timer_time,
'btn_editable': template_field.btn_editable,
'btn_special_actionimage': template_field.btn_special_actionimage,
'btn_special_actionimage_x': template_field.btn_special_actionimage_x,
'btn_special_actionimage_y': template_field.btn_special_actionimage_y,
'btn_special_actionimage_w': template_field.btn_special_actionimage_w,
'btn_special_actionimage_h': template_field.btn_special_actionimage_h,
'btn_special_actionimage_time': template_field.btn_special_actionimage_time,
'btn_text_1_visible': template_field.btn_text_1_visible,
'btn_text_2_visible': template_field.btn_text_2_visible,
'btn_text_3_visible': template_field.btn_text_3_visible,
'btn_text_4_visible': template_field.btn_text_4_visible,
'btn_text_1_pos_x': template_field.btn_text_1_pos_x,
'btn_text_1_pos_y': template_field.btn_text_1_pos_y,
'btn_text_2_pos_x': template_field.btn_text_2_pos_x,
'btn_text_2_pos_y': template_field.btn_text_2_pos_y,
'btn_text_3_pos_x': template_field.btn_text_3_pos_x,
'btn_text_3_pos_y': template_field.btn_text_3_pos_y,
'btn_text_4_pos_x': template_field.btn_text_4_pos_x,
'btn_text_4_pos_y': template_field.btn_text_4_pos_y,
'btn_text_1_font': template_field.btn_text_1_font,
'btn_text_2_font': template_field.btn_text_2_font,
'btn_text_3_font': template_field.btn_text_3_font,
'btn_text_4_font': template_field.btn_text_4_font,
})
created_fields = self.env['dss.advertisefields'].create(create_vals)
return {
'type': 'ir.actions.act_window',
'name': _('Erstellte Werbefelder'),
'res_model': 'dss.advertisefields',
'view_mode': 'tree,form',
'domain': [('id', 'in', created_fields.ids)],
}

View File

@ -0,0 +1,113 @@
from odoo import fields, models, _
from odoo.exceptions import ValidationError
class DssAdvertisefieldsSaveAsTemplateWizard(models.TransientModel):
_name = "dss.advertisefields.save.as.template.wizard"
_description = "Werbefelder als Vorlage speichern"
source_project_id = fields.Many2one(
'dss.projects',
string='Altes Projekt',
required=True,
)
new_template_name = fields.Char(
string='Neuer Vorlagenname',
required=True,
)
def action_save_as_template(self):
self.ensure_one()
source_fields = self.env['dss.advertisefields'].search([
('project', '=', self.source_project_id.id)
])
if not source_fields:
raise ValidationError(_("Im gewaehlten alten Projekt wurden keine Werbefelder gefunden."))
template_name = self.new_template_name.strip()
if not template_name:
raise ValidationError(_("Bitte einen gueltigen Vorlagennamen eingeben."))
first_display_template = source_fields[0].btn_displaytemplate
display_template_vals = {
'displayname': template_name,
}
if first_display_template:
display_template_vals.update({
'fullsize_w': first_display_template.fullsize_w,
'fullsize_h': first_display_template.fullsize_h,
'adportal_visible': first_display_template.adportal_visible,
})
new_display_template = self.env['dss.display.templates'].create(display_template_vals)
template_field_vals = []
for source_field in source_fields:
template_field_name = source_field.feldname or source_field.auto_feldname or ''
if not source_field.feldname and source_field.auto_feldname and '_' in source_field.auto_feldname:
template_field_name = source_field.auto_feldname.split('_', 1)[1]
vals = {
'templatefeldname': template_field_name,
'display': source_field.display,
'displaytemplate': new_display_template.id,
'feldname': source_field.feldname,
'color_used': source_field.color_used,
'color_unused': source_field.color_unused,
'mediastructure': source_field.mediastructure.id,
'is_btn': source_field.is_btn,
'btn_pos_x': source_field.btn_pos_x,
'btn_pos_y': source_field.btn_pos_y,
'btn_pos_w': source_field.btn_pos_w,
'btn_pos_h': source_field.btn_pos_h,
'btn_name': source_field.btn_name,
'btn_image': source_field.btn_image,
'btn_fieldname': source_field.btn_fieldname,
'btn_visible': source_field.btn_visible,
'btn_active': source_field.btn_active,
'btn_action_inactive': source_field.btn_action_inactive,
'btn_action_active': source_field.btn_action_active,
'btn_playlist_zuordnung': source_field.btn_playlist_zuordnung,
'btn_gallery_rows': source_field.btn_gallery_rows,
'btn_gallery_cols': source_field.btn_gallery_cols,
'btn_gallery_prv_w': source_field.btn_gallery_prv_w,
'btn_gallery_prv_h': source_field.btn_gallery_prv_h,
'btn_has_timer': source_field.btn_has_timer,
'btn_has_timer_time': source_field.btn_has_timer_time,
'btn_editable': source_field.btn_editable,
'btn_special_actionimage': source_field.btn_special_actionimage,
'btn_special_actionimage_x': source_field.btn_special_actionimage_x,
'btn_special_actionimage_y': source_field.btn_special_actionimage_y,
'btn_special_actionimage_w': source_field.btn_special_actionimage_w,
'btn_special_actionimage_h': source_field.btn_special_actionimage_h,
'btn_special_actionimage_time': source_field.btn_special_actionimage_time,
'btn_text_1_visible': source_field.btn_text_1_visible,
'btn_text_2_visible': source_field.btn_text_2_visible,
'btn_text_3_visible': source_field.btn_text_3_visible,
'btn_text_4_visible': source_field.btn_text_4_visible,
'btn_text_1_pos_x': source_field.btn_text_1_pos_x,
'btn_text_1_pos_y': source_field.btn_text_1_pos_y,
'btn_text_2_pos_x': source_field.btn_text_2_pos_x,
'btn_text_2_pos_y': source_field.btn_text_2_pos_y,
'btn_text_3_pos_x': source_field.btn_text_3_pos_x,
'btn_text_3_pos_y': source_field.btn_text_3_pos_y,
'btn_text_4_pos_x': source_field.btn_text_4_pos_x,
'btn_text_4_pos_y': source_field.btn_text_4_pos_y,
'btn_text_1_font': source_field.btn_text_1_font,
'btn_text_2_font': source_field.btn_text_2_font,
'btn_text_3_font': source_field.btn_text_3_font,
'btn_text_4_font': source_field.btn_text_4_font,
}
template_field_vals.append(vals)
created_template_fields = self.env['dss.advertisefields.templates'].create(template_field_vals)
new_display_template.write({'feldtemplates': [(6, 0, created_template_fields.ids)]})
return {
'type': 'ir.actions.act_window',
'name': _('Neue Werbefeld-Vorlage'),
'res_model': 'dss.display.templates',
'view_mode': 'form',
'res_id': new_display_template.id,
}

17
models/dss_ai_chat_history.py Executable file
View File

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class DssAIChatHistory(models.Model):
_name = "dss.ai.chat.history"
_description = "DigitalSignage AI Chat History"
_order = "create_date desc, id desc"
user_id = fields.Many2one('res.users', required=True, index=True, default=lambda self: self.env.user)
role = fields.Selection(
[('user', 'User'), ('assistant', 'Assistant')],
required=True,
index=True,
)
content = fields.Text(required=True)

111
models/dss_binaries.py Normal file → Executable file
View File

@ -66,6 +66,7 @@ class dssbinaries(models.Model):
binary_secured_ro = fields.Boolean(string="Gesperrt", default=False, help = "Ein Flag, das angibt, ob die Datei gesperrt ist. Wenn dies aktiviert ist, kann die Datei nicht mehr bearbeitet oder gelöscht werden. Sie wird dann auch nicht in Übertragungen oder Ansichten beachtet.")
binary_used_ro = fields.Boolean(string="Genutzt", default=False, help = "Ein Flag, das angibt, ob die Datei in einer Übertragung/Ausstrahlung verwendet wird. Wenn dies aktiviert ist, kann die Datei nicht mehr gelöscht oder überarbeitet werden.")
binary_contract = fields.Many2one('dss.contracts', string="Vertrag", help = "Der Vertrag, zu dem diese Datei gehört. Dies kann verwendet werden, um Dateien zu organisieren und zu verwalten, die mit bestimmten Verträgen verbunden sind.")
binary_project = fields.Many2one('dss.projects', string="Projekt", help = "Das Projekt, zu dem diese Datei gehört. Dies kann verwendet werden, um Dateien zu organisieren und zu verwalten, die mit bestimmten Projekten verbunden sind.")
@ -122,64 +123,66 @@ class dssbinaries(models.Model):
@api.onchange('binary_binary')
def _onchange_binary_binary(self):
"""Update binary size and type when binary content is changed."""
_logger.info("New Binary B_"+str(self.id))
existing_binary = False
if not self._origin.binary_binary:
if self.binary_contract and 1 == 2:
"""Update binary size and type when binary content is changed."""
_logger.info("New Binary B_"+str(self.id))
existing_binary = False
else:
existing_binary = True
if not self._origin.binary_binary:
existing_binary = False
else:
existing_binary = True
if not self.binary_contract.cloudlink:
raise ValidationError(_("Der Vertrag hat keinen Cloudlink. Bitte überprüfen Sie den Vertrag."))
cloudpath = str(self.binary_contract.cloudlink)
_logger.info("New Binary checking Cloudlink B_"+str(self.id)+" - "+str(cloudpath))
cloudpath = cloudpath + '/' + str(self.binary_mediatype.cloudlink)
client = Client("https://cloud.logumedia.de/remote.php/dav/files/OdooDav/", auth=("odooClient@logumedia.de", "lm2020#OdooDav"))
_logger.info("New Binary checking Cloudlink for cont : "+str(cloudpath))
cloudexists = False
if client.exists(cloudpath):
# Convert self.binary_binary to bytes-like object
if isinstance(self.binary_binary, str):
filedata = base64.b64decode(self.binary_binary.split("base64,")[-1])
if not self.binary_contract.cloudlink:
raise ValidationError(_("Der Vertrag hat keinen Cloudlink. Bitte überprüfen Sie den Vertrag."))
cloudpath = str(self.binary_contract.cloudlink)
_logger.info("New Binary checking Cloudlink B_"+str(self.id)+" - "+str(cloudpath))
cloudpath = cloudpath + '/' + str(self.binary_mediatype.cloudlink)
client = Client("https://cloud.logumedia.de/remote.php/dav/files/OdooDav/", auth=("odooClient@logumedia.de", "lm2020#OdooDav"))
_logger.info("New Binary checking Cloudlink for cont : "+str(cloudpath))
cloudexists = False
if client.exists(cloudpath):
# Convert self.binary_binary to bytes-like object
if isinstance(self.binary_binary, str):
filedata = base64.b64decode(self.binary_binary.split("base64,")[-1])
else:
filedata = base64.b64decode(self.binary_binary)
filedataStr = str(filedata)
_logger.info("New Binary Cloudlink exists - Saving File - B_"+str(self.id)+" - "+str(cloudpath)+" - " + str(filedata)+' - ' + filedataStr[0:8])
try:
decoded_data = filedata
except base64.binascii.Error as e:
if 'Incorrect padding' in str(e):
filedataStr += b'=' * (4 - len(filedataStr) % 4)
decoded_data = base64.b64decode(filedataStr)
if filedataStr[0:10] == "b'\x89PNG":
extension = 'png'
else:
extension = 'jpg'
_logger.info("New Binary Cloudlink exists - Saving File - B_"+str(self.id)+" - "+str(cloudpath)+" - " + str(base64.b64encode(decoded_data)))
##infile = open('/tmp/'+filename, "w+b")
#infile = tempfile.NamedTemporaryFile(delete=False)
##infile.write(filedata)
##client.upload_fileobj(infile, f"{cloudpath}/{filename}")
##infile.close()
##infile.delete() # Delete the temporary file after upload
cloudexists = True
else:
filedata = base64.b64decode(self.binary_binary)
filedataStr = str(filedata)
_logger.info("New Binary Cloudlink exists - Saving File - B_"+str(self.id)+" - "+str(cloudpath)+" - " + str(filedata)+' - ' + filedataStr[0:8])
try:
decoded_data = filedata
except base64.binascii.Error as e:
if 'Incorrect padding' in str(e):
filedataStr += b'=' * (4 - len(filedataStr) % 4)
decoded_data = base64.b64decode(filedataStr)
if filedataStr[0:10] == "b'\x89PNG":
extension = 'png'
else:
extension = 'jpg'
_logger.info("New Binary Cloudlink exists - Saving File - B_"+str(self.id)+" - "+str(cloudpath)+" - " + str(base64.b64encode(decoded_data)))
##infile = open('/tmp/'+filename, "w+b")
#infile = tempfile.NamedTemporaryFile(delete=False)
##infile.write(filedata)
##client.upload_fileobj(infile, f"{cloudpath}/{filename}")
##infile.close()
##infile.delete() # Delete the temporary file after upload
cloudexists = True
else:
_logger.info("New Binary Cloudlink does not exists B_"+str(self.id)+" - "+str(cloudpath))
raise ValidationError(_("Der Cloudlink im Vertrag ist nicht vorhanden. Bitte überprüfen Sie den Link ggf. anlegen."))
_logger.info("New Binary Cloudlink does not exists B_"+str(self.id)+" - "+str(cloudpath))
raise ValidationError(_("Der Cloudlink im Vertrag ist nicht vorhanden. Bitte überprüfen Sie den Link ggf. anlegen."))
if cloudexists:
if self.binary_binary:
# Calculate the size of the binary content
self.binary_size = len(filedata)
# Determine the file type based on the binary content
self.binary_cloud_link = f"{cloudpath}/{self.binary_filename}"
self.binary_issaved = True
else:
self.binary_size = 0
self.binary_type = ''
self.binary_cloud_link = ''
self.binary_issaved = False
if cloudexists:
if self.binary_binary:
# Calculate the size of the binary content
self.binary_size = len(filedata)
# Determine the file type based on the binary content
self.binary_cloud_link = f"{cloudpath}/{self.binary_filename}"
self.binary_issaved = True
else:
self.binary_size = 0
self.binary_type = ''
self.binary_cloud_link = ''
self.binary_issaved = False
def dload(self):

74
models/dss_company_type.py Executable file
View File

@ -0,0 +1,74 @@
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
import hashlib
from PIL import Image,ImageDraw,ImageFont
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
from webdav4.client import Client
from webdav4.fsspec import WebdavFileSystem
import sys
TUYA_LOGGER.setLevel(logging.DEBUG)
_logger = logging.getLogger(__name__)
class dsscompanytype(models.Model):
@api.model
def create(self,vals):
result = super().create(vals)
return result
_name = "dss.company.type"
_description = "DigitalSignage Firmentypen"
# _inherit = ['mail.thread','mail.activity.mixin']
_rec_name = "Firmetyp_name"
# _inherit = ['mail.thread', 'mail.activity.mixin']
uuid = fields.Char(default=lambda self: self._default_uuid(), required=True, readonly=True, copy=False, string='UUID')
date_create = fields.Date('Erstellungsdatum',default=lambda self: self._default_create_date())
date_lastedit = fields.Date('Änderungsdatum')
user_create = fields.Char('Erstellungsuser',default=lambda self: self._default_create_user())
user_lastedit = fields.Char('Änderungsuser')
Firmetyp_name = fields.Char(string="Firmetyp Name", required=True, help = "Der Name des Firmetyps, die Sie hochladen möchten. Dieser Name wird in der Benutzeroberfläche angezeigt.")
firmentyp_needs_suffix = fields.Boolean(string="Benötigt Suffix", help="Gibt an, ob der Firmentyp ein Suffix benötigt")
firmentyp_suf = fields.Char(string="Firmetyp Suffix", help="Suffix für den Firmentyp im Namen")
firmentyp_turn_data = fields.Boolean(string="Turn Data", help="Gibt an, ob der Firmenname und Name getauscht werden müssen")
@api.model
def _default_uuid(self):
return str(uuid.uuid4())
@api.model
def _default_create_date(self):
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
@api.model
def _default_create_user(self):
return self.env.user.name if self.env.user else 'System'
@api.model
def _default_binary_id(self):
# Generate a unique binary ID based on the current timestamp and a random UUID
return f"Datei_{str(uuid.uuid4())[:8]}"

View File

@ -16,6 +16,7 @@ import tempfile
import easywebdav
import os
import os.path
import kontocheck
from PIL import Image,ImageDraw,ImageFont
@ -33,6 +34,7 @@ from tuya_iot import TuyaOpenAPI, TUYA_LOGGER
from tuya_connector import TuyaOpenAPI, TUYA_LOGGER
from webdav4.client import Client
from webdav4.fsspec import WebdavFileSystem
#from dss_trigger import trigger
import sys
TUYA_LOGGER.setLevel(logging.DEBUG)
@ -85,7 +87,7 @@ class dsscontracts(models.Model):
_rec_name = "contract_auto_name"
_inherit = ['mail.thread','dss.activity.mixin','dss.triggermodel']
uuid = fields.Char(default=lambda self: self._default_uuid(), required=True, readonly=True, copy=False, string='UUID')
contract_creator = fields.Many2one('res.users', string='Vertrag erstellt von', default=lambda self: self.env.user, readonly=True)
contract_creator = fields.Many2one('res.users', string='Vertrag erstellt von', default=lambda self: self.env.user)
contract_create_date = fields.Datetime('Vertrag erstellt am', default=lambda self: fields.Datetime.now(), readonly=True)
cruuid = fields.Char(related='uuid')
contract_id = fields.Char("Kundennummer",store=True,tracking=True)
@ -94,7 +96,8 @@ class dsscontracts(models.Model):
contract_state_name = fields.Char(related='contract_state.statusname',string='Vertragsstatus - Text',store=True)
contract_state_color = fields.Char(related='contract_state.color',string='Vertragsstatus - Farbe',store=True)
contract_state_order = fields.Integer(related='contract_state.order',string='Vertragsstatus - Reihenfolge',store=True)
contract_state_minimal_kanban = fields.Boolean(related='contract_state.kanban_display_minimal',string='Kanban mininmal',store=True)
contract_state_minimal_kanban = fields.Boolean(related='contract_state.kanban_display_minimal',string='Kanban minimieren',store=True)
contract_state_kanban_collapsed = fields.Selection(related='contract_state.kanban_collapsed_choice', string='Kanban eingeklappt', store=True)
contract_auto_id = fields.Char("Kundennummer",tracking=True,help="Wird berechnet aus Projektnummer + Kunden ID")
contract_auto_name = fields.Char('Vertragskennug',tracking=True,help="Wird berechnet aus Kundennummer + Vertragskennung")
@ -109,6 +112,9 @@ class dsscontracts(models.Model):
contract_receipt_done_date = fields.Date('Rechnungsdatum',tracking=True)
contract_receipt_done_until_date = fields.Date('Zahlungsziel', tracking=True)
client_is_export_finance = fields.Boolean('An Buchhaltung exportiert ?', tracking=True)
client_is_export_finance_date = fields.Date('An Buchhaltung exportiert am', tracking=True)
client_is_inkaso = fields.Boolean('Ist Inkasso eröffnet ?', tracking=True)
client_inkaso_datum = fields.Datetime('Inkasso eröffnet am ', tracking=True)
client_inkaso_info = fields.Char('Inkasso Bemerkungen', tracking=True)
@ -144,7 +150,9 @@ class dsscontracts(models.Model):
project_grundsystem_typ_cloud_contract_template = fields.Char(related='project.grundsystem_default_cloud_structure_contract', tracking=True)
project_grafiker = fields.Many2one(related="project.project_grafiker",tracking=True)
project_system_uuid = fields.Char(related='project.grundsystemnameuuid', tracking=True)
project_techniker = fields.Many2one(related='project.project_techniker', string='Aufbau')
techniker_computed = fields.Many2one('res.partner', compute='_compute_techniker_computed', string='Techniker tatsächlich')
run_trigger = fields.Boolean(tracking=True)
run_uni_trigger = fields.Boolean(tracking=True)
run_uni_sub_trigger = fields.Boolean(tracking=True)
@ -168,10 +176,12 @@ class dsscontracts(models.Model):
contact_name = fields.Char(related="client.name")
contact_dsspartner_vorname = fields.Char(related="client.dsspartner_vorname")
contact_dsspartner_name = fields.Char(related="client.dsspartner_name")
contact_company_type = fields.Many2one(related="client.dsspartner_Company_type")
parent_id = fields.Many2one('dss.contracts', string='Parent Task', index=True,tracking=True)
client_short_company = fields.Char('Firmenname Kunde',tracking=True)
client_short_company_type = fields.Many2one('dss.company.type',string='Firmentyp Kunde',tracking=True)
client_short_vorname = fields.Char('Vorname Kunde',tracking=True)
client_short_name = fields.Char('Name Kunde',tracking=True)
client_short_strasse = fields.Char('Strasse Kunde',tracking=True)
@ -188,6 +198,9 @@ class dsscontracts(models.Model):
client_short_leitwegid = fields.Char('Leitwegid Kunde',tracking=True)
client_short_BIC = fields.Char('BIC Kunde',tracking=True)
client_short_IBAN = fields.Char('IBAN Kunde',tracking=True)
client_short_BANK_computed = fields.Char(string='Bank Kunde',compute='_compute_cutshort_Bank')
client_short_KONTO_computed = fields.Char(string='Konto Kunde',compute='_compute_cutshort_Konto')
client_short_BLZ_computed = fields.Char(string='BLZ Kunde',compute='_compute_cutshort_BLZ')
client_other_projects = fields.Many2many('dss.projects',string='Weitere Projekte',tracking=True)
@ -220,12 +233,17 @@ class dsscontracts(models.Model):
contract_date = fields.Date('Vertragsdatum',tracking=True)
start_date = fields.Date('Start/Auslief.datum',tracking=True,help="Datum des geplanten Startes der 1. Kampagne. Also die Uraustrahlung!")
calc_end_date = fields.Date('Enddatum berechnet',tracking=True,help="Berechnetes Enddatum der Kampagne basierend auf Startdatum und Laufzeit (Ändert sich bei Verlängerungen).")
contract_cancel_mon = fields.Integer('Kündigungsfrist Monaten',tracking=True)
contract_cancel_date = fields.Date('Kündigungsfrist Errechnet', tracking=True)
contract_iscanceled = fields.Boolean('Vertrag gekündigt',tracking=True)
contract_iscanceled_date = fields.Date('Vertrag gekündigt am',tracking=True)
contract_iscanceled_logumedia = fields.Boolean('Vertrag gekündigt durch Logumedia',tracking=True)
contract_iscanceled_logumedia_date = fields.Date('Vertrag von Logumedia gekündigt am ',tracking=True)
contract_iscanceled_logumedia_reason = fields.Char('Vertrag von Logumedia gekündigt Grund',tracking=True)
contract_auto_extend = fields.Boolean('autom. V.Verlängerung',tracking=True)
contract_auto_extend_time = fields.Char('Verl. Monate',tracking=True)
contract_auto_extend_count = fields.Integer('Anzahl Vertragsverlängerungen', tracking=True, default=0)
scan_vertrag = fields.Binary('Datei')
scan_vertrag_filename = fields.Char("Dateiname") # Hilft, den Dateinamen zu speichern
@ -279,6 +297,7 @@ class dsscontracts(models.Model):
vnnox_zugang_username = fields.Char('Vnnox Username',tracking=True)
vnnox_zugang_password = fields.Char('Vnnox Passwort',tracking=True)
vnnox_zugang_gesendet = fields.Boolean('Vnnox Zugang gesendet?',tracking=True)
vnnox_playlist_checked = fields.Boolean('Vnnox Liste geprüft ?',tracking=True)
vnnox_playlist_checked_empty = fields.Boolean('Vnnox Liste war leer ?',tracking=True)
vnnox_playlist_checked_date = fields.Date('Vnnox Liste geprüft am ',tracking=True)
@ -291,6 +310,7 @@ class dsscontracts(models.Model):
lmw_zugang_erstellt = fields.Boolean('LMW Zugang ?',tracking=True)
lmw_zugang_username = fields.Char('LMW Username',tracking=True)
lmw_zugang_password = fields.Char('LMW Passwort',tracking=True)
lmw_zugang_gesendet = fields.Boolean('LMW Zugang gesendet?',tracking=True)
wflow_korrekturabzug = fields.Boolean('Korekturabzug gesendet ?',tracking=True)
@ -342,9 +362,9 @@ class dsscontracts(models.Model):
cloudlink = fields.Char('Cloud Verzeichnis',help='Verzeichnis für den Kunde innerhalb des Projekt Ordners')
web_contract = fields.Many2one('dss.web_contracts' , string='Web_Vertrag', store=True,tracking=True)
web_contract_input_notfinished = fields.Boolean('Web Vertrag Eingabe nicht abgeschlossen',tracking=True)
web_contract_input_finished = fields.Boolean('Web Vertrag Eingabe abgeschlossen',tracking=True)
web_contract_input_finished = fields.Boolean('Web Vertrag Eingabe war abgeschlossen',tracking=True)
web_contract_paysum_gfx = fields.Float('Web Vertrag Zahlungssumme Grafik',tracking=True)
web_contract_paysum_lz = fields.Float('Web Vertrag Zahlungssumme Laufzeit',tracking=True)
web_contract_paysum_lz = fields.Float('Web Vertrag Zahlungssumme Laufzeit',tracking=True)
web_contract_paysum_all = fields.Float('Web Vertrag Zahlungssumme Gesamt',tracking=True)
web_contract_sign_ort = fields.Char('Web Vertrag Ort',tracking=True)
web_contract_sign_datum = fields.Char('Web Vertrag Datum',tracking=True)
@ -355,15 +375,115 @@ class dsscontracts(models.Model):
web_contract_sonder_0 = fields.Char('Web Vertrag Sonderfeld 01',tracking=True)
web_contract_sonder_1 = fields.Char('Web Vertrag Sonderfeld 02',tracking=True)
web_contract_sonder_2 = fields.Char('Web Vertrag Sonderfeld 03',tracking=True)
web_contract_satzmail = fields.Boolean('Web Vertrag Satzmaterial als Mail',tracking=True)
web_contract_satzgiven = fields.Boolean('Web Vertrag Satzmaterial gegeben',tracking=True)
web_contract_rgmail = fields.Boolean('Web Vertrag Rechnung als Mail',tracking=True)
web_contract_kamail = fields.Boolean('Web Vertrag Korrekturabzug Zugang',tracking=True)
web_constract_zahlstonst = fields.Char('Web Vertrag Zahlungsweise',tracking=True)
web_contract_rejected = fields.Boolean('Eingabe - Web Vertrag abgelehnt',tracking=True)
web_contract_reject_reason = fields.Char('Eingabe - Web Vertrag Ablehnungsgrund',tracking=True)
web_bande_spieltage = fields.Char('Web Vertrag Bande - gebuchte Spieltage',tracking=True)
web_contract_start_planed = fields.Char("TV Kampagne Start",tracking=True)
web_contract_remove_planed = fields.Char("TV Kampagne Ende",tracking=True)
tv_reach_PLZ = fields.Char(string='Reichweite PLZ',tracking=True)
ads_radius_PLZ = fields.Integer('Umkreis PLZ in Km',tracking=True)
ads_count_perYear = fields.Selection([('30000','30.000'),('60000','60.000'),('120000','120.000'),('1000000','1.000.000')],'Einblendungen alt',tracking=True)
ads_count_perYear2 = fields.Integer('Einblendungen',tracking=True)
ads_topics = fields.Many2many('dss.contracts_ads_topics', string='Themenliste', tracking=True)
ads_topics_text = fields.Char('Themenliste gesamt',compute='_compute_themenliste')
userdef_vars = fields.Many2many('dss.contracts_uservars', string='Vertrags Uservariablen', tracking=True)
contract_finish_img_cloud = fields.Char('Überg. Bildlink',tracking=True)
@api.depends('techniker','project_techniker')
@api.onchange('techniker')
@api.onchange('project_techniker')
def _compute_techniker_computed(self):
for record in self:
if record.techniker:
record.techniker_computed = record.techniker
elif record.project_techniker:
record.techniker_computed = record.project_techniker
else:
record.techniker_computed = False
@api.depends('client_short_IBAN')
@api.onchange('client_short_IBAN')
@api.onchange('client_short_BIC')
def _compute_cutshort_Bank(self):
# BIC für Deutsche Bank
kontocheck.lut_load()
for record in self:
bankname =''
if record.client_short_BIC:
try:
bankname = kontocheck.scl_get_bankname(record.client_short_BIC)
except ValueError:
bankname = ''
else :
if record.client_short_BLZ_computed:
try:
bankname = kontocheck.get_bankname(record.client_short_BLZ_computed)
except ValueError:
bankname = ''
record.client_short_BANK_computed = bankname
def get_account_from_iban(iban):
# Entfernen von Leerzeichen und Großbuchstaben
clean_iban = "".join(iban.split()).upper()
# Überprüfen, ob es eine deutsche IBAN ist
if len(clean_iban) == 22 and clean_iban.startswith("DE"):
# Die letzten 10 Stellen sind die Kontonummer
account_number = clean_iban[-10:]
return account_number
else:
return "Ungültige deutsche IBAN"
def get_blz_from_iban(iban):
# Entfernen von Leerzeichen und Großbuchstaben
clean_iban = "".join(iban.split()).upper()
# Überprüfen, ob es eine deutsche IBAN ist
if len(clean_iban) == 22 and clean_iban.startswith("DE"):
# Die letzten 10 Stellen sind die Kontonummer
blz_number = clean_iban[4:12]
return blz_number
else:
return "Ungültige deutsche IBAN"
@api.depends('client_short_IBAN')
@api.onchange('client_short_IBAN')
def _compute_cutshort_Konto(self):
kontocheck.lut_load()
for record in self:
Kontonummer = ''
if record.client_short_IBAN:
try:
Kontonummer = dsscontracts.get_account_from_iban(record.client_short_IBAN)
except ValueError:
Kontonummer = ''
record.client_short_KONTO_computed = Kontonummer
@api.depends('client_short_IBAN')
@api.onchange('client_short_IBAN')
def _compute_cutshort_BLZ(self):
kontocheck.lut_load()
for record in self:
Bankleitzahl = ''
if record.client_short_IBAN:
try:
Bankleitzahl = dsscontracts.get_blz_from_iban(record.client_short_IBAN)
except ValueError:
Bankleitzahl = ''
record.client_short_BLZ_computed = Bankleitzahl
@api.depends('work_marker_1')
@api.onchange('work_marker_1')
def _get_marker_1_color_false(self):
@ -706,7 +826,7 @@ class dsscontracts(models.Model):
myplz=self.client_short_plz
result = self.env['res.partner'].create({'name':fullname,
'company_name': self.client_short_company,
'dsspartner_vorname':self.client_short_vorname,
'dsspartner_vorname':self.client_short_vorname,
'dsspartner_name':self.client_short_name,
'dsspartner':True,
'dsspartner_werbung':True,
@ -719,6 +839,7 @@ class dsscontracts(models.Model):
'rgemail':self.client_short_rgemail,
'is_company':False})
result.country_id = self.client_short_land
result.dsspartner_Company_type = self.client_short_company_type.id
_logger.info('Creating Contact ' + str(result) + ' ' + str(fullname))
self.client = result
# action = self.env['ir.actions.act_window'].with_context({'default_contractid': self.id})._for_xml_id('DigitalSignage.act_dss_project_view_contract')
@ -914,6 +1035,9 @@ class dsscontracts(models.Model):
defadstateid = defadstate.id
kampagne = self.env['dss.ads'].create({'contract': self.id, 'project': self.project.id, 'adname': 'WK '+self.contract_auto_name,'adtype':'MAIN','ad_state':defadstateid})
self.ads_last_ad = kampagne.id
if self.web_contract_start_planed :
kampagne.date_start_planed = self.web_contract_start_planed
kampagne.date_remove_planed = self.web_contract_remove_planed
_logger.info('Click auf Werbekampagne : C_'+str(self.id)+' Kampagne erstellt A_'+str(kampagne.id))
kampagne.parent_ad = kampagne.id
mediaids = []
@ -1018,6 +1142,7 @@ class dsscontracts(models.Model):
else:
_logger.info('Prüfe Medien : C_'+str(self.id)+'A_' +str(kamp.id)+' Keine Kampagne gefunden !')
# _logger.info('Click auf Werbekampagne : '+str(self.id)+' 4 '+str(kampagne.id))
return {
'type': 'ir.actions.act_window',
'view_type':'form',
@ -1054,6 +1179,91 @@ class dsscontracts(models.Model):
if record.contract_auto_id == "":
record.contract_auto_id = cidstr
def _compute_contract_end_for_calc_date(self):
self.ensure_one()
start_date = self.start_date or self.contract_date
if not start_date:
return False
if self.contract_iscanceled and self.ads_last_todo_state_until:
return self.ads_last_todo_state_until
runtime_months = int(self.runtime_m or 0) + int(self.runtime_bonus_m or 0)
if self.contract_auto_extend:
try:
runtime_months += int((self.contract_auto_extend_time or '0').strip() or 0)
except Exception:
pass
return start_date + relativedelta(months=runtime_months)
def action_update_calc_contract_end_selected(self):
updated_count = 0
for record in self:
new_end_date = record._compute_contract_end_for_calc_date()
if record.calc_end_date != new_end_date:
record.calc_end_date = new_end_date
updated_count += 1
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Vertragsende aktualisiert'),
'message': _('%s Vertrag(e) wurden aktualisiert.') % updated_count,
'type': 'success',
'sticky': False,
}
}
def action_remove_selected_fields(self):
selected_fields = self.mapped('werbe_feld_selected')
contract_ids = self.ids
self.write({'werbe_feld_selected': [(5, 0, 0)]})
for ad_field in selected_fields:
remove_cmds = [(3, contract_id) for contract_id in contract_ids if contract_id in ad_field.contract.ids]
if remove_cmds:
ad_field.write({'contract': remove_cmds})
if ad_field.contract_save and ad_field.contract_save.id in contract_ids:
ad_field.contract_save = ad_field.contract[:1].id or False
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Felder entfernt'),
'message': _('%s Vertrag(e) wurden von ihren Werbefeldern getrennt.') % len(self),
'type': 'success',
'sticky': False,
}
}
def action_open_move_to_fallback_field_wizard(self):
if not self:
raise ValidationError(_("Bitte mindestens einen Vertrag auswaehlen."))
projects = self.mapped('project')
if len(projects) != 1:
raise ValidationError(_("Bitte nur Vertraege aus einem Projekt gleichzeitig auswaehlen."))
view = self.env.ref('DigitalSignage.dss_contracts_move_field_wizard_view_form')
return {
'type': 'ir.actions.act_window',
'name': _('Felder auf ausweichfeld umsetzen'),
'res_model': 'dss.contracts.move.field.wizard',
'view_mode': 'form',
'view_id': view.id,
'views': [(view.id, 'form')],
'target': 'new',
'context': {
'active_ids': self.ids,
'default_project_id': projects.id,
},
}
# def setRealGrafiker(self):
#for record in self:
#if self.grafiker:
@ -1165,6 +1375,212 @@ class dsscontracts(models.Model):
)
if action:
return action
def export_kd_csv_all_not_work(self):
_logger.info('Export Kunden CSV : C_'+str(self.id))
action = (self.env["confirmation.wizard"].confirm_message(_("Soll der Kunde einzeln übertragen werden ?"),title="Bitte bestätigen",method="export_kd_csv",records=self,callback_params={"template":self.id}))
if action:
_logger.info('Export Kunden CSV : C_'+str(self.id)+' - einzeln bestätigt ')
csv = 'Kundenname;Firma;Strasse;Ort;PLZ;Telefon;Mobil;Email;RG-Email;Leitwegid;Company Type\n'
#csv += str(self.client_short_vorname) + ' ' + str(self.client_short_name) + ';' + str(self.client_short_company) + ';' + str(self.client_short_strasse) + ';' + str(self.client_short_ort) + ';' + str(self.client_short_plz) + ';' + str(self.client_short_telefon) + ';' + str(self.client_short_mobil) + ';' + str(self.client_short_email) + ';' + str(self.client_short_rgemail) + ';' + str(self.client_short_leitwegid) + ';' + str(self.client_short_company_type.name if self.client_short_company_type else '')+'\n'
else:
action = (self.env["confirmation.wizard"].confirm_message(_("Soll der Kunde zur Liste der Übertragungen hinzugefügt werden ?"),title="Bitte bestätigen",method="export_kd_csv",records=self,callback_params={"template":self.id}))
if action:
_logger.info('Export Kunden CSV : C_'+str(self.id)+' - Hinzufügen bestätigt ')
action = (self.env["confirmation.wizard"].confirm_message(_("Liste jetzt als CSV erstellen ?"),title="Bitte bestätigen",method="export_kd_csv",records=self,callback_params={"template":self.id}))
if action:
_logger.info('Export Kunden CSV : C_'+str(self.id)+' - CSV bestätigt ')
@staticmethod
def _split_text_for_csv(value, first_max=40, second_max=40):
text = str(value or '').strip()
if len(text) <= first_max:
return text, ''
boundary_cut = -1
remainder_start = first_max
for match in re.finditer(r'[\s\W_]+', text[:first_max + 1]):
if 0 < match.start() <= first_max:
boundary_cut = match.start()
remainder_start = match.end()
# If no useful separator exists near the limit, fallback to strict split.
if boundary_cut < int(first_max * 0.6):
first_line = text[:first_max]
second_line = text[first_max:]
else:
first_line = text[:boundary_cut]
second_line = text[remainder_start:]
return first_line, second_line.lstrip()[:second_max]
@api.model
def checkattachents(self,project,contract):
_logger.info('Check Attachments : C_'+str(contract)+' - P_'+str(project))
html = ""
if project.contract_finish_img_cloud:
_logger.info('Check Attachments : C_'+str(contract)+' - P_'+str(project)+' - Projekt hat Cloudbilder :'+str(project.contract_finish_img_cloud))
return True
else:
if contract.contract_finish_img_cloud:
_logger.info('Check Attachments : C_'+str(contract)+' - P_'+str(project)+' - Vertrag hat Cloudbilder :'+str(contract.contract_finish_img_cloud))
return True
else:
return False
@staticmethod
def _encode_csv_attachment(csv_text):
# UTF-8 with BOM keeps umlauts/special chars readable in Excel and downstream tools.
normalized = str(csv_text or '').replace('\r\n', '\n').replace('\r', '\n')
return base64.b64encode(normalized.encode('utf-8-sig'))
def _export_kd_csv_single(self,record_id):
dataset=self.env['dss.contracts'].browse(record_id)
_logger.info('Export Kunden CSV : C_'+str(self.id)+' - Einzelner Export ')
company_name_line_1, company_name_line_2 = self._split_text_for_csv(dataset.client_short_company)
csv = '"DEBITORENKONTO";"Kunde_seit";"ZAHLUNGSART";"SKONTOTYP";"ANREDE";"akad.Grad";"TITEL1";"AP-VORNAME";"AP-NAME";"NAME";"Ergaenzung";"LAND";"STRASSE";"PLZ";"ORT";"TELEFON1";"FAX1";"Mobil1";"INTERNET";"EMAIL1";"TELEFON1_AP";"KUNDENNR";"BANKNAME_1";"BANKKONTO_1";"BLZ_1";"IBAN_1";"BIC_1";"FIBU-KONTENBEZEICHNUNG";"KREDITLIMIT";"HAUSBANK";"FAELLIG_TG"'+'\n'
csv += ('"'+str(dataset.contract_auto_id) +
'";"' + str(dataset.contract_date.strftime('%d.%m.%Y')) +
'";"' + ('L' if dataset.web_contract_is_lastschrift else '') +
'";"' + str("4") +
'";"' + str("") +
'";"' + str("") +
'";"' + str("") +
'";"' + str(dataset.client_short_vorname) +
'";"' + str(dataset.client_short_name) +
'";"' + str(company_name_line_1) +
'";"' + str(company_name_line_2) +
'";"' + str(dataset.client_short_land.name if dataset.client_short_land else 'DEUTSCHLAND') +
'";"' + str(dataset.client_short_strasse) +
'";"' + str(dataset.client_short_plz) +
'";"' + str(dataset.client_short_ort) +
'";"' + str(dataset.client_short_telefon) +
'";"' + str(dataset.runtime_m) +
'";"' + str(dataset.client_short_mobil) +
'";"' + str(dataset.cutshortwerbe_feld_selected) +
'";"' + str(dataset.client_short_email) +
'";"' + str("") +
'";"' + str(dataset.contract_auto_id) +
'";"' + str(dataset.client_short_BANK_computed) +
'";"' + str(dataset.client_short_KONTO_computed) +
'";"' + str(dataset.client_short_BLZ_computed) +
'";"' + str(dataset.client_short_IBAN).upper() +
'";"' + str(dataset.client_short_BIC).upper() +
'";"' + str(re.sub(r'[^a-zA-Z0-9]', '', str(dataset.client_short_company.upper().strip()))[:20]) +
'";"' + str(dataset.web_contract_paysum_all) +
'";"' + str('2' if dataset.web_contract_is_lastschrift else '1') +
'"8"'+
'"\n')
_logger.info('Export Kunden CSV : C_'+str(dataset.id)+' - CSV Inhalt : \n'+str(csv))
#csv = csv.replace('"False"', '""')
filename = 'KDEXPORT_' + str(dataset.contract_auto_id) + '_' + str(uuid.uuid4())[:8] + '.csv'
attachment_id = self.env['ir.attachment'].create({ 'name': filename, 'datas': self._encode_csv_attachment(csv),'res_model': 'dss.contracts', 'res_id': dataset.id})
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
download_url = '/web/content/' + str(attachment_id.id) + '?download=true'
self.client_is_export_finance_date = datetime.now()
self.client_is_export_finance = True
#
# this does the download
return {
"type": "ir.actions.act_url",
"url": str(base_url) + str(download_url),
"target": "new"
}
def export_kd_csv_single(self,data_id=None):
if not self.client_is_export_finance or data_id:
self._export_kd_csv_single(self.id)
else:
_logger.info('Export Kunden CSV : C_'+str(self.id)+' - Bereits exportiert am '+str(self.client_is_export_finance_date))
action = (self.env["confirmation.wizard"].confirm_message(_("Der Kunde wurde bereits am "+str(self.client_is_export_finance_date)+" exportiert ! Soll der Export erneut durchgeführt werden ?"),title="Bitte bestätigen",method="export_kd_csv_single",records=self,callback_params={"data_id":self.id}))
if action:
_logger.info('Export Kunden CSV : C_'+str(self.id)+' - Einzelner Export erneut bestätigt ')
self.client_is_export_finance = False
return self.export_kd_csv_single()
else:
_logger.info('Export Kunden CSV : C_'+str(self.id)+' - Einzelner Export erneut abgelehnt ')
def _export_kd_csv_add(self,record_ids):
dataset=self.env['dss.contracts'].browse(record_ids)
_logger.info('Export Kunden CSV : C_'+str(self.id)+' - Hinzufügen ')
existing_user_var = self.env['dss.contracts_uservars'].search([('contract', '=', self.id),('var_ID','=','BHEXMARK_1')], limit=1)
if not existing_user_var:
uvar = self.env['dss.contracts_uservars'].create({'contract': self.id, 'var_ID': 'BHEXMARK_1', 'var_name':'Buchhaltung Export vorgemerkt !', 'var_value': str(datetime.now())})
self.userdef_vars = [(4, uvar.id)]
_logger.info('Export Kunden CSV : C_'+str(self.id)+' - Hinzufügen - Uservar erstellt ')
def export_kd_csv_add(self,data_id=None):
if not self.client_is_export_finance or data_id:
self._export_kd_csv_add(self.id)
else:
_logger.info('Export Kunden CSV : C_'+str(self.id)+' - Bereits exportiert am '+str(self.client_is_export_finance_date))
action = (self.env["confirmation.wizard"].confirm_no_action_message(_("Der Kunde wurde bereits am "+str(self.client_is_export_finance_date)+" exportiert ! "),title="Schon exportiert !"))
return action
def export_kd_csv_send(self):
_logger.info('Export Kunden CSV : C_'+str(self.id)+' - Senden ')
existing_user_var = self.env['dss.contracts_uservars'].search([('var_ID','=','BHEXMARK_1')])
Text = 'Export Kunden CSV es wurden folgende Kunden gefunden und übertragen : \n'
csv = '"DEBITORENKONTO";"Kunde_seit";"ZAHLUNGSART";"SKONTOTYP";"ANREDE";"akad.Grad";"TITEL1";"AP-VORNAME";"AP-NAME";"NAME";"Ergaenzung";"LAND";"STRASSE";"PLZ";"ORT";"TELEFON1";"FAX1";"Mobil1";"INTERNET";"EMAIL1";"TELEFON1_AP";"KUNDENNR";"BANKNAME_1";"BANKKONTO_1";"BLZ_1";"IBAN_1";"BIC_1";"FIBU-KONTENBEZEICHNUNG";"HAUSBANK";"FAELLIG_TG"'+'\n'
for contract_user_var in existing_user_var:
contract=contract_user_var.contract
if contract:
dataset=contract
_logger.info('Export Kunden CSV : C_'+str(self.id)+' - Einzelner Export ')
company_name_line_1, company_name_line_2 = self._split_text_for_csv(dataset.client_short_company)
csv += ('"'+str(dataset.contract_auto_id) + #DEBITORENKONTO
'";"' + str(dataset.contract_date.strftime('%d.%m.%Y')) + #Kunde_seit
'";"' + ('L' if dataset.web_contract_is_lastschrift else '') + #ZAHLUNGSART
'";"' + str("4") + #SKONTOTYP
'";"' + str("") + #ANREDE
'";"' + str("") + #akad.Grad
'";"' + str("") + #TITEL1
'";"' + str(dataset.client_short_vorname) + #AP-VORNAME
'";"' + str(dataset.client_short_name) + #AP-NAME
'";"' + str(company_name_line_1) + #NAME
'";"' + str(company_name_line_2) + #Ergaenzung
'";"' + str(dataset.client_short_land.name if dataset.client_short_land else 'DEUTSCHLAND') + #LAND
'";"' + str(dataset.client_short_strasse) + #STRASSE
'";"' + str(dataset.client_short_plz) + #PLZ
'";"' + str(dataset.client_short_ort) + #ORT
'";"' + str(dataset.client_short_telefon) + #TELEFON1
'";"' + str(dataset.runtime_m) + #FAX1
'";"' + str(dataset.client_short_mobil) + #Mobil1
'";"' + str(dataset.cutshortwerbe_feld_selected) + #INTERNET
'";"' + str(dataset.client_short_email) + #EMAIL1
'";"' + str(dataset.client_short_telefon) + #TELEFON1_AP
'";"' + str(dataset.contract_auto_id) + #KUNDENNR
'";"' + str(dataset.client_short_BANK_computed) + #BANKNAME_1
'";"' + str(dataset.client_short_KONTO_computed) + #BANKKONTO_1
'";"' + str(dataset.client_short_BLZ_computed) + #BLZ_1
'";"' + str(dataset.client_short_IBAN).upper() + #IBAN_1
'";"' + str(dataset.client_short_BIC).upper() + #BIC_1
'";"' + str((dataset.client_short_vorname)+' '+str(dataset.client_short_name)+' '+str(dataset.client_short_company))[:20] + #FIBU-KONTENBEZEICHNUNG
'";"' + str(dataset.web_contract_paysum_all).replace('.', ',') + #KREDITLIMIT
'";"' + str('2') + #HAUSBANK
'";"' + str('8') + #FAELLIG_TG
'"\n')
dataset.client_is_export_finance_date = datetime.now()
dataset.client_is_export_finance = True
Text += 'Kunde : '+str(dataset.contract_auto_name)+' - '+str(dataset.client_short_company)+'\n'
#contract.userdef_vars = [(5,0,0)]
_logger.info('Export Kunden CSV : C_'+str(self.id)+' - Senden - Uservar entfernt ')
contract_user_var.var_ID = 'BHEXMARK_1_SENT'
contract_user_var.var_value = str(datetime.now())
filename = 'KDEXPORT_' + str(datetime.now().strftime("%Y%m%d%H%M%S")) + '_' + str(uuid.uuid4())[:8] + '.csv'
attachment_id = self.env['ir.attachment'].create({ 'name': filename, 'datas': self._encode_csv_attachment(csv)})
attachment_id.allow_shared_download=True
attachment_id.folder_id=self.env['documents.folders'].search([('name','=','Kundenanlage')],limit=1).id
csvs = self.env['dss.csvs'].create({'csv_id': filename,'csv_name': filename, 'csv_filename': filename, 'csv_attachment_id': attachment_id.id, 'csv_mailtext': Text, 'csv_description': 'Export Kunden CSV am '+str(datetime.now().strftime("%Y-%m-%d %H:%M:%S"))})
#trigger.SendMail(self,MailEmpfaenger,MailEmpfaengerCC,MailEmpfaengerBCC,MailTemplate,MailSender,has_attachments=False,attachmentlist=[],with_confirm=False,Dataset=None):
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
download_url = '/web/content/' + str(attachment_id.id) + '?download=true'
return {
"type": "ir.actions.act_url",
"url": str(base_url) + str(download_url),
"target": "new"
}
def create_cloud_structure_client(self, newClient):
_logger.info("Create Cloud Structure for Client "+str(newClient.id)+" - "+str(newClient.cloudlink))
@ -1253,6 +1669,7 @@ class dsscontracts(models.Model):
self.client.email = self.client_short_email
self.client.rgemail = self.client_short_rgemail
self.client.leitwegid = self.client_short_leitwegid
self.client.dsspartner_Company_type = self.client_short_company_type
def pyaction_dss_contract_update_short_from_partner(self):
self.client_short_company=self.client.company_name
@ -1266,8 +1683,9 @@ class dsscontracts(models.Model):
self.client_short_email=self.client.email
self.client_short_rgemail=self.client.rgemail
self.client_short_leitwegid=self.client.leitwegid
self.client_short_company_type=self.client.dsspartner_Company_type
@api.model
def buildText(self):
_logger.info('Build Text for Contract : C_'+str(self.id))
@ -1410,7 +1828,11 @@ class dsscontracts(models.Model):
#raise ValidationError((all_contract_text))
return contracts
def lm_contract_tool_execute(self):
return self.buildText()
def pyopen_dss_contract_add_media_form_view(self):
_logger.info('Open Add Media Form')

1
models/dss_contract_uservar.py Normal file → Executable file
View File

@ -53,6 +53,7 @@ class dsscontractuservars(models.Model):
_inherit = ['mail.thread','dss.activity.mixin']
uuid = fields.Char(default=lambda self: self._default_uuid(), required=True, readonly=True, copy=False, string='UUID')
var_name = fields.Char(string="Variable Name", tracking=True)
var_ID = fields.Char(string="Variable ID", tracking=True)
var_value = fields.Char(string="Variable Wert", tracking=True)
var_description = fields.Text(string="Variable Beschreibung",tracking=True)
contract = fields.Many2one('dss.contracts', string="Vertrag",required=True, tracking=True)

View File

@ -0,0 +1,76 @@
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class DssContractsMoveFieldWizard(models.TransientModel):
_name = "dss.contracts.move.field.wizard"
_description = "Vertragsfelder auf Ausweichfeld umsetzen"
project_id = fields.Many2one(
'dss.projects',
string='Projekt',
required=True,
readonly=True,
)
target_field_id = fields.Many2one(
'dss.advertisefields',
string='Ausweichfeld',
required=True,
domain="[('project','=',project_id), '|', ('contract','=',False), ('isblocked','=',False)]",
)
@api.model
def default_get(self, fields_list):
values = super().default_get(fields_list)
active_ids = self.env.context.get('active_ids') or []
contracts = self.env['dss.contracts'].browse(active_ids).exists()
if contracts:
projects = contracts.mapped('project')
if len(projects) == 1:
values.setdefault('project_id', projects.id)
return values
def action_move_contracts_to_field(self):
self.ensure_one()
active_ids = self.env.context.get('active_ids') or []
contracts = self.env['dss.contracts'].browse(active_ids).exists()
if not contracts:
raise ValidationError(_("Bitte mindestens einen Vertrag auswaehlen."))
projects = contracts.mapped('project')
if len(projects) != 1:
raise ValidationError(_("Bitte nur Vertraege aus einem Projekt gleichzeitig auswaehlen."))
if projects.id != self.project_id.id:
raise ValidationError(_("Das gewaehlte Ausweichfeld passt nicht zum Projekt der Vertraege."))
if self.target_field_id.project.id != self.project_id.id:
raise ValidationError(_("Das Ausweichfeld muss zum gleichen Projekt gehoeren."))
contract_ids = contracts.ids
old_fields = contracts.mapped('werbe_feld_selected')
# Remove selected contracts from all old fields first, so they become available again.
for ad_field in old_fields:
remove_cmds = [(3, contract_id) for contract_id in contract_ids if contract_id in ad_field.contract.ids]
if remove_cmds:
ad_field.write({'contract': remove_cmds})
if ad_field.contract_save and ad_field.contract_save.id in contract_ids:
ad_field.contract_save = ad_field.contract[:1].id or False
add_cmds = [(4, contract_id) for contract_id in contract_ids if contract_id not in self.target_field_id.contract.ids]
if add_cmds:
self.target_field_id.write({'contract': add_cmds})
if not self.target_field_id.contract_save and contract_ids:
self.target_field_id.contract_save = contract_ids[0]
contracts.write({'werbe_feld_selected': [(6, 0, [self.target_field_id.id])]})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Ausweichfeld gesetzt'),
'message': _('%s Vertrag(e) wurden auf das Feld %s umgesetzt.') % (len(contracts), self.target_field_id.feldname),
'type': 'success',
'sticky': False,
}
}

0
models/dss_crm_extended.py Normal file → Executable file
View File

326
models/dss_csvs.py Executable file
View File

@ -0,0 +1,326 @@
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
import hashlib
from PIL import Image,ImageDraw,ImageFont
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
from webdav4.client import Client
from webdav4.fsspec import WebdavFileSystem
import sys
TUYA_LOGGER.setLevel(logging.DEBUG)
_logger = logging.getLogger(__name__)
class dsscsvs(models.Model):
@api.model
def create(self,vals):
result = super().create(vals)
return result
_name = "dss.csvs"
_description = "DigitalSignage CSV Dateien"
# _inherit = ['mail.thread','mail.activity.mixin']
_rec_name = "csv_id"
# _inherit = ['mail.thread', 'mail.activity.mixin']
uuid = fields.Char(default=lambda self: self._default_uuid(), required=True, readonly=True, copy=False, string='UUID')
date_create = fields.Date('Erstellungsdatum',default=lambda self: self._default_create_date())
date_lastedit = fields.Date('Änderungsdatum')
user_create = fields.Char('Erstellungsuser',default=lambda self: self._default_create_user())
user_lastedit = fields.Char('Änderungsuser')
csv_id = fields.Char(string="CSV ID", required=True, help = "Das ist eine eindeutige ID für diese Dateityp. Sie wird automatisch generiert und sollte nicht manuell geändert werden.", default=lambda self: self._default_csv_id())
csv_name = fields.Char(string="CSV Name", required=True, help = "Der Name des Dateityp, die Sie hochladen möchten. Dieser Name wird in der Benutzeroberfläche angezeigt.")
csv_binary = fields.Binary(string="CSV Datei", help = "Die eigentliche Datei, die Sie hochladen möchten. Diese Datei wird in der Datenbank und Cloud gespeichert")
csv_filename = fields.Char(string="CSV Dateiname", help = "Der Name der hochgeladenen Datei. Dies wird automatisch aus dem Dateinamen der hochgeladenen Datei abgerufen.",compute="_compute_csv_filename", store=True)
csv_cloud_link = fields.Char(string="Cloud Link", help = "Der Link zur Datei in der Cloud, wenn die Datei in der Cloud gespeichert ist.")
csv_description = fields.Text(string="CSV Beschreibung", help = "Eine Beschreibung der hochgeladenen Datei. Dies kann Informationen über den Inhalt der Datei, den Verwendungszweck oder andere relevante Details enthalten.")
csv_attachment_id = fields.Many2one('ir.attachment', string="CSV Anhang", help = "Der Anhang, der mit dieser CSV-Datei verbunden ist. Dieser Anhang enthält die tatsächliche Datei, die in der Datenbank gespeichert ist.")
csv_mailtext = fields.Text(string="CSV Mailtext", help = "Der Text, der in der E-Mail verwendet wird, wenn diese CSV-Datei per E-Mail versendet wird.")
@api.depends('binary_binary')
@api.onchange('binary_binary')
def _compute_binary_filename(self):
"""Compute the binary filename based on the uploaded file."""
_logger.info("Binary getting Filename - B_"+str(self.id))
if len(self)>0:
for record in self:
if record.binary_binary:
# Extract the filename from the binary content
extension = 'jpg' # Default extension
if isinstance(self.binary_binary, str):
filedata = base64.b64decode(self.binary_binary.split("base64,")[-1])
else:
filedata = base64.b64decode(self.binary_binary)
filedataStr = str(filedata)
_logger.info("Binary getting Filename - B_"+str(self.id)+" - ["+str(filedataStr[0:23])+"]")
if filedataStr[0:12] == "b'\\x89PNG\\":
extension = 'png'
elif filedataStr[0:17] == "b'\\xff\\xd8\\xff":
extension = 'jpg'
elif filedataStr[0:8] == "b'GIF89a":
extension = 'gif'
elif filedataStr[0:22] == "b'\\x49\\x49\\x2a\\x00":
extension = 'tif'
elif filedataStr[0:27] == "b'\\x25\\x50\\x44\\x46\\x2d":
extension = 'pdf'
elif filedataStr[0:23] == "b'\\x00\\x00\\x00 ftypisom":
extension = 'mp4'
elif filedataStr[0:22] == "b'\\x00\\x00\\x00\\x18ftyp":
extension = 'mp4'
elif filedataStr[0:12] == "b'\\x42\\x4d":
extension = 'bmp'
record.binary_filename = f"{record.binary_mediatype.filepartname}{record.binary_contract.contract_auto_id}.{extension}"
@api.model
def _default_uuid(self):
return str(uuid.uuid4())
@api.model
def _default_create_date(self):
return datetime.now().strftime('%Y-%m-%d %H:%M:%S')
@api.model
def _default_create_user(self):
return self.env.user.name if self.env.user else 'System'
@api.model
def _default_binary_id(self):
# Generate a unique binary ID based on the current timestamp and a random UUID
return f"Datei_{str(uuid.uuid4())[:8]}"
@api.onchange('binary_binary')
def _onchange_binary_binary(self):
"""Update binary size and type when binary content is changed."""
_logger.info("New Binary B_"+str(self.id))
existing_binary = False
if not self._origin.binary_binary:
existing_binary = False
else:
existing_binary = True
if not self.binary_contract.cloudlink:
raise ValidationError(_("Der Vertrag hat keinen Cloudlink. Bitte überprüfen Sie den Vertrag."))
cloudpath = str(self.binary_contract.cloudlink)
_logger.info("New Binary checking Cloudlink B_"+str(self.id)+" - "+str(cloudpath))
cloudpath = cloudpath + '/' + str(self.binary_mediatype.cloudlink)
client = Client("https://cloud.logumedia.de/remote.php/dav/files/OdooDav/", auth=("odooClient@logumedia.de", "lm2020#OdooDav"))
_logger.info("New Binary checking Cloudlink for cont : "+str(cloudpath))
cloudexists = False
if client.exists(cloudpath):
# Convert self.binary_binary to bytes-like object
if isinstance(self.binary_binary, str):
filedata = base64.b64decode(self.binary_binary.split("base64,")[-1])
else:
filedata = base64.b64decode(self.binary_binary)
filedataStr = str(filedata)
_logger.info("New Binary Cloudlink exists - Saving File - B_"+str(self.id)+" - "+str(cloudpath)+" - " + str(filedata)+' - ' + filedataStr[0:8])
try:
decoded_data = filedata
except base64.binascii.Error as e:
if 'Incorrect padding' in str(e):
filedataStr += b'=' * (4 - len(filedataStr) % 4)
decoded_data = base64.b64decode(filedataStr)
if filedataStr[0:10] == "b'\x89PNG":
extension = 'png'
else:
extension = 'jpg'
_logger.info("New Binary Cloudlink exists - Saving File - B_"+str(self.id)+" - "+str(cloudpath)+" - " + str(base64.b64encode(decoded_data)))
##infile = open('/tmp/'+filename, "w+b")
#infile = tempfile.NamedTemporaryFile(delete=False)
##infile.write(filedata)
##client.upload_fileobj(infile, f"{cloudpath}/{filename}")
##infile.close()
##infile.delete() # Delete the temporary file after upload
cloudexists = True
else:
_logger.info("New Binary Cloudlink does not exists B_"+str(self.id)+" - "+str(cloudpath))
raise ValidationError(_("Der Cloudlink im Vertrag ist nicht vorhanden. Bitte überprüfen Sie den Link ggf. anlegen."))
if cloudexists:
if self.binary_binary:
# Calculate the size of the binary content
self.binary_size = len(filedata)
# Determine the file type based on the binary content
self.binary_cloud_link = f"{cloudpath}/{self.binary_filename}"
self.binary_issaved = True
else:
self.binary_size = 0
self.binary_type = ''
self.binary_cloud_link = ''
self.binary_issaved = False
def dload(self):
"""Download the binary file."""
if not self.binary_binary:
raise ValidationError(_("No binary file to download."))
# Create a temporary file to store the downloaded content
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=self.binary_filename)
temp_file.write(base64.b64decode(self.binary_binary))
temp_file.close()
# Return the file for download
return {
'type': 'ir.actions.act_url',
'url': f'/web/content/{self.id}/{self.binary_filename}?download=true&filename={self.binary_filename}',
'target': 'self',
}
def dedit(self):
"""Edit the binary file."""
return {
'type': 'ir.actions.act_window',
'res_model': 'dss.binaries',
'view_mode': 'form',
'res_id': self.id,
'target': 'new',
}
def resend(self):
"""Resend the binary file to the contract's partner email."""
if not self.binary_contract:
raise ValidationError(_("No contract associated with this binary file."))
if not self.binary_contract.project.standortpartner:
raise ValidationError(_("No partner associated with the contract."))
if not self.binary_contract.project.standortpartner.email:
raise ValidationError(_("The partner does not have an email address."))
# Send email with the binary file as attachment
def genstamp(self):
_logger.info('Create Stamps for Binary: B_'+str(self.id)+' '+str(id))
# Bildparameter definieren
Medium = self.binary_mediatype
if Medium and Medium.can_gen_stamp and self.binary_contract:
_logger.info('Create Stamps for Contract : B_'+str(self.id)+' - Contract: C_'+str(self.binary_contract.id)+'- Medium : M_'+str(Medium.id)+' - '+str(Medium.medianame))
# Bildgröße und Hintergrundfarbe festlegen
background_color = (255, 255, 255) # Weißer Hintergrund (RGB)
# Neues Bild erstellen
maxh = Medium.maxsize_h
maxw = Medium.maxsize_w
image = Image.new("RGB", (maxw, maxh), background_color)
d = ImageDraw.Draw(image)
Zeilen = Medium.maxsize_h // 6
zposy = 2
if self.binary_contract.client:
text = self.binary_contract.contact_company_name
_logger.info('Create Stamps for Contract : B_'+str(self.id)+' - Contract: C_'+str(self.binary_contract.id)+'- Medium : M_'+str(Medium.id)+' - Text1: '+str(text))
if len(text) > 35:
# Trenne den Text an einem Leerzeichen nahe bei Position 40
idx = text.rfind(' ', 0, 35)
if idx == -1:
idx = 35
text2 = text[:idx]
bcount = len(text2) if text2 else 1
fonts = maxw // (bcount ) *1.8
if fonts > maxh:
fonts = maxh - 10
if fonts <=0 :
fonts = 10
# font = ImageFont.truetype("addons/web/static/fonts/sign/Zeyada-Regular.ttf", fonts)
font = ImageFont.truetype("addons/web/static/fonts/google/Open_Sans/Open_Sans-SemiBold.ttf", fonts)
d.text((maxw // 2, 1*Zeilen), text2, fill="black",align="center", anchor="ms", font=font) # Beispiel: Ein Rechteck
text = text[idx:]
zposy = 2
if len(text) > 35:
# Trenne den Text an einem Leerzeichen nahe bei Position 40
idx = text.rfind(' ', 0, 35)
if idx == -1:
idx = 35
text2 = text[:idx]
# font = ImageFont.truetype("addons/web/static/fonts/sign/Zeyada-Regular.ttf", fonts)
font = ImageFont.truetype("addons/web/static/fonts/google/Open_Sans/Open_Sans-SemiBold.ttf", fonts)
d.text((maxw // 2, zposy*Zeilen), text2, fill="black",align="center", anchor="ms", font=font) # Beispiel: Ein Rechteck
text = text[idx:]
zposy = 3
else:
bcount = len(text) if text else 1
fonts = maxw // (bcount ) *1.8
if fonts > maxh:
fonts = maxh - 10
if fonts <=0 :
fonts = 10
# font = ImageFont.truetype("addons/web/static/fonts/sign/Zeyada-Regular.ttf", fonts)
font = ImageFont.truetype("addons/web/static/fonts/google/Open_Sans/Open_Sans-SemiBold.ttf", fonts)
d.text((maxw // 2, zposy*Zeilen), text, fill="black",align="center", anchor="ms", font=font) # Beispiel: Ein Rechteck
text = self.binary_contract.contact_street
bcount = len(text) if text else 1
Zeilen = maxh // 6
fonts = maxw // (bcount ) *1.2
if fonts > maxh:
fonts = maxh - 10
if fonts <=0 :
fonts = 10
# font = ImageFont.truetype("addons/web/static/fonts/sign/Zeyada-Regular.ttf", fonts)
font = ImageFont.truetype("addons/web/static/fonts/google/Open_Sans/Open_Sans-Regular.ttf", fonts)
d.text((maxw // 2, 4*Zeilen), text, fill="black",align="center", anchor="ms", font=font) # Beispiel: Ein Rechteck
text = self.binary_contract.contact_zip+' '+self.binary_contract.contact_city
bcount = len(text) if text else 1
Zeilen = maxh // 6
fonts = maxw // (bcount ) *1.2
if fonts > maxh:
fonts = maxh - 10
if fonts <=0 :
fonts = 10
# font = ImageFont.truetype("addons/web/static/fonts/sign/Zeyada-Regular.ttf", fonts)
font = ImageFont.truetype("addons/web/static/fonts/google/Open_Sans/Open_Sans-Regular.ttf", fonts)
d.text((maxw // 2, 5*Zeilen), text, fill="black",align="center", anchor="ms", font=font) # Beispiel: Ein Rechteck
# Bild speichern
filename = Medium.filepartname+self.binary_contract.contract_auto_id+".png"
_logger.info('Create Stamps for Contract : B_'+str(self.id)+' - Contract: C_'+str(self.binary_contract.id)+'- Feld : F_'+str(Medium.id)+' - Filename: '+str(filename))
image.save(filename)
_logger.info("Stamp file created "+str(filename))
_logger.info("Copy file to Binaries B_"+str(self))
try:
self.binary_binary = base64.b64encode(open(filename, "rb").read())
self.binary_filename = filename
self.binary_type = 'image/png'
self.binary_size = os.path.getsize(filename)
self.binary_issaved = False
self.binary_secured_ro = False
self.binary_used_ro = False
finally:
_logger.info("Binary inserted "+str(self))
cloudpath = self.binary_contract.cloudlink
if not cloudpath:
raise ValidationError(_("Der Vertrag hat keinen Cloudlink. Bitte überprüfen Sie den Vertrag."))
if not Medium.cloudlink:
raise ValidationError(_("Der Mediatyp hat keinen Cloudlink. Bitte überprüfen Sie den Mediatyp."))
cloudpath = cloudpath + '/' + str(Medium.cloudlink)
_logger.info("Copy file to Cloud Path : "+str(cloudpath))
try:
client = Client("https://cloud.logumedia.de/remote.php/dav/files/OdooDav/", auth=("odooClient@logumedia.de", "lm2020#OdooDav"))
try:
#client.mkdir(new_folder)
#_logger.info("Make Cloud Path : "+str(new_folder))
client.upload_file(filename, f"{cloudpath}/{filename}")
except Exception as e:
_logger.info("Make Cloud Path error : "+str(e))
finally:
self.binary_issaved = True
_logger.info("File copied to Cloud Path : "+str(cloudpath))
return True

View File

@ -50,6 +50,7 @@ class dssadvertisefields(models.Model):
feldtemplates = fields.Many2many('dss.advertisefields.templates' , string='Vertrag', store=True,tracking=True)
fullsize_w = fields.Integer("Auflösung Breite",tracking=True)
fullsize_h = fields.Integer("Auflösung Höhe",tracking=True)
adportal_visible = fields.Boolean('Ad Portal sichtbar', default=False, tracking=True)
def _default_create_date(self):
return str(date.today())

0
models/dss_kileads.py Normal file → Executable file
View File

0
models/dss_kileads_requests.py Normal file → Executable file
View File

View File

@ -54,22 +54,26 @@ class dssmaintaintasks(models.Model):
maintain_travel_end = fields.Datetime('Ankunftszeit', tracking=True)
maintain_travel_distance = fields.Float('Anreise Entfernung', tracking=True)
maintain_travel_duration = fields.Float('Anreise Dauer', tracking=True, compute='_compute_travel_total_time', store=True)
maintain_travel_duration_man = fields.Float('Anreise Dauer', tracking=True, store=True)
maintain_travel_duration_text = fields.Char('Anreise Dauer', tracking=True, compute='_compute_travel_total_time_text', store=True)
maintain_work_start = fields.Datetime('Arbeitsbeginn', tracking=True)
maintain_work_end = fields.Datetime('Arbeitsende', tracking=True)
maintain_work_duration = fields.Float('Arbeits Dauer', tracking=True,compute='_compute_work_total_time', store=True)
maintain_work_duration_man = fields.Float('Arbeits Dauer', tracking=True, store=True)
maintain_work_duration_text = fields.Char('Arbeits Dauer', tracking=True, compute='_compute_work_total_time_text', store=True)
maintain_return_start = fields.Datetime('Rückreisebeginn', tracking=True)
maintain_return_end = fields.Datetime('Rückreiseende', tracking=True)
maintain_return_duration = fields.Float('Rückreise Dauer', tracking=True,compute='_compute_return_total_time', store=True)
maintain_return_duration_man = fields.Float('Rückreise Dauer', tracking=True, store=True)
maintain_return_distance = fields.Float('Rückreise Entfernung', tracking=True)
maintain_return_duration_text = fields.Char('Rückreise Dauer', tracking=True, compute='_compute_return_total_time_text', store=True)
maintain_total_start = fields.Datetime('Einsatzbeginn', tracking=True,compute='_compute_maintain_total_start', store=True)
maintain_total_end = fields.Datetime('Einsatzende', tracking=True, compute='_compute_maintain_total_end', store=True)
maintain_total_duration = fields.Float('Einsatz Dauer', tracking=True,compute='_compute_maintain_total_time', store=True)
maintain_total_duration_man = fields.Float('Einsatz Dauer', tracking=True, store=True)
maintain_total_duration_text = fields.Char('Einsatz Dauer', tracking=True, compute='_compute_maintain_total_time_text', store=True)
maintain_total_duration_money = fields.Float('Einsatz Kosten', tracking=True, compute='_compute_maintain_total_cost', store=True)
@ -94,33 +98,43 @@ class dssmaintaintasks(models.Model):
def _compute_travel_total_time_text(self):
for record in self:
if record.maintain_travel_duration:
hours = int(record.maintain_travel_duration)
minutes = int((record.maintain_travel_duration - hours) * 60)
record.maintain_travel_duration_text = f"{hours} Stunden {minutes} Minuten"
if record.maintain_travel_duration:
hours = int(record.maintain_travel_duration)
minutes = int((record.maintain_travel_duration - hours) * 60)
record.maintain_travel_duration_text = f"{hours} Stunden {minutes} Minuten"
else:
record.maintain_travel_duration_text = "0 Stunden 0 Minuten"
else:
record.maintain_travel_duration_text = "0 Stunden 0 Minuten"
record.maintain_travel_duration_text = ""
@api.depends('maintain_work_duration')
@api.onchange('maintain_work_duration')
def _compute_work_total_time_text(self):
for record in self:
if record.maintain_work_duration:
hours = int(record.maintain_work_duration)
minutes = int((record.maintain_work_duration - hours) * 60)
record.maintain_work_duration_text = f"{hours} Stunden {minutes} Minuten"
if not record.maintain_work_duration_man:
if record.maintain_work_duration:
hours = int(record.maintain_work_duration)
minutes = int((record.maintain_work_duration - hours) * 60)
record.maintain_work_duration_text = f"{hours} Stunden {minutes} Minuten"
else:
record.maintain_work_duration_text = "0 Stunden 0 Minuten"
else:
record.maintain_work_duration_text = "0 Stunden 0 Minuten"
record.maintain_work_duration_text = ""
@api.depends('maintain_return_duration')
@api.onchange('maintain_return_duration')
def _compute_return_total_time_text(self):
for record in self:
if record.maintain_return_duration:
hours = int(record.maintain_return_duration)
minutes = int((record.maintain_return_duration - hours) * 60)
record.maintain_return_duration_text = f"{hours} Stunden {minutes} Minuten"
if not record.maintain_return_duration_man:
if record.maintain_return_duration:
hours = int(record.maintain_return_duration)
minutes = int((record.maintain_return_duration - hours) * 60)
record.maintain_return_duration_text = f"{hours} Stunden {minutes} Minuten"
else:
record.maintain_return_duration_text = "0 Stunden 0 Minuten"
else:
record.maintain_return_duration_text = "0 Stunden 0 Minuten"
record.maintain_return_duration_text = ""
@api.depends('maintain_total_duration')
@api.onchange('maintain_total_duration')
@ -183,15 +197,21 @@ class dssmaintaintasks(models.Model):
record.maintain_total_end = False
@api.depends('maintain_travel_duration', 'maintain_work_duration', 'maintain_return_duration')
@api.onchange('maintain_travel_duration', 'maintain_work_duration', 'maintain_return_duration')
@api.onchange('maintain_travel_duration', 'maintain_work_duration', 'maintain_return_duration','maintain_travel_duration_man', 'maintain_work_duration_man', 'maintain_return_duration_man')
def _compute_maintain_total_time(self):
for record in self:
total_duration = 0.0
if record.maintain_travel_duration:
if record.maintain_travel_duration_man:
total_duration += record.maintain_travel_duration_man
elif record.maintain_travel_duration:
total_duration += record.maintain_travel_duration
if record.maintain_work_duration:
if record.maintain_work_duration_man:
total_duration += record.maintain_work_duration_man
elif record.maintain_work_duration:
total_duration += record.maintain_work_duration
if record.maintain_return_duration:
if record.maintain_return_duration_man:
total_duration += record.maintain_return_duration_man
elif record.maintain_return_duration:
total_duration += record.maintain_return_duration
record.maintain_total_duration = total_duration

472
models/dss_phonecenter_query.py Executable file
View File

@ -0,0 +1,472 @@
# -*- coding: utf-8 -*-
from datetime import timedelta
from odoo import api, fields, models
class DssPhonecenterQuery(models.Model):
_name = "dss.phonecenter.query"
_description = "Telefoniecenter Suchanfrage"
_order = "id desc"
name = fields.Char("Anfrage", required=True)
project_id = fields.Many2one("dss.projects", string="Projekt", required=True, index=True)
user_id = fields.Many2one("res.users", string="Benutzer", required=True, default=lambda self: self.env.user)
location_query = fields.Char("Ort", required=True)
radius_km = fields.Integer("Umkreis (km)", default=20)
industry_query = fields.Char("Branche", required=True)
state = fields.Selection([
("draft", "Neu"),
("active", "Aktiv"),
("done", "Abgeschlossen"),
], default="draft", string="Status")
search_count = fields.Integer("Suchen", default=0)
line_ids = fields.One2many("dss.phonecenter.query.line", "query_id", string="Ergebnisse")
open_line_count = fields.Integer("Offene Eintraege", compute="_compute_line_counts")
done_line_count = fields.Integer("Abarbeitet", compute="_compute_line_counts")
@api.depends("line_ids.processed", "line_ids.rejected", "line_ids.reschedule_at")
def _compute_line_counts(self):
now = fields.Datetime.now()
for rec in self:
total = len(rec.line_ids)
done = len(rec.line_ids.filtered(
lambda line: line.processed or line.rejected or (
line.reschedule_at and line.reschedule_at > now and not line.rejected and not line.processed
)
))
rec.done_line_count = done
rec.open_line_count = max(0, total - done)
@api.model
def get_project_queries(self, project_id):
project_id = int(project_id or 0)
if not project_id:
return []
records = self.search([
("project_id", "=", project_id),
("user_id", "=", self.env.user.id),
], order="id desc", limit=100)
payload = []
for rec in records:
payload.append({
"id": rec.id,
"name": rec.name,
"location_query": rec.location_query or "",
"radius_km": rec.radius_km or 20,
"industry_query": rec.industry_query or "",
"state": rec.state,
"search_count": rec.search_count,
"open_line_count": rec.open_line_count,
"done_line_count": rec.done_line_count,
"can_repeat": (rec.open_line_count == 0),
})
now = fields.Datetime.now()
reschedule_lines = self.env["dss.phonecenter.query.line"].search([
("query_id.project_id", "=", project_id),
("query_id.user_id", "=", self.env.user.id),
("reschedule_at", "!=", False),
("rejected", "=", False),
("processed", "=", False),
])
if reschedule_lines:
due_count = len(reschedule_lines.filtered(lambda l: l.reschedule_at and l.reschedule_at <= now))
total_count = len(reschedule_lines)
payload.append({
"id": "reschedule:%s" % project_id,
"name": "Wiedervorlagen",
"location_query": "",
"radius_km": 0,
"industry_query": "",
"state": "active",
"search_count": 0,
"open_line_count": due_count,
"done_line_count": max(0, total_count - due_count),
"can_repeat": True,
"is_reschedule_bucket": True,
})
return payload
@api.model
def get_reschedule_details(self, project_id):
project_id = int(project_id or 0)
if not project_id:
return {"ok": False, "message": "Projekt fehlt."}
now = fields.Datetime.now()
lines = self.env["dss.phonecenter.query.line"].search([
("query_id.project_id", "=", project_id),
("query_id.user_id", "=", self.env.user.id),
("reschedule_at", "!=", False),
("rejected", "=", False),
("processed", "=", False),
], order="reschedule_at asc, id desc")
due_lines = lines.filtered(lambda l: l.reschedule_at and l.reschedule_at <= now)
companies = []
for line in due_lines:
companies.append({
"id": str(line.external_id or line.id),
"line_id": line.id,
"name": line.name,
"address": line.address or "",
"city": line.city or "",
"website": line.website or "",
"phone": line.phone or "",
"email": line.email or "",
"rating": line.rating or 0,
"source": line.source or "",
"verified": bool(line.verified),
"verified_at": line.verified_at or "",
"place_id": line.place_id or "",
"google_maps_url": line.google_maps_url or "",
"lat": line.lat,
"lng": line.lng,
"types": line.types_json or [],
"processed": bool(line.processed),
"rejected": bool(line.rejected),
"reschedule_at": fields.Datetime.to_string(line.reschedule_at) if line.reschedule_at else "",
"reschedule_note": line.reschedule_note or "",
})
return {
"ok": True,
"query": {
"id": "reschedule:%s" % project_id,
"name": "Wiedervorlagen",
"location_query": "",
"radius_km": 0,
"industry_query": "",
"state": "active",
"search_count": 0,
"open_line_count": len(companies),
"done_line_count": max(0, len(lines) - len(companies)),
"can_repeat": True,
"is_reschedule_bucket": True,
},
"companies": companies,
}
@api.model
def create_query(self, payload):
payload = payload or {}
project_id = int(payload.get("project_id") or 0)
if not project_id:
return {"ok": False, "message": "Projekt fehlt."}
location_query = (payload.get("location_query") or "").strip()
industry_query = (payload.get("industry_query") or "").strip()
radius_km = int(payload.get("radius_km") or 20)
if not location_query or not industry_query:
return {"ok": False, "message": "Ort und Branche sind erforderlich."}
record = self.create({
"project_id": project_id,
"user_id": self.env.user.id,
"location_query": location_query,
"industry_query": industry_query,
"radius_km": radius_km,
"name": "%s | %s | %s km" % (industry_query, location_query, radius_km),
"state": "active",
})
return {
"ok": True,
"query": {
"id": record.id,
"name": record.name,
"location_query": record.location_query,
"radius_km": record.radius_km,
"industry_query": record.industry_query,
"state": record.state,
"search_count": record.search_count,
"open_line_count": record.open_line_count,
"done_line_count": record.done_line_count,
"can_repeat": (record.open_line_count == 0),
},
}
@api.model
def get_query_details(self, query_id):
query_id = int(query_id or 0)
record = self.browse(query_id)
if not record.exists() or record.user_id.id != self.env.user.id:
return {"ok": False, "message": "Anfrage nicht gefunden."}
now = fields.Datetime.now()
companies = []
visible_lines = record.line_ids.filtered(
lambda l: not l.rejected and not (
l.reschedule_at and l.reschedule_at > now and not l.processed
)
)
for line in visible_lines.sorted(key=lambda l: (l.processed, -l.id)):
companies.append({
"id": str(line.external_id or line.id),
"line_id": line.id,
"name": line.name,
"address": line.address or "",
"city": line.city or "",
"website": line.website or "",
"phone": line.phone or "",
"email": line.email or "",
"rating": line.rating or 0,
"source": line.source or "",
"verified": bool(line.verified),
"verified_at": line.verified_at or "",
"place_id": line.place_id or "",
"google_maps_url": line.google_maps_url or "",
"lat": line.lat,
"lng": line.lng,
"types": line.types_json or [],
"processed": bool(line.processed),
"rejected": bool(line.rejected),
"reschedule_at": fields.Datetime.to_string(line.reschedule_at) if line.reschedule_at else "",
"reschedule_note": line.reschedule_note or "",
})
return {
"ok": True,
"query": {
"id": record.id,
"name": record.name,
"location_query": record.location_query or "",
"radius_km": record.radius_km or 20,
"industry_query": record.industry_query or "",
"state": record.state,
"search_count": record.search_count,
"open_line_count": record.open_line_count,
"done_line_count": record.done_line_count,
"can_repeat": (record.open_line_count == 0),
},
"companies": companies,
}
@api.model
def can_repeat_query(self, query_id):
query_id = int(query_id or 0)
record = self.browse(query_id)
if not record.exists() or record.user_id.id != self.env.user.id:
return {"ok": False, "can_repeat": False}
return {
"ok": True,
"can_repeat": bool(record.open_line_count == 0),
"open_line_count": record.open_line_count,
}
@api.model
def save_query_results(self, query_id, companies):
query_id = int(query_id or 0)
record = self.browse(query_id)
if not record.exists() or record.user_id.id != self.env.user.id:
return {"ok": False, "message": "Anfrage nicht gefunden."}
if not isinstance(companies, list):
companies = []
line_model = self.env["dss.phonecenter.query.line"]
added = 0
for company in companies:
if not isinstance(company, dict):
continue
name = (company.get("name") or "").strip()
if not name:
continue
external_id = str(company.get("id") or company.get("place_id") or name)
existing = line_model.search([
("query_id", "=", record.id),
("external_id", "=", external_id),
], limit=1)
vals = {
"query_id": record.id,
"external_id": external_id,
"place_id": (company.get("place_id") or "").strip(),
"name": name,
"address": (company.get("address") or "").strip(),
"city": (company.get("city") or "").strip(),
"website": (company.get("website") or "").strip(),
"phone": (company.get("phone") or "").strip(),
"email": (company.get("email") or "").strip(),
"rating": company.get("rating") or 0,
"source": (company.get("source") or "").strip(),
"verified": bool(company.get("verified", False)),
"verified_at": (company.get("verified_at") or "").strip(),
"google_maps_url": (company.get("google_maps_url") or "").strip(),
"lat": company.get("lat"),
"lng": company.get("lng"),
"types_json": company.get("types") if isinstance(company.get("types"), list) else [],
"processed": bool(existing.processed) if existing else False,
"rejected": bool(existing.rejected) if existing else False,
"reject_reason": existing.reject_reason if existing else "",
"decision_summary": existing.decision_summary if existing else "",
"reschedule_at": existing.reschedule_at if existing else False,
"reschedule_note": existing.reschedule_note if existing else "",
}
if existing:
existing.write(vals)
else:
line_model.create(vals)
added += 1
record.search_count = (record.search_count or 0) + 1
record.state = "active"
details = self.get_query_details(record.id)
details.update({"ok": True, "added": added})
return details
@api.model
def mark_result_processed(self, line_id, processed=True):
line_id = int(line_id or 0)
line = self.env["dss.phonecenter.query.line"].browse(line_id)
if not line.exists() or line.query_id.user_id.id != self.env.user.id:
return {"ok": False, "message": "Eintrag nicht gefunden."}
line.processed = bool(processed)
if processed:
line.rejected = False
line.reschedule_at = False
line.reschedule_note = ""
if line.query_id.open_line_count == 0:
line.query_id.state = "done"
else:
line.query_id.state = "active"
return {
"ok": True,
"query_id": line.query_id.id,
"open_line_count": line.query_id.open_line_count,
"can_repeat": (line.query_id.open_line_count == 0),
}
@api.model
def set_reschedule(self, line_id, reschedule_at=False, days=0, note=""):
line_id = int(line_id or 0)
line = self.env["dss.phonecenter.query.line"].browse(line_id)
if not line.exists() or line.query_id.user_id.id != self.env.user.id:
return {"ok": False, "message": "Eintrag nicht gefunden."}
target_dt = False
if reschedule_at:
target_dt = fields.Datetime.to_datetime(reschedule_at)
if not target_dt:
day_count = int(days or 0)
if day_count > 0:
target_dt = fields.Datetime.now() + timedelta(days=day_count)
if not target_dt:
return {"ok": False, "message": "Bitte Wiedervorlagezeit angeben."}
line.write({
"last_decision": "reschedule",
"processed": False,
"rejected": False,
"reschedule_at": target_dt,
"reschedule_note": (note or "").strip(),
})
if line.query_id.open_line_count == 0:
line.query_id.state = "done"
else:
line.query_id.state = "active"
return {
"ok": True,
"line_id": line.id,
"query_id": line.query_id.id,
"reschedule_at": fields.Datetime.to_string(line.reschedule_at) if line.reschedule_at else "",
"open_line_count": line.query_id.open_line_count,
}
@api.model
def apply_contact_decision(self, line_id, decision, reason="", summary=""):
line_id = int(line_id or 0)
line = self.env["dss.phonecenter.query.line"].browse(line_id)
if not line.exists() or line.query_id.user_id.id != self.env.user.id:
return {"ok": False, "message": "Eintrag nicht gefunden."}
decision_value = (decision or "").strip()
# Backward-compatible alias: treat no_interest as a rejection/discard decision.
if decision_value == "no_interest":
decision_value = "discard"
vals = {
"last_decision": decision_value,
"reject_reason": (reason or "").strip(),
"decision_summary": (summary or "").strip(),
}
if decision_value == "discard":
vals.update({
"rejected": True,
"processed": True,
"reschedule_at": False,
"reschedule_note": "",
})
elif decision_value in ("research", "reschedule", "keep"):
vals.update({
"rejected": False,
"processed": False,
})
if decision_value in ("research", "keep"):
vals.update({
"reschedule_at": False,
"reschedule_note": "",
})
line.write(vals)
if line.query_id.open_line_count == 0:
line.query_id.state = "done"
else:
line.query_id.state = "active"
return {
"ok": True,
"line_id": line.id,
"query_id": line.query_id.id,
"decision": decision_value,
"rejected": bool(line.rejected),
"open_line_count": line.query_id.open_line_count,
}
class DssPhonecenterQueryLine(models.Model):
_name = "dss.phonecenter.query.line"
_description = "Telefoniecenter Suchergebnis"
_order = "id desc"
query_id = fields.Many2one("dss.phonecenter.query", string="Anfrage", required=True, ondelete="cascade", index=True)
external_id = fields.Char("Externe ID", index=True)
place_id = fields.Char("Place ID")
name = fields.Char("Unternehmen", required=True)
address = fields.Char("Adresse")
city = fields.Char("Ort")
website = fields.Char("Webseite")
phone = fields.Char("Telefon")
email = fields.Char("Email")
rating = fields.Float("Rating")
source = fields.Char("Quelle")
verified = fields.Boolean("Verifiziert")
verified_at = fields.Char("Verifiziert am")
google_maps_url = fields.Char("Google Maps URL")
lat = fields.Float("Breitengrad")
lng = fields.Float("Laengengrad")
types_json = fields.Json("Typen")
processed = fields.Boolean("Abarbeitet", default=False)
rejected = fields.Boolean("Abgelehnt", default=False)
reject_reason = fields.Text("Ablehnungsgrund")
decision_summary = fields.Text("Entscheidungszusammenfassung")
last_decision = fields.Selection([
("reschedule", "Wiedervorlage"),
("discard", "Komplett verwerfen"),
("no_interest", "Kein Interesse"),
("research", "Weitere Recherchen"),
("keep", "Zunaechst behalten"),
], string="Letzte Entscheidung")
reschedule_at = fields.Datetime("Wiedervorlage am")
reschedule_note = fields.Text("Wiedervorlage Notiz")

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from odoo import fields, models
class DssPortalUserProfile(models.Model):
_name = 'dss.portal.user.profile'
_description = 'AD Portal User Profile'
user_id = fields.Many2one('res.users', required=True, ondelete='cascade', index=True)
ranking_enabled = fields.Boolean('Im Ranking', default=False)
ranking_position = fields.Integer('Ranking Position', default=0)
ranking_sum = fields.Float('Ranking Summe', default=0.0)
right_contract_overview = fields.Boolean('Portal Recht: Vertragsanzeige', default=True)
right_contract_detail = fields.Boolean('Portal Recht: Vertragsdetails', default=True)
right_media_view = fields.Boolean('Portal Recht: Medienanzeige', default=True)
right_ranking_view = fields.Boolean('Portal Recht: Rankinganzeige', default=True)
right_graphic_examples = fields.Boolean('Portal Recht: Grafikbeispiele anzeigen/erstellen', default=False)
_sql_constraints = [
('dss_portal_user_profile_user_unique', 'unique(user_id)', 'Fuer jeden Benutzer ist nur ein Portal-Profil erlaubt.'),
]

View File

@ -78,6 +78,7 @@ class dssprojects(models.Model):
grundsystemicon_different = fields.Boolean('Grundsystem Icon anders',default=False,tracking=True)
grundsystemicon_different_image = fields.Binary('Anderes Icon',tracking=True)
grundsystem_webinput_template = fields.Char(related='grundsystemname.web_input_template',tracking=True)
grundsystem_webinput_showimage_url = fields.Char(related='grundsystemname.web_input_showimage_url',tracking=True)
btntemplate = fields.Many2one('dss.display.templates',string='Buttonsvorlage',tracking=True)
@ -168,9 +169,45 @@ class dssprojects(models.Model):
lastkdnr = fields.Integer('Letzte Temp. KdNr', tracking=True)
lastrealkdnr = fields.Integer('Letzte Real. KdNr', tracking=True)
reservedkdnr = fields.Char('Reservierte KdNr', tracking=True)
lostkdnr = fields.Char('Verlorene KdNr "," Seperated', tracking=True)
web_input_template = fields.Char('Web-Eingabe Template', tracking=True)
project_binaries = fields.Many2many('dss.binaries', string='Projektdateien', tracking=True)
contract_finish_img_cloud = fields.Char('Überg. Bildlink',tracking=True)
def pyopen_dss_project_add_binary_form_view(self):
_logger.info('Open Add Binary Form')
view = self.env.ref("DigitalSignage.dss_project_binaries_view_form_wizard")
_logger.info('Open Wizzard Binary Form '+str(view))
return {
'type': 'ir.actions.act_window',
'view_mode': 'form' ,
'view_type': 'form' ,
'view_id': view.id,
'res_model': 'dss.binaries.wizard.new' ,
'target': 'new' ,
'views':[(view.id,'form')],
'context': {'default_binary_project':self.id},
}
def pyopen_dss_project_add_binary_form_view_from_wizzard(self,media):
_logger.info('Open Add Binary Form')
view = self.env.ref("DigitalSignage.dss_binaries_view_form")
binary = self.env['dss.binaries'].create({'binary_project':self.id,'binary_name':self.projektname,'binary_mediatype':media.id,'binary_id':str(media.filepartname+'_'+str(self.projectid)),'binary_binary':media.standard_image})
self.project_binaries = [(4, binary.id)]
_logger.info('Open Add Binary Form '+str(view)+' B_'+str(binary))
return {
'type': 'ir.actions.act_window',
'view_mode': 'form' ,
'view_type': 'form' ,
'view_id': view.id,
'res_model': 'dss.binaries' ,
'target': 'new' ,
'views':[(view.id,'form')],
'res_id':binary.id,
}
@api.model
def _read_group_system_ids(self, stages, domain, order):
project_system_ids = self.env['dss.systemtypen'].search([('open_close_standard_state','=',False)], order='order')

View File

@ -1,11 +1,6 @@
# -*- coding: utf-8 -*-
import uuid
import logging
from odoo import api,fields, models
_logger = logging.getLogger(__name__)
from odoo import api, fields, models
class Resusers(models.Model):
@ -14,5 +9,111 @@ class Resusers(models.Model):
# company_type = fields.Selection(selection_add=[('dss_client','Werbekunde'),('dss_partner','Partnerunternehmen')])
dss_projects = fields.Many2many('dss.projects',string='DigitalSignage Projekte')
phonecenter_view_mode = fields.Selection([
('callcenter', 'Callcenter'),
('projectmanagement', 'Projektmanagement'),
('aussendienst', 'Aussendienst'),
('administrator', 'Administrator'),
], string='Ansichtsmodus', default='callcenter')
portal_ranking_enabled = fields.Boolean('Im Ranking', compute='_compute_portal_profile_flags', inverse='_inverse_portal_ranking_enabled')
portal_ranking_position = fields.Integer('Ranking Position', compute='_compute_portal_profile_flags', inverse='_inverse_portal_ranking_position')
portal_ranking_sum = fields.Float('Ranking Summe', compute='_compute_portal_profile_flags', inverse='_inverse_portal_ranking_sum')
portal_right_contract_overview = fields.Boolean('Portal Recht: Vertragsanzeige', compute='_compute_portal_profile_flags', inverse='_inverse_portal_right_contract_overview')
portal_right_contract_detail = fields.Boolean('Portal Recht: Vertragsdetails', compute='_compute_portal_profile_flags', inverse='_inverse_portal_right_contract_detail')
portal_right_media_view = fields.Boolean('Portal Recht: Medienanzeige', compute='_compute_portal_profile_flags', inverse='_inverse_portal_right_media_view')
portal_right_ranking_view = fields.Boolean('Portal Recht: Rankinganzeige', compute='_compute_portal_profile_flags', inverse='_inverse_portal_right_ranking_view')
portal_right_graphic_examples = fields.Boolean('Portal Recht: Grafikbeispiele anzeigen/erstellen', compute='_compute_portal_profile_flags', inverse='_inverse_portal_right_graphic_examples')
show_graphic_examples = fields.Boolean('Grafikbeispiele anzeigen/erstellen', compute='_compute_portal_profile_flags', inverse='_inverse_show_graphic_examples')
def _get_or_create_portal_profile(self):
self.ensure_one()
profile_model = self.env['dss.portal.user.profile'].sudo()
profile = profile_model.search([('user_id', '=', self.id)], limit=1)
if profile:
return profile
return profile_model.create({
'user_id': self.id,
'ranking_enabled': False,
'ranking_position': 0,
'ranking_sum': 0.0,
'right_contract_overview': True,
'right_contract_detail': True,
'right_media_view': True,
'right_ranking_view': True,
'right_graphic_examples': False,
})
def _set_portal_profile_value(self, field_name, value):
for user in self:
profile = user._get_or_create_portal_profile()
profile.write({field_name: value})
def _compute_portal_profile_flags(self):
profile_model = self.env['dss.portal.user.profile'].sudo()
profiles = profile_model.search([('user_id', 'in', self.ids)])
profile_by_user = {profile.user_id.id: profile for profile in profiles}
for user in self:
profile = profile_by_user.get(user.id)
user.portal_ranking_enabled = bool(profile.ranking_enabled) if profile else False
user.portal_ranking_position = int(profile.ranking_position or 0) if profile else 0
user.portal_ranking_sum = float(profile.ranking_sum or 0.0) if profile else 0.0
user.portal_right_contract_overview = bool(profile.right_contract_overview) if profile else True
user.portal_right_contract_detail = bool(profile.right_contract_detail) if profile else True
user.portal_right_media_view = bool(profile.right_media_view) if profile else True
user.portal_right_ranking_view = bool(profile.right_ranking_view) if profile else True
user.portal_right_graphic_examples = bool(profile.right_graphic_examples) if profile else False
user.show_graphic_examples = bool(profile.right_graphic_examples) if profile else False
def _inverse_portal_ranking_enabled(self):
for user in self:
user._set_portal_profile_value('ranking_enabled', bool(user.portal_ranking_enabled))
def _inverse_portal_ranking_position(self):
for user in self:
user._set_portal_profile_value('ranking_position', int(user.portal_ranking_position or 0))
def _inverse_portal_ranking_sum(self):
for user in self:
user._set_portal_profile_value('ranking_sum', float(user.portal_ranking_sum or 0.0))
def _inverse_portal_right_contract_overview(self):
for user in self:
user._set_portal_profile_value('right_contract_overview', bool(user.portal_right_contract_overview))
def _inverse_portal_right_contract_detail(self):
for user in self:
user._set_portal_profile_value('right_contract_detail', bool(user.portal_right_contract_detail))
def _inverse_portal_right_media_view(self):
for user in self:
user._set_portal_profile_value('right_media_view', bool(user.portal_right_media_view))
def _inverse_portal_right_ranking_view(self):
for user in self:
user._set_portal_profile_value('right_ranking_view', bool(user.portal_right_ranking_view))
def _inverse_portal_right_graphic_examples(self):
for user in self:
user._set_portal_profile_value('right_graphic_examples', bool(user.portal_right_graphic_examples))
def _inverse_show_graphic_examples(self):
for user in self:
user._set_portal_profile_value('right_graphic_examples', bool(user.show_graphic_examples))
@api.model
def get_phone_center_profile(self):
user = self.env.user
mode = user.phonecenter_view_mode or 'callcenter'
can_see_all_projects = mode in ('projectmanagement', 'administrator')
return {
'user_id': user.id,
'view_mode': mode,
'can_see_all_projects': can_see_all_projects,
'assigned_project_ids': user.dss_projects.ids,
}
# dss_uuid = fields.Char('uuid')

View File

@ -10,6 +10,7 @@ import tempfile
import easywebdav
import os
import os.path
import requests
from odoo import api, fields, models, _
@ -74,6 +75,26 @@ class dssSettings(models.Model):
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)
@ -84,11 +105,126 @@ class dssSettings(models.Model):
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)
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]
@ -230,6 +366,652 @@ class dssSettings(models.Model):
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):

View File

@ -55,6 +55,7 @@ class dsssystemtypen(models.Model):
showonlinestate_type = fields.Selection([('RPORT','RPort Status'),('VNNOX','Vnnox System')])
inputform_type = fields.Char('Eingabeformular Typ', tracking=True)
web_input_template = fields.Char('Web-Eingabe Template', tracking=True)
web_input_showimage_url = fields.Char('Web-Eingabe Bild URL', tracking=True)

View File

@ -20,7 +20,7 @@ TUYA_LOGGER.setLevel(logging.ERROR)
logging.setLoggerClass(OdooCustomLogger)
_logger = logging.getLogger(__name__)
#_logger.setloging(False)
_logger.setloging(False)
##_logger = logging.getLogger(__name__)
@ -291,7 +291,7 @@ class trigger(models.Model):
if mailcc:
_logger.info('Trigger Aktion TRA_' + str(self)+' Sending Email - vor CC Add deep')
VsPartner = self.env['res.partner'].search([('id','=',Dataset.contract_writer.id)])
ccmailto = ccmailto+';'+VsPartner.email
ccmailto = ccmailto+', '+VsPartner.email
_logger.info('Trigger Aktion TRA_' + str(self)+' Sending Email CC Writer - FIX Empfänger : '+str(VsPartner)+' -> '+str(ccmailto)+' + '+str(VsPartner.email))
_logger.info('Trigger Aktion TRA_' + str(self)+' Sending Email - Template : '+str(email_template)+" Empfänger :"+str(mailto)+'/'+str(ccmailto)+' -> '+str(Dataset))
email_template.email_to = mailto
@ -327,7 +327,7 @@ class trigger(models.Model):
return True
class dsstriggerconditions(models.Model):
_name = "dss.triggerconditions"
_description = "DigitalSignage Trigger Bedingungen"
@ -752,6 +752,7 @@ class dsstriggertypes(models.Model):
trigger_block_config = fields.Char('Blockly Design',tracking=True)
trigger_block_raw_config = fields.Char('Blockly command Design',tracking=True)
trigger_block_command_uptodate = fields.Boolean('Blockly command aktuell',tracking=True)
trigger_auto_code_create = fields.Boolean('Autom. Cod erstellen',tracking=True, default=False)
trigger_block_command = fields.Char('Blockly command',tracking=True)
@api.model
@ -860,11 +861,11 @@ class dsstriggertypes(models.Model):
@api.model
def NewBlockTrigger(self,triggername,triggeraktiv):
_logger.info("Trigger New Block Trigger !"+str(triggername))
newtrigger = self.env['dss.triggertypes'].create({'triggername':triggername,'trigger_is_block_config':True,'trigger_active':triggeraktiv})
newtrigger = self.env['dss.triggertypes'].create({'triggername':triggername,'trigger_is_block_config':True,'trigger_active':triggeraktiv,'trigger_auto_code_create':False})
return newtrigger
@api.model
def UpdateBlockTrigger(self,triggername,blockcode,blockcommand,blockcommand_uptodate,triggertable,triggeraktiv ):
def UpdateBlockTrigger(self,triggername,blockcode,blockcommand,blockcommand_uptodate,triggertable,triggeraktiv,triggerautocode=False):
_logger.info("Trigger Update Block Trigger !"+str(triggername))
foundtrigger = self.env['dss.triggertypes'].search([('triggername','=',triggername)])
if foundtrigger:
@ -875,6 +876,7 @@ class dsstriggertypes(models.Model):
foundtrigger.trigger_block_command_uptodate=blockcommand_uptodate
foundtrigger.trigger_block_command=blockcommand
foundtrigger.trigger_table = triggertable_
foundtrigger.trigger_auto_code_create = bool(triggerautocode)
return foundtrigger
@api.model
@ -939,7 +941,10 @@ class dsstriggeractions(models.Model):
email_has_attachments = fields.Boolean('Email mit Anhang',tracking=True,help='Soll die Email mit Anhang versendet werden ?')
email_contract_attachment_types = fields.Many2many('dss.mediatypes',tracking=True,help='Welche Dateitypen aus dem Vertrag sollen als Anhang versendet werden ?')
activity_user_fix = fields.Boolean('Empfänger der Aufgabe ist fix',tracking=True)
activity_user = fields.Many2one('res.users','Empfänger der Aufgabe',tracking=True)
activity_user_field = fields.Many2one('ir.model.fields', string='Empfänger der Aufgabe',tracking=True)
activity_type = fields.Many2one('mail.activity.type','Art der Aufgabe',tracking=True)
activity_date_type = fields.Selection([('TOPLU','Von heute in x Tagen'),('TOFIX','An einem festen Datum'),('TOFIXDB','An einem festen Datum der Datenbank'),('TOPLUDB','An einem festen Datum der Datenbank + x Tage')],tracking=True)
activity_date_type_add_days = fields.Integer('x Tage Aufschlag',tracking=True)
@ -1010,7 +1015,7 @@ class dsstriggeractions(models.Model):
if self.email_getfrom == "FIX":
mailto = ""
for partner in self.email_fix_email:
mailto = mailto+';'+partner.email
mailto = mailto+','+partner.email
_logger.info('Trigger Aktion TRA_' + str(self)+' Sending Email - FIX Empfänger : '+str(self.email_getfrom)+' -> '+str(mailto))
elif self.email_getfrom == "DATA":
mailto = str(Dataset[self.email_data_field.name])
@ -1090,8 +1095,11 @@ class dsstriggeractions(models.Model):
elif self.activity_date_type == 'TOFIXDB':
deadline = Dataset[self.action_field.name]
elif self.activity_date_type == 'TOPLUDB':
deadline = Dataset[self.action_field.name]
deadline = deadline + relativedelta(days=self.activity_date_type_add_days)
base_deadline = Dataset[self.action_field.name]
if not base_deadline:
# Fallback avoids bool + relativedelta crash when source field is empty.
base_deadline = datetime.now()
deadline = base_deadline + relativedelta(days=self.activity_date_type_add_days)
if not Dataset:
_logger.info('Trigger Aktion TRA_' + str(self)+' Create Task - No Dataset - use Test '+str(self.trigger_table)+' '+str(self.action_test_id))
@ -1102,11 +1110,26 @@ class dsstriggeractions(models.Model):
_logger.info('Trigger Aktion TRA_' + str(self)+' Create Task - No Model '+str(ds)+' '+str(self.trigger_table))
else:
_logger.info('Trigger Aktion TRA_' + str(self)+' Create Task '+str(self.Info_Subject)+' ('+str(self.activity_user)+') - Model : '+str(self.trigger_table.id)+'/'+str(ds.id)+' '+str(deadline))
if self.activity_user_fix:
_logger.info('Trigger Aktion TRA_' + str(self)+' Create Task '+str(self.Info_Subject)+' - User FIX ('+str(self.activity_user)+')')
userid = self.activity_user.id
else:
_logger.info('Trigger Aktion TRA_' + str(self)+' Create Task '+str(self.Info_Subject)+' - User current ('+str(self.env.user.name)+') from field '+str(self.activity_user_field.name)+' -> '+str(Dataset[self.activity_user_field.name]))
partner = self.env['res.partner'].search([('id','=',Dataset[self.activity_user_field.name].id)],limit=1)
_logger.info('Trigger Aktion TRA_' + str(self)+' Create Task '+str(self.Info_Subject)+' - User current ('+str(self.env.user.name)+') from field '+str(self.activity_user_field.name)+' -> '+str(Dataset[self.activity_user_field.name])+' -> Partner : '+str(partner))
if partner.user_ids:
if len(partner.user_ids) > 1:
userid = partner.user_ids[0].id
else:
userid = partner.user_ids.id
else:
userid = self.env.user.id
_logger.info('Trigger Aktion TRA_' + str(self)+' Create Task '+str(self.Info_Subject)+' - User current ('+str(self.env.user.name)+') from field '+str(self.activity_user_field.name)+' -> '+str(Dataset[self.activity_user_field.name])+' -> Partner : '+str(partner)+' -> User : '+str(userid))
self.env['mail.activity'].create({
'display_name': self.Info_Subject,
'summary': self.activity_text,
'date_deadline': deadline,
'user_id': self.activity_user.id,
'user_id': userid,
'res_id': ds.id,
'res_model_id': self.trigger_table.id,
'activity_type_id': self.activity_type.id
@ -1167,11 +1190,11 @@ class dsstriggermodel(models.Model):
return obj
@api.model
def trigger_track_template(self, changes, Dataset):
def trigger_track_template(self, changes, Dataset):
_logger.info("Trigger in track_template - "+str(Dataset)+" - Changes : "+str(changes))
hook = self.env['dss.settings.vars'].search([('varname','=','N8NWebhook-OnChange')])
if hook:
if hook.varvalue:
if (hook):
if not (hook.varvalue == "FALSE"):
_logger.info("Trigger in track_template - N8NWebhook-OnChange - "+str(Dataset.triggergroup)+" - Changes : "+str(changes))
# Call N8N Webhook
try:

0
models/utils.py Normal file → Executable file
View File

View File

@ -4,6 +4,11 @@
<field name="sequence">15</field>
</record>
<record id="dss_module_phonecenter" model="ir.module.category">
<field name="name">Telefoniecenter</field>
<field name="sequence">16</field>
</record>
<record id="dssgroup_user" model="res.groups">
<field name="name">Standarduser</field>
<field name="category_id" ref="DigitalSignage.dss_module"/>
@ -35,4 +40,34 @@
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="group_phonecenter_user" model="res.groups">
<field name="name">Grundrecht Telefoniecenter</field>
<field name="category_id" ref="DigitalSignage.dss_module_phonecenter"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="group_phonecenter_mode_callcenter" model="res.groups">
<field name="name">Ansichtsmodus: Callcenter</field>
<field name="category_id" ref="DigitalSignage.dss_module_phonecenter"/>
<field name="implied_ids" eval="[(4, ref('DigitalSignage.group_phonecenter_user'))]"/>
</record>
<record id="group_phonecenter_mode_projectmanagement" model="res.groups">
<field name="name">Ansichtsmodus: Projektmanagement</field>
<field name="category_id" ref="DigitalSignage.dss_module_phonecenter"/>
<field name="implied_ids" eval="[(4, ref('DigitalSignage.group_phonecenter_user'))]"/>
</record>
<record id="group_phonecenter_mode_aussendienst" model="res.groups">
<field name="name">Ansichtsmodus: Aussendienst</field>
<field name="category_id" ref="DigitalSignage.dss_module_phonecenter"/>
<field name="implied_ids" eval="[(4, ref('DigitalSignage.group_phonecenter_user'))]"/>
</record>
<record id="group_phonecenter_mode_administrator" model="res.groups">
<field name="name">Ansichtsmodus: Administrator</field>
<field name="category_id" ref="DigitalSignage.dss_module_phonecenter"/>
<field name="implied_ids" eval="[(4, ref('DigitalSignage.group_phonecenter_user'))]"/>
</record>
</odoo>

View File

@ -33,6 +33,7 @@ digitalsignage_dss_triggergroups_group_user,access.dss.triggergroups,model_dss_t
digitalsignage_dss_maintaintask_group_user,access.dss.maintaintask,model_dss_maintaintask,base.group_user,1,1,1,1
digitalsignage_dss_provision_group_user,access.dss.provision,model_dss_provision,base.group_user,1,1,1,1
digitalsignage_dss_provisionstypen_group_user,access.dss.provisionstypen,model_dss_provisionstypen,DigitalSignage.group_prov_user,1,1,1,1
digitalsignage_dss_company_type_group_user,access.dss.company.type,model_dss_company_type,base.group_user,1,1,1,1
digitalsignage_dss_payintervals_group_user,access.dss.payintervals,model_dss_payintervals,base.group_user,1,1,1,1
digitalsignage_dss_importinvoicelist_group_user,access.dss.importinvoicelist,model_dss_importinvoicelist,base.group_user,1,1,1,1
digitalsignage_dss_web_contracts_group_user,access.dss_web_contracts,model_dss_web_contracts,base.group_user,1,1,1,1
@ -53,8 +54,17 @@ digitalsignage_dss_triggeractions_groups_group_user,access.dss.triggeractions.gr
digitalsignage_dss_screendesign_group_user,access.dss.screendesign,model_dss_screendesign,base.group_user,1,1,1,1
digitalsignage_dss_binaries_group_user,access.dss.binaries,model_dss_binaries,base.group_user,1,1,1,1
digitalsignage_dss_binaries_wizard_new_group_user,access.dss.binaries.wizard.new,model_dss_binaries_wizard_new,base.group_user,1,1,1,1
digitalsignage_dss_advertisefields_create_from_template_wizard_group_user,access.dss.advertisefields.create.from.template.wizard,model_dss_advertisefields_create_from_template_wizard,base.group_user,1,1,1,1
digitalsignage_dss_advertisefields_create_from_project_wizard_group_user,access.dss.advertisefields.create.from.project.wizard,model_dss_advertisefields_create_from_project_wizard,base.group_user,1,1,1,1
digitalsignage_dss_advertisefields_save_as_template_wizard_group_user,access.dss.advertisefields.save.as.template.wizard,model_dss_advertisefields_save_as_template_wizard,base.group_user,1,1,1,1
digitalsignage_dss_linkmail_group_user,access.dss.linkmail,model_dss_linkmail,base.group_user,1,1,1,1
digitalsignage_dss_kileads_requests,access.dss.kileads.requests,model_dss_kileads_requests,base.group_user,1,1,1,1
digitalsignage_dss_kileads,access.dss.kileads,model_dss_kileads,base.group_user,1,1,1,1
digitalsignage_dss_contracts_uservars_group_user,access.dss.contracts_uservars,model_dss_contracts_uservars,base.group_user,1,1,1,1
digitalsignage_dss_settings_vars_group_user,access.dss.settings.vars,model_dss_settings_vars,base.group_user,1,1,1,1
digitalsignage_dss_settings_vars_group_user,access.dss.settings.vars,model_dss_settings_vars,base.group_user,1,1,1,1
digitalsignage_dss_csvs_group_user,access.dss.csvs,model_dss_csvs,base.group_user,1,1,1,1
digitalsignage_dss_ai_chat_history_group_user,access.dss.ai.chat.history,model_dss_ai_chat_history,base.group_user,1,1,1,1
digitalsignage_dss_portal_user_profile_group_system,access.dss.portal.user.profile,model_dss_portal_user_profile,base.group_system,1,1,1,1
digitalsignage_dss_phonecenter_query_group_user,access.dss.phonecenter.query,model_dss_phonecenter_query,base.group_user,1,1,1,1
digitalsignage_dss_phonecenter_query_line_group_user,access.dss.phonecenter.query.line,model_dss_phonecenter_query_line,base.group_user,1,1,1,1
digitalsignage_dss_contracts_move_field_wizard_group_user,access.dss.contracts.move.field.wizard,model_dss_contracts_move_field_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
33 digitalsignage_dss_maintaintask_group_user access.dss.maintaintask model_dss_maintaintask base.group_user 1 1 1 1
34 digitalsignage_dss_provision_group_user access.dss.provision model_dss_provision base.group_user 1 1 1 1
35 digitalsignage_dss_provisionstypen_group_user access.dss.provisionstypen model_dss_provisionstypen DigitalSignage.group_prov_user 1 1 1 1
36 digitalsignage_dss_company_type_group_user access.dss.company.type model_dss_company_type base.group_user 1 1 1 1
37 digitalsignage_dss_payintervals_group_user access.dss.payintervals model_dss_payintervals base.group_user 1 1 1 1
38 digitalsignage_dss_importinvoicelist_group_user access.dss.importinvoicelist model_dss_importinvoicelist base.group_user 1 1 1 1
39 digitalsignage_dss_web_contracts_group_user access.dss_web_contracts model_dss_web_contracts base.group_user 1 1 1 1
54 digitalsignage_dss_screendesign_group_user access.dss.screendesign model_dss_screendesign base.group_user 1 1 1 1
55 digitalsignage_dss_binaries_group_user access.dss.binaries model_dss_binaries base.group_user 1 1 1 1
56 digitalsignage_dss_binaries_wizard_new_group_user access.dss.binaries.wizard.new model_dss_binaries_wizard_new base.group_user 1 1 1 1
57 digitalsignage_dss_advertisefields_create_from_template_wizard_group_user access.dss.advertisefields.create.from.template.wizard model_dss_advertisefields_create_from_template_wizard base.group_user 1 1 1 1
58 digitalsignage_dss_advertisefields_create_from_project_wizard_group_user access.dss.advertisefields.create.from.project.wizard model_dss_advertisefields_create_from_project_wizard base.group_user 1 1 1 1
59 digitalsignage_dss_advertisefields_save_as_template_wizard_group_user access.dss.advertisefields.save.as.template.wizard model_dss_advertisefields_save_as_template_wizard base.group_user 1 1 1 1
60 digitalsignage_dss_linkmail_group_user access.dss.linkmail model_dss_linkmail base.group_user 1 1 1 1
61 digitalsignage_dss_kileads_requests access.dss.kileads.requests model_dss_kileads_requests base.group_user 1 1 1 1
62 digitalsignage_dss_kileads access.dss.kileads model_dss_kileads base.group_user 1 1 1 1
63 digitalsignage_dss_contracts_uservars_group_user access.dss.contracts_uservars model_dss_contracts_uservars base.group_user 1 1 1 1
64 digitalsignage_dss_settings_vars_group_user access.dss.settings.vars model_dss_settings_vars base.group_user 1 1 1 1
65 digitalsignage_dss_csvs_group_user access.dss.csvs model_dss_csvs base.group_user 1 1 1 1
66 digitalsignage_dss_ai_chat_history_group_user access.dss.ai.chat.history model_dss_ai_chat_history base.group_user 1 1 1 1
67 digitalsignage_dss_portal_user_profile_group_system access.dss.portal.user.profile model_dss_portal_user_profile base.group_system 1 1 1 1
68 digitalsignage_dss_phonecenter_query_group_user access.dss.phonecenter.query model_dss_phonecenter_query base.group_user 1 1 1 1
69 digitalsignage_dss_phonecenter_query_line_group_user access.dss.phonecenter.query.line model_dss_phonecenter_query_line base.group_user 1 1 1 1
70 digitalsignage_dss_contracts_move_field_wizard_group_user access.dss.contracts.move.field.wizard model_dss_contracts_move_field_wizard base.group_user 1 1 1 1

52
static/src/helpers/helper.html Executable file
View File

@ -0,0 +1,52 @@
<!-- ...existing code... -->
<div style="display: flex; flex-direction: column; gap: 12px; align-items: flex-start;">
<label style="display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
<span>Keine besondere Auswahl</span>
<input type="checkbox" id="web_affinitaet_1" name="web_affinitaet_1" t-att-value="web_affinitaet_1" data-fill-with="undefined"/>
</label>
<label style="display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
<span>News + Finanzen</span>
<input type="checkbox" id="web_affinitaet_2" name="web_affinitaet_2" t-att-value="web_affinitaet_2" data-fill-with="undefined"/>
</label>
<label style="display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
<span>Auto + Mobiles</span>
<input type="checkbox" id="web_affinitaet_3" name="web_affinitaet_3" t-att-value="web_affinitaet_3" data-fill-with="undefined"/>
</label>
<label style="display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
<span>Beauty + Fashion</span>
<input type="checkbox" id="web_affinitaet_4" name="web_affinitaet_4" t-att-value="web_affinitaet_4" data-fill-with="undefined"/>
</label>
<label style="display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
<span>Food</span>
<input type="checkbox" id="web_affinitaet_5" name="web_affinitaet_5" t-att-value="web_affinitaet_5" data-fill-with="undefined"/>
</label>
<label style="display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
<span>Gesundheit</span>
<input type="checkbox" id="web_affinitaet_6" name="web_affinitaet_6" t-att-value="web_affinitaet_6" data-fill-with="undefined"/>
</label>
<label style="display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
<span>Lifestyle</span>
<input type="checkbox" id="web_affinitaet_7" name="web_affinitaet_7" t-att-value="web_affinitaet_7" data-fill-with="undefined"/>
</label>
<label style="display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
<span>Living</span>
<input type="checkbox" id="web_affinitaet_8" name="web_affinitaet_8" t-att-value="web_affinitaet_8" data-fill-with="undefined"/>
</label>
<label style="display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
<span>Entertainment</span>
<input type="checkbox" id="web_affinitaet_9" name="web_affinitaet_9" t-att-value="web_affinitaet_9" data-fill-with="undefined"/>
</label>
<label style="display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
<span>Reise</span>
<input type="checkbox" id="web_affinitaet_10" name="web_affinitaet_10" t-att-value="web_affinitaet_10" data-fill-with="undefined"/>
</label>
<label style="display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
<span>Sport</span>
<input type="checkbox" id="web_affinitaet_11" name="web_affinitaet_11" t-att-value="web_affinitaet_11" data-fill-with="undefined"/>
</label>
<label style="display: flex; align-items: center; gap: 8px; width: 100%; justify-content: flex-start;">
<span>Musik</span>
<input type="checkbox" id="web_affinitaet_12" name="web_affinitaet_12" t-att-value="web_affinitaet_12" data-fill-with="undefined"/>
</label>
</div>
<!-- ...existing code... -->

0
static/src/img/amazon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

0
static/src/img/img_105_b.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 603 KiB

After

Width:  |  Height:  |  Size: 603 KiB

0
static/src/img/img_105_b_2.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 440 KiB

After

Width:  |  Height:  |  Size: 440 KiB

0
static/src/img/img_105_b_3.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 832 KiB

After

Width:  |  Height:  |  Size: 832 KiB

0
static/src/img/img_113_b.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 481 KiB

After

Width:  |  Height:  |  Size: 481 KiB

0
static/src/img/img_113_b_2.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 174 KiB

0
static/src/img/img_113_b_3.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 524 KiB

After

Width:  |  Height:  |  Size: 524 KiB

0
static/src/img/img_115_b.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 778 KiB

After

Width:  |  Height:  |  Size: 778 KiB

0
static/src/img/img_117_b.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 593 KiB

After

Width:  |  Height:  |  Size: 593 KiB

0
static/src/img/img_119_b.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 205 KiB

0
static/src/img/img_119_b_2.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 205 KiB

0
static/src/img/img_119_b_3.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 239 KiB

0
static/src/img/img_121_b.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 658 KiB

After

Width:  |  Height:  |  Size: 658 KiB

0
static/src/img/img_134_b.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 423 KiB

After

Width:  |  Height:  |  Size: 423 KiB

0
static/src/img/img_134_b_2.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 440 KiB

After

Width:  |  Height:  |  Size: 440 KiB

0
static/src/img/img_134_b_3.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 687 KiB

After

Width:  |  Height:  |  Size: 687 KiB

0
static/src/img/img_135_b.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 210 KiB

0
static/src/img/img_137_b.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 797 KiB

After

Width:  |  Height:  |  Size: 797 KiB

0
static/src/img/img_137_b_2.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 876 KiB

After

Width:  |  Height:  |  Size: 876 KiB

0
static/src/img/img_137_b_3.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

0
static/src/img/img_138_b.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 409 KiB

After

Width:  |  Height:  |  Size: 409 KiB

0
static/src/img/img_138_b_2.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 498 KiB

After

Width:  |  Height:  |  Size: 498 KiB

0
static/src/img/img_138_b_3.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

0
static/src/img/img_142_b.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 191 KiB

0
static/src/img/img_142_b_2.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 208 KiB

0
static/src/img/img_142_b_3.jpg Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 205 KiB

After

Width:  |  Height:  |  Size: 205 KiB

Some files were not shown because too many files have changed in this diff Show More