Compare commits
297 Commits
web_respon
...
18.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 72f76d8bd7 | |||
| 6138670817 | |||
| b5e2d5b9b3 | |||
| 043d30e16e | |||
| 92a74356f7 | |||
| d4be3eed12 | |||
| c5ff83ec82 | |||
| 2eebf70d5c | |||
|
|
906e5ebd6d | ||
|
|
a103d8129b | ||
|
|
13a0f0faa1 | ||
| 888f87d8ec | |||
| 63c62699f5 | |||
| c412640ca2 | |||
| fd62a75b51 | |||
| e50acbac83 | |||
| ed5f0d6535 | |||
| bf36bd383a | |||
| ee7e3fb398 | |||
| c83da26305 | |||
| 5880120a84 | |||
| 207a122e37 | |||
| e40caa55e8 | |||
| d4789aaed6 | |||
|
|
7048450ad5 | ||
|
|
a3b7a9a521 | ||
| 686c06f52c | |||
| 99dd5ad688 | |||
| 2d88df12c0 | |||
| bb3dd53fbd | |||
| d2c8473122 | |||
| f726994409 | |||
| 5915c32ba7 | |||
| 2d76c220f0 | |||
| 005da073e8 | |||
| 7a4a89cef9 | |||
| 9295668143 | |||
| c53a0b6b30 | |||
| a498653c26 | |||
| 730cb8ddde | |||
| 33b1eeedf8 | |||
| 4915aaa882 | |||
| ffd1dd0b18 | |||
| c670be57f6 | |||
| 4ac0b04bca | |||
| c5a4899c9f | |||
| bbb6d4c35c | |||
| 0a9a96ae77 | |||
| 7a4b7d8e8f | |||
| 285e2e807a | |||
| b3c96f6416 | |||
| 30f5ab37d2 | |||
| de21d5cb55 | |||
| d4dd28c402 | |||
| 45eaa252bd | |||
| 45222f6bb7 | |||
| d9715fff07 | |||
| 5227767283 | |||
| f76d79a606 | |||
| 14fd3e2f1e | |||
| e3ce2be1f4 | |||
| 0329122548 | |||
| f31284a113 | |||
| dcccf8b034 | |||
| 9751faf173 | |||
| 0d94303107 | |||
| fcd0cfb26f | |||
| 48863fc6d5 | |||
| 8ba35217bf | |||
| afe168723d | |||
| 72273b580f | |||
| 095ed48ded | |||
| 3a18336cc4 | |||
| 33c3ae3585 | |||
| aa7c94b0dc | |||
| e30a37898b | |||
| 88e1fa7965 | |||
| 9f19b53414 | |||
| 26fa9e2871 | |||
| 825423b3af | |||
| 06a1e15a4f | |||
| 23c6cf64cf | |||
| ae9617dedd | |||
| 2884546072 | |||
| d5ae939266 | |||
| 89047fe79c | |||
| d323267a28 | |||
| edf915943b | |||
| d7c9aee436 | |||
| e497c2b5d0 | |||
| 582b9e1b9b | |||
| 2b1c7afdf1 | |||
| 882ba9247a | |||
| 0671d0b2e9 | |||
| 509006cce4 | |||
| 44c55e900a | |||
| db83188b88 | |||
| aa4d44164c | |||
| ea9d3ff61f | |||
| f63dd818aa | |||
| 27f8b2541e | |||
| 558a815535 | |||
| 6d6add697b | |||
| bb235faf39 | |||
| 3951faca64 | |||
| 3db314cedb | |||
| 3e4faf0dc7 | |||
| a70964ad21 | |||
| d0a79ac5a6 | |||
| 04c6e4d523 | |||
| 3d2cbfcb74 | |||
| 320f8e5bbf | |||
| 4d495e5cc5 | |||
| dbb7322fce | |||
| ad78baae06 | |||
| b3f9c9f989 | |||
| 773476029f | |||
| bdf313b39e | |||
| 0b3fafc478 | |||
| ebe63f69ab | |||
| a59bf97a36 | |||
| a64c72a5d4 | |||
| 8b0f9d6361 | |||
| aa5439325d | |||
| b1dbab9942 | |||
| 43ae1490f5 | |||
| 2a07cab00b | |||
| 08fe99a3dd | |||
| 1dae9452af | |||
| 0c15c42d4f | |||
| 7a0ffe51cd | |||
| effc02f01b | |||
| e585583bf5 | |||
| eab73a9964 | |||
| fe62898189 | |||
| dd454b6269 | |||
| 3a469c333d | |||
| 3fe3d5944a | |||
| 42ecceac62 | |||
| f5a379f683 | |||
| 834b292f73 | |||
| d29c58ac6c | |||
| 1095317b23 | |||
| 80ff9d095b | |||
| e110187874 | |||
| 0786f81d63 | |||
| dcb6ca59fc | |||
| 3eb6ee6758 | |||
| 97753affb8 | |||
| e19d374f2b | |||
| 30a33e939f | |||
| d2e38977c2 | |||
| fa3cb047c8 | |||
| 8441d1956d | |||
| 633253de00 | |||
| 9dfa1d4d03 | |||
| 4350577ab7 | |||
| 63becde73e | |||
| 65dc01b15e | |||
| d0e0bb7b0c | |||
| 26ce32efb8 | |||
| aa9004b2ef | |||
| cd8b9c7975 | |||
| 500026c640 | |||
| bd2ee6d072 | |||
| da58849aae | |||
| 570140673c | |||
| 0e56aa652c | |||
| f3b0ba4632 | |||
| 2462220921 | |||
| 17efcd0fbc | |||
| 5d3db75b14 | |||
| ea03f85024 | |||
| be600989ba | |||
| 362fe7126a | |||
| 439ebf4356 | |||
| 9b50ec37a6 | |||
| 829a0fe36f | |||
| 6fc36fd5c5 | |||
| cbc31eaf55 | |||
| d63a402aaf | |||
| 31047a2670 | |||
| 24adc03ab3 | |||
| 4e051d6c52 | |||
| 2ebb0a73c2 | |||
| 906671a8b5 | |||
| a401dc3abd | |||
| 124377f7c5 | |||
| 90836e2f2a | |||
| 90d9a2b202 | |||
| c62d637f56 | |||
| ce90945990 | |||
| bb42154f35 | |||
| 3fd4eb215e | |||
| 543f423825 | |||
| 3d2ad55cfa | |||
| 50cf4b4107 | |||
| 8b6528cb8b | |||
| a55dafa6e0 | |||
| 24c89d97e5 | |||
| a7c36be076 | |||
| 2ef98a3897 | |||
| e21d4e66a9 | |||
| 38c91ef94c | |||
| 454e6936fb | |||
| 6d49162c09 | |||
| 5bd5c7b906 | |||
| a4de8697da | |||
| ce8f7f8711 | |||
| ca0ca65f14 | |||
| 30f2cf9b0e | |||
| f68e2d23e7 | |||
| 05c33d06c3 | |||
| 46fca55d81 | |||
| 4db469f273 | |||
| 72a652a5c6 | |||
| 5523995594 | |||
| 7b7bcf73e6 | |||
| acb3564750 | |||
| bb3359a309 | |||
| 8b1544647a | |||
| f727f3ab67 | |||
| a371ac9bdc | |||
| f84c891e96 | |||
| c31947bfab | |||
| d783d0d08d | |||
| 7a9e3c56fe | |||
| 11770d4950 | |||
| 0d150fa92e | |||
| 2933122464 | |||
| 5ab0886edf | |||
| e2045b21e2 | |||
| 596bd0a761 | |||
| 2a19267d2f | |||
| 50105c5ba6 | |||
| 434b1d46ab | |||
| d4945d20ee | |||
| 6e05ee4c57 | |||
| 33ba2de5dd | |||
| bb075fe036 | |||
| 60687c611a | |||
| 776bcc1f7f | |||
| 61061bdee9 | |||
| 77d467de70 | |||
| e4a63ec0e4 | |||
| 77dc1762c0 | |||
| e2bf6c22f9 | |||
| f9b4e1582a | |||
| 53a05d2240 | |||
| 8512a1d196 | |||
| 41a130c1d6 | |||
| 13b8322e3c | |||
| d18265433c | |||
| 16e661f8e8 | |||
| 8a40edf6c6 | |||
| 831cbe1449 | |||
| aea1722933 | |||
| 51a39b2e6e | |||
| 21e6a6f3f6 | |||
| dbca7e34e1 | |||
| ac8400512b | |||
| 817b05fe06 | |||
| cb1c9a1ffa | |||
| bc7dbb494a | |||
| 7827e2f224 | |||
| c4edb6b3af | |||
| bdc66120f3 | |||
| 636938581d | |||
| 2c3cfc3978 | |||
| 0236ef0809 | |||
| de9fd7b71d | |||
| eaeb978c19 | |||
| 2e55efb312 | |||
| b786492d5e | |||
| 6086717e66 | |||
| 375466beea | |||
| 8dfe9e9874 | |||
| 385ef060ed | |||
| 6c8b3cbd74 | |||
| 90d1e47e03 | |||
| 9700c192d0 | |||
| 25596b4149 | |||
| 0885b59bb3 | |||
| fcbe9a17db | |||
| 860fedb7e0 | |||
| 85ada8e9b7 | |||
| 6b98bf8be1 | |||
| 55fb716490 | |||
|
|
ab214ac9dd | ||
|
|
304da43eb8 | ||
| 48b0b7a283 | |||
| 40c3e1d471 | |||
| c1ecf1289d | |||
| 0741834b31 | |||
| 7a2debb3d7 | |||
| 6ef9d029eb | |||
|
|
96edc0c694 |
393
.gitea/qualify-addon.py
Normal file
@@ -0,0 +1,393 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
qualify-addon.py — Pillar 1 of the addon qualification gate.
|
||||
|
||||
Static checks against an Odoo addon source tree:
|
||||
manifest __manifest__.py parses, has 'name', 'version' starts with '<digit>.0.'
|
||||
pip-deps every non-stdlib import is declared in external_dependencies['python']
|
||||
app-name every <app> element in any view XML has a name= attribute
|
||||
menu-icon top-level <menuitem web_icon=> is set OR the addon ships
|
||||
static/description/icon.png
|
||||
hoot-import no JS file under static/src/ imports from '@odoo/hoot' or '@odoo/hoot-dom'
|
||||
webpack-name no JS file under static/lib/ uses self.webpackChunk_<unprefixed-name> —
|
||||
chunk array names must be addon-namespaced (e.g. webpackChunk_am5_<addon>)
|
||||
|
||||
Usage:
|
||||
python3 scripts/qualify-addon.py <addon-dir> [<addon-dir> ...]
|
||||
python3 scripts/qualify-addon.py --json <addon-dir>
|
||||
|
||||
Exit codes:
|
||||
0 all checks passed for every addon
|
||||
1 at least one addon failed at least one check
|
||||
2 bad usage / I/O error
|
||||
|
||||
Each finding is (severity, check, message). Severity:
|
||||
ERROR — the addon is broken-by-construction; refuse to admit to catalog
|
||||
WARN — likely problem but could be intentional; admit-with-warning posture
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, asdict
|
||||
from pathlib import Path
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
|
||||
# Python stdlib modules. Conservative — anything imported NOT in here AND not in
|
||||
# ODOO_BUILTINS is flagged as needing declaration. Better to false-positive (fixable
|
||||
# by adding to external_dependencies) than miss a real missing dep.
|
||||
STDLIB = frozenset({
|
||||
'abc', 'argparse', 'ast', 'asyncio', 'base64', 'binascii', 'bisect', 'calendar',
|
||||
'collections', 'configparser', 'contextlib', 'contextvars', 'copy', 'csv', 'ctypes',
|
||||
'dataclasses', 'datetime', 'decimal', 'difflib', 'dis', 'email', 'enum', 'errno',
|
||||
'fcntl', 'fnmatch', 'functools', 'gc', 'getpass', 'gettext', 'glob', 'gzip', 'hashlib',
|
||||
'heapq', 'hmac', 'html', 'http', 'imaplib', 'importlib', 'inspect', 'io', 'ipaddress',
|
||||
'itertools', 'json', 'keyword', 'locale', 'logging', 'math', 'mimetypes',
|
||||
'multiprocessing', 'numbers', 'operator', 'os', 'pathlib', 'pickle', 'pkgutil',
|
||||
'platform', 'pprint', 'queue', 'random', 're', 'select', 'selectors', 'shlex',
|
||||
'shutil', 'signal', 'smtplib', 'socket', 'sqlite3', 'ssl', 'stat', 'string', 'struct',
|
||||
'subprocess', 'sys', 'tempfile', 'textwrap', 'threading', 'time', 'timeit', 'token',
|
||||
'tokenize', 'traceback', 'types', 'typing', 'unicodedata', 'unittest', 'urllib',
|
||||
'uuid', 'warnings', 'weakref', 'xml', 'xmlrpc', 'zipfile', 'zlib', 'zoneinfo',
|
||||
'__future__',
|
||||
})
|
||||
|
||||
# Modules shipped by Odoo's base image. Never need declaration.
|
||||
ODOO_BUILTINS = frozenset({
|
||||
'odoo', 'psycopg2', 'lxml', 'PIL', 'requests', 'dateutil', 'pytz', 'passlib',
|
||||
'werkzeug', 'jinja2', 'markupsafe', 'docutils', 'reportlab', 'babel', 'xlsxwriter',
|
||||
'xlrd', 'xlwt', 'qrcode', 'vobject', 'polib', 'PyPDF2', 'cryptography', 'pyOpenSSL',
|
||||
'OpenSSL', 'suds', 'num2words', 'pyldap', 'ldap', 'xmltodict', 'zeep', 'gevent',
|
||||
'greenlet', 'libsass', 'idna', 'pyusb', 'serial', 'qrcode', 'mock', 'freezegun',
|
||||
'phonenumbers',
|
||||
})
|
||||
|
||||
# Bare-name webpackChunk arrays we know collide. Detector flags any
|
||||
# `self.webpackChunk_<name>` where <name> doesn't have an addon-derived suffix.
|
||||
# We allow the canonical chunk array names listed here ONLY if the addon's
|
||||
# directory name matches — i.e. we suggest namespacing.
|
||||
WEBPACK_CHUNK_RE = re.compile(r'self\.(webpackChunk[a-zA-Z0-9_]+)\b')
|
||||
|
||||
# JS imports of the hoot test framework that should never appear in production code.
|
||||
HOOT_IMPORT_RE = re.compile(
|
||||
r'''(?:from\s+['"]@odoo/hoot[a-z\-]*['"]|require\s*\(\s*['"]@odoo/hoot[a-z\-]*['"]\s*\))'''
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Finding:
|
||||
severity: str # 'ERROR' | 'WARN'
|
||||
check: str # short check id
|
||||
message: str # human-readable
|
||||
file: str | None = None # relative path, if applicable
|
||||
line: int | None = None # 1-indexed, if applicable
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------- #
|
||||
# Check 1 — manifest parses + has required keys
|
||||
# ---------------------------------------------------------------------------- #
|
||||
def check_manifest(addon_dir: Path) -> tuple[list[Finding], dict | None]:
|
||||
findings: list[Finding] = []
|
||||
mf_path = addon_dir / '__manifest__.py'
|
||||
if not mf_path.exists():
|
||||
findings.append(Finding('ERROR', 'manifest', 'no __manifest__.py'))
|
||||
return findings, None
|
||||
try:
|
||||
manifest = ast.literal_eval(mf_path.read_text())
|
||||
except (SyntaxError, ValueError) as e:
|
||||
findings.append(Finding('ERROR', 'manifest',
|
||||
f'__manifest__.py does not parse as Python literal: {e}',
|
||||
file='__manifest__.py'))
|
||||
return findings, None
|
||||
if not isinstance(manifest, dict):
|
||||
findings.append(Finding('ERROR', 'manifest',
|
||||
'__manifest__.py top-level is not a dict',
|
||||
file='__manifest__.py'))
|
||||
return findings, None
|
||||
if not manifest.get('name'):
|
||||
findings.append(Finding('ERROR', 'manifest',
|
||||
"missing 'name' key (Odoo refuses install)",
|
||||
file='__manifest__.py'))
|
||||
version = manifest.get('version', '')
|
||||
if not re.match(r'^\d+\.0\.\d+\.\d+\.\d+$', version):
|
||||
findings.append(Finding('WARN', 'manifest',
|
||||
f"version {version!r} is not in '<odoo_major>.0.x.y.z' form — "
|
||||
"Odoo will prepend the running Odoo major and may refuse install "
|
||||
"on a different major (incident #9)",
|
||||
file='__manifest__.py'))
|
||||
return findings, manifest
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------- #
|
||||
# Check 2 — pip deps: every non-stdlib import is in external_dependencies
|
||||
# ---------------------------------------------------------------------------- #
|
||||
def check_pip_deps(addon_dir: Path, manifest: dict) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
declared = set(manifest.get('external_dependencies', {}).get('python', []))
|
||||
addon_name = addon_dir.name
|
||||
|
||||
# Pre-scan: collect this addon's submodule names so we don't flag intra-addon imports.
|
||||
own_submodules = {p.stem for p in addon_dir.rglob('*.py') if p.stem != '__init__'}
|
||||
own_submodules.add(addon_name)
|
||||
|
||||
seen_imports: set[tuple[str, str, int]] = set() # (toplevel, file, line)
|
||||
|
||||
for py_file in addon_dir.rglob('*.py'):
|
||||
if any(part.startswith('.') for part in py_file.parts):
|
||||
continue
|
||||
try:
|
||||
tree = ast.parse(py_file.read_text())
|
||||
except (SyntaxError, UnicodeDecodeError):
|
||||
continue
|
||||
rel = py_file.relative_to(addon_dir).as_posix()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
seen_imports.add((alias.name.split('.')[0], rel, node.lineno))
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.level: # relative import — intra-addon, skip
|
||||
continue
|
||||
if node.module:
|
||||
seen_imports.add((node.module.split('.')[0], rel, node.lineno))
|
||||
|
||||
for top, rel, lineno in sorted(seen_imports):
|
||||
if top in STDLIB or top in ODOO_BUILTINS or top in own_submodules:
|
||||
continue
|
||||
if top in declared:
|
||||
continue
|
||||
# PEP 8 names that are clearly local helpers (e.g. utils, models) — skip if
|
||||
# they look like a sibling module we missed in own_submodules.
|
||||
if (addon_dir / top).is_dir() or (addon_dir / f'{top}.py').exists():
|
||||
continue
|
||||
findings.append(Finding(
|
||||
'ERROR', 'pip-deps',
|
||||
f"imports '{top}' but it is not in external_dependencies['python'] "
|
||||
"(install will fail with ModuleNotFoundError — incident #5)",
|
||||
file=rel, line=lineno,
|
||||
))
|
||||
return findings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------- #
|
||||
# Check 3 — every <app> element has a name= attribute
|
||||
# ---------------------------------------------------------------------------- #
|
||||
def check_app_name(addon_dir: Path) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
# XML files in views/ + data/ may contain res_config_settings <app> elements.
|
||||
for xml_file in list(addon_dir.rglob('views/*.xml')) + list(addon_dir.rglob('data/*.xml')):
|
||||
try:
|
||||
text = xml_file.read_text()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
rel = xml_file.relative_to(addon_dir).as_posix()
|
||||
# Multi-line tolerant regex: <app ... > with everything between.
|
||||
for m in re.finditer(r'<app\b[^>]*?>', text, re.DOTALL):
|
||||
tag = m.group()
|
||||
if 'name=' in tag:
|
||||
continue
|
||||
line = text[:m.start()].count('\n') + 1
|
||||
findings.append(Finding(
|
||||
'ERROR', 'app-name',
|
||||
"<app> element missing name= attribute. Odoo 18 SettingsFormCompiler "
|
||||
"calls toStringExpression(null) and crashes the entire Settings page "
|
||||
"(incident #7)",
|
||||
file=rel, line=line,
|
||||
))
|
||||
return findings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------- #
|
||||
# Check 4 — top-level menus declare web_icon OR addon ships static/description/icon.png
|
||||
# ---------------------------------------------------------------------------- #
|
||||
def check_menu_icon(addon_dir: Path) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
has_default_icon = (addon_dir / 'static' / 'description' / 'icon.png').exists()
|
||||
for xml_file in addon_dir.rglob('*.xml'):
|
||||
try:
|
||||
text = xml_file.read_text()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
rel = xml_file.relative_to(addon_dir).as_posix()
|
||||
# Find <menuitem ... > whose XML has no parent= attribute (top-level menu).
|
||||
for m in re.finditer(r'<menuitem\b[^>]*?/?>', text, re.DOTALL):
|
||||
tag = m.group()
|
||||
if 'parent=' in tag:
|
||||
continue
|
||||
if 'web_icon=' in tag:
|
||||
continue
|
||||
if has_default_icon:
|
||||
# Odoo 18's auto-fallback path. Soft warning since it works for top-level
|
||||
# menus that get web_icon auto-populated from the module's icon.png.
|
||||
# But our incident #6 showed even with icon.png present, web_icon often
|
||||
# ends up empty in DB. So WARN, not ERROR.
|
||||
line = text[:m.start()].count('\n') + 1
|
||||
findings.append(Finding(
|
||||
'WARN', 'menu-icon',
|
||||
"top-level <menuitem> has no web_icon=. Will fall back to "
|
||||
"static/description/icon.png IF Odoo's auto-populate fires; "
|
||||
"if not, menu shows blank (incident #6). Set web_icon explicitly.",
|
||||
file=rel, line=line,
|
||||
))
|
||||
else:
|
||||
line = text[:m.start()].count('\n') + 1
|
||||
findings.append(Finding(
|
||||
'ERROR', 'menu-icon',
|
||||
"top-level <menuitem> has no web_icon= AND addon ships no "
|
||||
"static/description/icon.png — menu will render blank.",
|
||||
file=rel, line=line,
|
||||
))
|
||||
return findings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------- #
|
||||
# Check 5 — no @odoo/hoot* imports in static/src/
|
||||
# ---------------------------------------------------------------------------- #
|
||||
def check_hoot_import(addon_dir: Path) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
src_dir = addon_dir / 'static' / 'src'
|
||||
if not src_dir.exists():
|
||||
return findings
|
||||
for js_file in src_dir.rglob('*.js'):
|
||||
try:
|
||||
text = js_file.read_text()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
rel = js_file.relative_to(addon_dir).as_posix()
|
||||
for m in HOOT_IMPORT_RE.finditer(text):
|
||||
line = text[:m.start()].count('\n') + 1
|
||||
findings.append(Finding(
|
||||
'ERROR', 'hoot-import',
|
||||
"imports from @odoo/hoot* in production code (static/src/). "
|
||||
"@odoo/hoot is the test framework; the production bundle does not "
|
||||
"register it. Page will white-screen (incident #3 class)",
|
||||
file=rel, line=line,
|
||||
))
|
||||
return findings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------- #
|
||||
# Check 6 — webpack chunk arrays in static/lib/ must be addon-namespaced
|
||||
# ---------------------------------------------------------------------------- #
|
||||
def check_webpack_chunk(addon_dir: Path) -> list[Finding]:
|
||||
findings: list[Finding] = []
|
||||
lib_dir = addon_dir / 'static' / 'lib'
|
||||
if not lib_dir.exists():
|
||||
return findings
|
||||
addon_name = addon_dir.name
|
||||
seen: set[str] = set()
|
||||
for js_file in lib_dir.rglob('*.js'):
|
||||
try:
|
||||
text = js_file.read_text()
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
rel = js_file.relative_to(addon_dir).as_posix()
|
||||
for m in WEBPACK_CHUNK_RE.finditer(text):
|
||||
chunk_name = m.group(1)
|
||||
if chunk_name in seen:
|
||||
continue
|
||||
seen.add(chunk_name)
|
||||
# Acceptable if chunk name contains: full addon name OR any 4+ char
|
||||
# sub-token of the addon name (e.g. 'ksdn' for 'ks_dashboard_ninja')
|
||||
# OR a known-namespaced suffix (anything past the standard library
|
||||
# prefix). We just need confidence the chunk array is unique-per-addon.
|
||||
addon_lower = addon_name.lower()
|
||||
chunk_lower = chunk_name.lower()
|
||||
tokens = [addon_lower.replace('_', '')] + [
|
||||
t for t in addon_lower.split('_') if len(t) >= 4
|
||||
]
|
||||
# Also accept any short 4+ char abbrev derived from initials of
|
||||
# underscore-separated parts (ks_dashboard_ninja -> ksdn)
|
||||
initials = ''.join(t[0] for t in addon_lower.split('_') if t)
|
||||
if len(initials) >= 3:
|
||||
tokens.append(initials)
|
||||
if any(t in chunk_lower for t in tokens):
|
||||
continue
|
||||
line = text[:m.start()].count('\n') + 1
|
||||
findings.append(Finding(
|
||||
'ERROR', 'webpack-chunk',
|
||||
f"uses bare webpack chunk array '{chunk_name}'. Two addons that ship "
|
||||
f"the same library (e.g. amCharts) collide on this global → bundle "
|
||||
f"execution aborts (incident #4). Rename to '{chunk_name}_{addon_name}' "
|
||||
"or similar.",
|
||||
file=rel, line=line,
|
||||
))
|
||||
return findings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------- #
|
||||
# Runner
|
||||
# ---------------------------------------------------------------------------- #
|
||||
def qualify_addon(addon_dir: Path) -> dict:
|
||||
findings: list[Finding] = []
|
||||
|
||||
manifest_findings, manifest = check_manifest(addon_dir)
|
||||
findings.extend(manifest_findings)
|
||||
|
||||
if manifest is not None:
|
||||
findings.extend(check_pip_deps(addon_dir, manifest))
|
||||
|
||||
findings.extend(check_app_name(addon_dir))
|
||||
findings.extend(check_menu_icon(addon_dir))
|
||||
findings.extend(check_hoot_import(addon_dir))
|
||||
findings.extend(check_webpack_chunk(addon_dir))
|
||||
|
||||
errors = sum(1 for f in findings if f.severity == 'ERROR')
|
||||
warns = sum(1 for f in findings if f.severity == 'WARN')
|
||||
return {
|
||||
'addon': addon_dir.name,
|
||||
'path': str(addon_dir),
|
||||
'qualified': errors == 0,
|
||||
'errors': errors,
|
||||
'warns': warns,
|
||||
'findings': [asdict(f) for f in findings],
|
||||
}
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
json_out = False
|
||||
args: list[str] = []
|
||||
for a in argv[1:]:
|
||||
if a == '--json':
|
||||
json_out = True
|
||||
elif a in ('-h', '--help'):
|
||||
print(__doc__)
|
||||
return 0
|
||||
else:
|
||||
args.append(a)
|
||||
if not args:
|
||||
print(__doc__, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
results = []
|
||||
for path_str in args:
|
||||
path = Path(path_str).resolve()
|
||||
if not path.is_dir() or not (path / '__manifest__.py').exists():
|
||||
print(f'ERROR: {path} is not an Odoo addon directory '
|
||||
'(missing __manifest__.py)', file=sys.stderr)
|
||||
return 2
|
||||
results.append(qualify_addon(path))
|
||||
|
||||
if json_out:
|
||||
print(json.dumps(results, indent=2))
|
||||
else:
|
||||
for r in results:
|
||||
badge = '\033[32mQUALIFIED\033[0m' if r['qualified'] else '\033[31mFAILED\033[0m'
|
||||
print(f"\n{badge} {r['addon']} ({r['errors']} error(s), {r['warns']} warning(s))")
|
||||
if not r['findings']:
|
||||
continue
|
||||
for f in r['findings']:
|
||||
tag = '\033[31m' if f['severity'] == 'ERROR' else '\033[33m'
|
||||
loc = ''
|
||||
if f['file']:
|
||||
loc = f" [{f['file']}" + (f":{f['line']}" if f['line'] else '') + ']'
|
||||
print(f" {tag}{f['severity']:5}\033[0m {f['check']:<14} {f['message']}{loc}")
|
||||
|
||||
any_failed = any(not r['qualified'] for r in results)
|
||||
return 1 if any_failed else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv))
|
||||
40
.gitea/workflows/addon-qualify.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
# Pillar 1 of the addon-qualification proposal — runs on every push to any
|
||||
# branch and on every PR. Runs the vendored qualify-addon.py against every
|
||||
# addon directory in this repo.
|
||||
#
|
||||
# admit-with-warning posture: lint findings are reported but do NOT fail
|
||||
# the build (matches Pillar 3 informed-consent posture).
|
||||
#
|
||||
# To update the qualifier itself, edit scripts/qualify-addon.py in
|
||||
# odoo-tower/odooskyv3 then sync it here.
|
||||
name: addon-qualify
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
qualify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout addons repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run qualifier on every addon
|
||||
run: |
|
||||
set +e
|
||||
ADDONS=()
|
||||
for d in addons/*/; do
|
||||
[ -f "$d/__manifest__.py" ] && ADDONS+=("${d%/}")
|
||||
done
|
||||
if [ ${#ADDONS[@]} -eq 0 ]; then
|
||||
echo "No addons under addons/ — nothing to qualify"
|
||||
exit 0
|
||||
fi
|
||||
echo "Qualifying ${#ADDONS[@]} addons..."
|
||||
python3 .gitea/qualify-addon.py "${ADDONS[@]}"
|
||||
QUAL_RC=$?
|
||||
echo
|
||||
echo "::notice ::qualifier exit code $QUAL_RC (admit-with-warning — not failing build)"
|
||||
exit 0
|
||||
123
addons/cx_web_refresh_from_backend/README.rst
Normal file
@@ -0,0 +1,123 @@
|
||||
========================
|
||||
Web Refresh From Backend
|
||||
========================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:199e0da56a7d94568d062706d1f34ac6b38310034c25f5840e2631722e9d9f65
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/license-LGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
|
||||
:alt: License: LGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github
|
||||
:target: https://github.com/cetmix/cetmix-tower/tree/18.0/cx_web_refresh_from_backend
|
||||
:alt: cetmix/cetmix-tower
|
||||
|
||||
|badge1| |badge2| |badge3|
|
||||
|
||||
Refresh UI views from backend
|
||||
=============================
|
||||
|
||||
This is a **technical module** that allows triggering a **UI reload**
|
||||
from the backend. It enables triggering the reload action for selected
|
||||
users and record IDs.
|
||||
|
||||
--------------
|
||||
|
||||
🔧 Helper Function: ``reload_views``
|
||||
------------------------------------
|
||||
|
||||
A special helper function ``reload_views`` is added to the ``res.users``
|
||||
model.
|
||||
|
||||
**Arguments**
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
+----------------+--------------------------+--------------------------+
|
||||
| Argument | Type | Description |
|
||||
+================+==========================+==========================+
|
||||
| **model** | ``Char`` | Model name, e.g. |
|
||||
| | | ``'res.partner'`` |
|
||||
+----------------+--------------------------+--------------------------+
|
||||
| **view_types** | ``List of Char`` | View types to reload, |
|
||||
| | *(optional)* | e.g. |
|
||||
| | | ``["form", "kanban"]``. |
|
||||
| | | Leave blank to reload |
|
||||
| | | all views. |
|
||||
+----------------+--------------------------+--------------------------+
|
||||
| **rec_ids** | ``List of Integer`` | The view will be |
|
||||
| | *(optional)* | reloaded only if a |
|
||||
| | | record with an ID from |
|
||||
| | | this list is present in |
|
||||
| | | the view. |
|
||||
+----------------+--------------------------+--------------------------+
|
||||
|
||||
--------------
|
||||
|
||||
⚠️ Important Notes
|
||||
------------------
|
||||
|
||||
Use this function **wisely**.
|
||||
|
||||
When reloading **form views**, be aware that if a user is currently
|
||||
editing a record, **their unsaved updates may be lost** when the form
|
||||
reloads from the server (no confirmation dialog is shown).
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
🧩 Example Usage
|
||||
----------------
|
||||
|
||||
Below is a code snippet showing how to use the ``reload_views`` helper
|
||||
function.
|
||||
|
||||
.. code:: python
|
||||
|
||||
# Reload the kanban and form views for all salespeople when an opportunity is won
|
||||
# Will reload views only if the current opportunity is being displayed
|
||||
|
||||
group_id = self.env.ref("sales_team.group_sale_salesman").id
|
||||
users_to_reload = self.env["res.users"].search([("groups_id", "in", [group_id])])
|
||||
users_to_reload.reload_views(
|
||||
model="crm.lead",
|
||||
view_types=["kanban", "form"],
|
||||
rec_ids=[self.ids],
|
||||
)
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/cetmix/cetmix-tower/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cx_web_refresh_from_backend%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
-------
|
||||
|
||||
* Cetmix
|
||||
|
||||
Maintainers
|
||||
-----------
|
||||
|
||||
This module is part of the `cetmix/cetmix-tower <https://github.com/cetmix/cetmix-tower/tree/18.0/cx_web_refresh_from_backend>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute.
|
||||
4
addons/cx_web_refresh_from_backend/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright 2025 Cetmix OÜ
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from . import models
|
||||
30
addons/cx_web_refresh_from_backend/__manifest__.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# Copyright 2025 Cetmix OÜ
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
# Mail is required: its ir.websocket override subscribes the partner channel to the
|
||||
# bus, so users receive web.refresh_view notifications.
|
||||
|
||||
{
|
||||
"name": "Web Refresh From Backend",
|
||||
"summary": "Refresh frontend views from backend",
|
||||
"version": "18.0.1.0.0",
|
||||
"category": "Web",
|
||||
"license": "LGPL-3",
|
||||
"author": "Cetmix",
|
||||
"website": "https://tower.cetmix.com",
|
||||
"images": ["static/description/banner.png"],
|
||||
"depends": ["mail"],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"cx_web_refresh_from_backend/static/src/views/utils/get_loaded_record_ids.esm.js",
|
||||
"cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js",
|
||||
"cx_web_refresh_from_backend/static/src/views/kanban/kanban_controller_patch.esm.js",
|
||||
"cx_web_refresh_from_backend/static/src/views/form/form_controller_patch.esm.js",
|
||||
],
|
||||
"web.qunit_suite_tests": [
|
||||
"cx_web_refresh_from_backend/static/tests/refresh_from_backend_tests.esm.js",
|
||||
],
|
||||
},
|
||||
"installable": True,
|
||||
"auto_install": False,
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * cx_web_refresh_from_backend
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: cx_web_refresh_from_backend
|
||||
#. odoo-javascript
|
||||
#: code:addons/cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js:0
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#. module: cx_web_refresh_from_backend
|
||||
#. odoo-javascript
|
||||
#: code:addons/cx_web_refresh_from_backend/static/src/views/form/form_controller_patch.esm.js:0
|
||||
msgid "Could not reload form. %(message)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: cx_web_refresh_from_backend
|
||||
#. odoo-javascript
|
||||
#: code:addons/cx_web_refresh_from_backend/static/src/views/kanban/kanban_controller_patch.esm.js:0
|
||||
msgid "Could not reload kanban. %(message)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: cx_web_refresh_from_backend
|
||||
#. odoo-javascript
|
||||
#: code:addons/cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js:0
|
||||
msgid "Could not reload list. %(message)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: cx_web_refresh_from_backend
|
||||
#. odoo-javascript
|
||||
#: code:addons/cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js:0
|
||||
msgid "Could not save record. %(message)s"
|
||||
msgstr ""
|
||||
|
||||
#. module: cx_web_refresh_from_backend
|
||||
#. odoo-javascript
|
||||
#: code:addons/cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js:0
|
||||
msgid "List is being refreshed from backend"
|
||||
msgstr ""
|
||||
|
||||
#. module: cx_web_refresh_from_backend
|
||||
#. odoo-javascript
|
||||
#: code:addons/cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js:0
|
||||
msgid "Save & Refresh"
|
||||
msgstr ""
|
||||
|
||||
#. module: cx_web_refresh_from_backend
|
||||
#: model:ir.model,name:cx_web_refresh_from_backend.model_res_users
|
||||
msgid "User"
|
||||
msgstr ""
|
||||
|
||||
#. module: cx_web_refresh_from_backend
|
||||
#. odoo-javascript
|
||||
#: code:addons/cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm.js:0
|
||||
msgid "You have unsaved edits. Save them before refreshing?"
|
||||
msgstr ""
|
||||
4
addons/cx_web_refresh_from_backend/models/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright 2025 Cetmix OÜ
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from . import res_users
|
||||
50
addons/cx_web_refresh_from_backend/models/res_users.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# Copyright 2025 Cetmix OÜ
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = "res.users"
|
||||
|
||||
def reload_views(self, model, view_types=None, rec_ids=None):
|
||||
"""
|
||||
Trigger UI reload for selected users and record IDs.
|
||||
|
||||
This method allows to reload specific views from the backend.
|
||||
Be aware that when reloading form views, if a user is currently
|
||||
doing some updates, those updates may be lost when the form reloads
|
||||
(no confirmation dialog on the client).
|
||||
|
||||
:param model: str, Model name (e.g., 'res.partner')
|
||||
:param view_types: list of str, optional, View types to reload
|
||||
(e.g., ['form', 'kanban']). Leave blank to reload all views.
|
||||
:param rec_ids: list of int, optional, View will be reloaded only if a record
|
||||
with id from the list is present in the view.
|
||||
|
||||
Example usage:
|
||||
# Reload the kanban and form views for all salespeople
|
||||
# when an opportunity is won.
|
||||
# Will reload views only if the current opportunity is being displayed
|
||||
group_id = self.env.ref("sales_team.group_sale_salesman").id
|
||||
users_to_reload = self.env["res.users"].search(
|
||||
[("groups_id", "in", [group_id])]
|
||||
)
|
||||
users_to_reload.reload_views(
|
||||
model="crm.lead",
|
||||
view_types=["kanban", "form"],
|
||||
rec_ids=[self.ids]
|
||||
)
|
||||
"""
|
||||
|
||||
# Prepare the message payload
|
||||
bus_message = {
|
||||
"model": model,
|
||||
"view_types": view_types or [],
|
||||
"rec_ids": rec_ids or [],
|
||||
}
|
||||
|
||||
# Send one notification per user's partner in deterministic order.
|
||||
bus_bus = self.env["bus.bus"]
|
||||
for user in self.sorted("id"):
|
||||
bus_bus._sendone(user.partner_id, "web.refresh_view", bus_message)
|
||||
3
addons/cx_web_refresh_from_backend/pyproject.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
[build-system]
|
||||
requires = ["whool"]
|
||||
build-backend = "whool.buildapi"
|
||||
28
addons/cx_web_refresh_from_backend/readme/DESCRIPTION.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Refresh UI views from backend
|
||||
|
||||
This is a **technical module** that allows triggering a **UI reload** from the backend.
|
||||
It enables triggering the reload action for selected users and record IDs.
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Helper Function: `reload_views`
|
||||
|
||||
A special helper function `reload_views` is added to the `res.users` model.
|
||||
|
||||
### **Arguments**
|
||||
|
||||
| Argument | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| **model** | `Char` | Model name, e.g. `'res.partner'` |
|
||||
| **view_types** | `List of Char` *(optional)* | View types to reload, e.g. `["form", "kanban"]`. Leave blank to reload all views. |
|
||||
| **rec_ids** | `List of Integer` *(optional)* | The view will be reloaded only if a record with an ID from this list is present in the view. |
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Important Notes
|
||||
|
||||
Use this function **wisely**.
|
||||
|
||||
When reloading **form views**, be aware that if a user is currently editing a record,
|
||||
**their unsaved updates may be lost** when the form reloads from the server (no confirmation
|
||||
dialog is shown).
|
||||
16
addons/cx_web_refresh_from_backend/readme/USAGE.md
Normal file
@@ -0,0 +1,16 @@
|
||||
## 🧩 Example Usage
|
||||
|
||||
Below is a code snippet showing how to use the `reload_views` helper function.
|
||||
|
||||
```python
|
||||
# Reload the kanban and form views for all salespeople when an opportunity is won
|
||||
# Will reload views only if the current opportunity is being displayed
|
||||
|
||||
group_id = self.env.ref("sales_team.group_sale_salesman").id
|
||||
users_to_reload = self.env["res.users"].search([("groups_id", "in", [group_id])])
|
||||
users_to_reload.reload_views(
|
||||
model="crm.lead",
|
||||
view_types=["kanban", "form"],
|
||||
rec_ids=[self.ids],
|
||||
)
|
||||
```
|
||||
BIN
addons/cx_web_refresh_from_backend/static/description/banner.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
addons/cx_web_refresh_from_backend/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
479
addons/cx_web_refresh_from_backend/static/description/index.html
Normal file
@@ -0,0 +1,479 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>Web Refresh From Backend</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
Despite the name, some widely supported CSS2 features are used.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: gray; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic, pre.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="web-refresh-from-backend">
|
||||
<h1 class="title">Web Refresh From Backend</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:199e0da56a7d94568d062706d1f34ac6b38310034c25f5840e2631722e9d9f65
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/license-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/cetmix/cetmix-tower/tree/18.0/cx_web_refresh_from_backend"><img alt="cetmix/cetmix-tower" src="https://img.shields.io/badge/github-cetmix%2Fcetmix--tower-lightgray.png?logo=github" /></a></p>
|
||||
<div class="section" id="refresh-ui-views-from-backend">
|
||||
<h1>Refresh UI views from backend</h1>
|
||||
<p>This is a <strong>technical module</strong> that allows triggering a <strong>UI reload</strong>
|
||||
from the backend. It enables triggering the reload action for selected
|
||||
users and record IDs.</p>
|
||||
<hr class="docutils" />
|
||||
<div class="section" id="helper-function-reload-views">
|
||||
<h2>🔧 Helper Function: <tt class="docutils literal">reload_views</tt></h2>
|
||||
<p>A special helper function <tt class="docutils literal">reload_views</tt> is added to the <tt class="docutils literal">res.users</tt>
|
||||
model.</p>
|
||||
<div class="section" id="arguments">
|
||||
<h3><strong>Arguments</strong></h3>
|
||||
<table border="1" class="docutils">
|
||||
<colgroup>
|
||||
<col width="24%" />
|
||||
<col width="38%" />
|
||||
<col width="38%" />
|
||||
</colgroup>
|
||||
<thead valign="bottom">
|
||||
<tr><th class="head">Argument</th>
|
||||
<th class="head">Type</th>
|
||||
<th class="head">Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody valign="top">
|
||||
<tr><td><strong>model</strong></td>
|
||||
<td><tt class="docutils literal">Char</tt></td>
|
||||
<td>Model name, e.g.
|
||||
<tt class="docutils literal">'res.partner'</tt></td>
|
||||
</tr>
|
||||
<tr><td><strong>view_types</strong></td>
|
||||
<td><tt class="docutils literal">List of Char</tt>
|
||||
<em>(optional)</em></td>
|
||||
<td>View types to reload,
|
||||
e.g.
|
||||
<tt class="docutils literal">["form", "kanban"]</tt>.
|
||||
Leave blank to reload
|
||||
all views.</td>
|
||||
</tr>
|
||||
<tr><td><strong>rec_ids</strong></td>
|
||||
<td><tt class="docutils literal">List of Integer</tt>
|
||||
<em>(optional)</em></td>
|
||||
<td>The view will be
|
||||
reloaded only if a
|
||||
record with an ID from
|
||||
this list is present in
|
||||
the view.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="docutils" />
|
||||
<div class="section" id="important-notes">
|
||||
<h2>⚠️ Important Notes</h2>
|
||||
<p>Use this function <strong>wisely</strong>.</p>
|
||||
<p>When reloading <strong>form views</strong>, be aware that if a user is currently
|
||||
editing a record, <strong>their unsaved updates may be lost</strong> when the form
|
||||
reloads from the server (no confirmation dialog is shown).</p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1>Usage</h1>
|
||||
<div class="section" id="example-usage">
|
||||
<h2>🧩 Example Usage</h2>
|
||||
<p>Below is a code snippet showing how to use the <tt class="docutils literal">reload_views</tt> helper
|
||||
function.</p>
|
||||
<pre class="code python literal-block">
|
||||
<span class="c1"># Reload the kanban and form views for all salespeople when an opportunity is won</span><span class="w">
|
||||
</span><span class="c1"># Will reload views only if the current opportunity is being displayed</span><span class="w">
|
||||
|
||||
</span><span class="n">group_id</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">ref</span><span class="p">(</span><span class="s2">"sales_team.group_sale_salesman"</span><span class="p">)</span><span class="o">.</span><span class="n">id</span><span class="w">
|
||||
</span><span class="n">users_to_reload</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">env</span><span class="p">[</span><span class="s2">"res.users"</span><span class="p">]</span><span class="o">.</span><span class="n">search</span><span class="p">([(</span><span class="s2">"groups_id"</span><span class="p">,</span> <span class="s2">"in"</span><span class="p">,</span> <span class="p">[</span><span class="n">group_id</span><span class="p">])])</span><span class="w">
|
||||
</span><span class="n">users_to_reload</span><span class="o">.</span><span class="n">reload_views</span><span class="p">(</span><span class="w">
|
||||
</span> <span class="n">model</span><span class="o">=</span><span class="s2">"crm.lead"</span><span class="p">,</span><span class="w">
|
||||
</span> <span class="n">view_types</span><span class="o">=</span><span class="p">[</span><span class="s2">"kanban"</span><span class="p">,</span> <span class="s2">"form"</span><span class="p">],</span><span class="w">
|
||||
</span> <span class="n">rec_ids</span><span class="o">=</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">ids</span><span class="p">],</span><span class="w">
|
||||
</span><span class="p">)</span>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1>Bug Tracker</h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/cetmix/cetmix-tower/issues/new?body=module:%20cx_web_refresh_from_backend%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1>Credits</h1>
|
||||
<div class="section" id="authors">
|
||||
<h2>Authors</h2>
|
||||
<ul class="simple">
|
||||
<li>Cetmix</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2>Maintainers</h2>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/cetmix/cetmix-tower/tree/18.0/cx_web_refresh_from_backend">cetmix/cetmix-tower</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,161 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import {FormController} from "@web/views/form/form_controller";
|
||||
import {isResIdInRecIds} from "../utils/get_loaded_record_ids.esm";
|
||||
import {onWillUnmount} from "@odoo/owl";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
import {_t} from "@web/core/l10n/translation";
|
||||
|
||||
patch(FormController.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
|
||||
// Bus_service is async; useService("bus_service") breaks (SERVICES_METADATA).
|
||||
this.busService = this.env.services.bus_service;
|
||||
this.notificationService = useService("notification");
|
||||
|
||||
this._lastLocalSave = null;
|
||||
this._isRefreshInFlight = false;
|
||||
this._hasRefreshQueued = false;
|
||||
|
||||
this._boundBusHandler = this._onWebRefreshNotification.bind(this);
|
||||
this.busService.subscribe("web.refresh_view", this._boundBusHandler);
|
||||
|
||||
onWillUnmount(() => {
|
||||
if (this.busService && this._boundBusHandler) {
|
||||
this.busService.unsubscribe("web.refresh_view", this._boundBusHandler);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a web.refresh_view bus notification for this form.
|
||||
* Called once per notification; coalesces concurrent refreshes via _queueRefresh.
|
||||
*
|
||||
* @param {Object} payload - Notification payload {model, view_types, rec_ids}
|
||||
*/
|
||||
async _onWebRefreshNotification(payload) {
|
||||
if (!this.model || !this.model.root) {
|
||||
return;
|
||||
}
|
||||
if (this._shouldRefreshView(payload)) {
|
||||
await this._queueRefresh("refreshForm");
|
||||
}
|
||||
},
|
||||
|
||||
async _queueRefresh(methodName) {
|
||||
if (this._isRefreshInFlight) {
|
||||
this._hasRefreshQueued = true;
|
||||
return;
|
||||
}
|
||||
this._isRefreshInFlight = true;
|
||||
try {
|
||||
do {
|
||||
this._hasRefreshQueued = false;
|
||||
await this[methodName]();
|
||||
} while (this._hasRefreshQueued);
|
||||
} finally {
|
||||
this._isRefreshInFlight = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether a refresh notification is relevant to this form.
|
||||
*
|
||||
* Returns true when all of the following hold:
|
||||
* - model matches current form model
|
||||
* - requested view types include "form" (or none specified)
|
||||
* - record id matches current record (or none specified)
|
||||
* - form is not inside a dialog / wizard
|
||||
*
|
||||
* @param {Object} payload - Notification payload
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_shouldRefreshView(payload) {
|
||||
const {model, view_types = [], rec_ids = []} = payload;
|
||||
|
||||
if (this.props.resModel !== model) {
|
||||
return false;
|
||||
}
|
||||
if (view_types.length > 0 && !view_types.includes("form")) {
|
||||
return false;
|
||||
}
|
||||
const currentResId = this.model && this.model.root && this.model.root.resId;
|
||||
if (rec_ids.length > 0 && !isResIdInRecIds(currentResId, rec_ids)) {
|
||||
return false;
|
||||
}
|
||||
// Skip refresh when form is in a dialog or when a wizard is on top
|
||||
// of the stack. Refreshing in that context can leave wizard/confirmation
|
||||
// dialogs stuck open (e.g. confirm="..." in wizard view).
|
||||
if (this.env.inDialog) {
|
||||
return false;
|
||||
}
|
||||
const currentController = this.actionService.currentController;
|
||||
const currentAction = currentController && currentController.action;
|
||||
if (currentAction && currentAction.target === "new") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh the form with actual data from server.
|
||||
*
|
||||
* Reloads without confirmation even when the record is dirty (client changes
|
||||
* may be discarded). Dialog / wizard forms are filtered out in
|
||||
* _shouldRefreshView().
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async refreshForm() {
|
||||
if (this._lastLocalSave && Date.now() - this._lastLocalSave < 2500) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.model || !this.model.root) {
|
||||
return;
|
||||
}
|
||||
|
||||
const record = this.model.root;
|
||||
|
||||
try {
|
||||
await record.load();
|
||||
} catch (error) {
|
||||
this.notificationService.add(this._getRefreshErrorMessage(error), {
|
||||
type: "danger",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.model && this.model.root) {
|
||||
this.render(true);
|
||||
}
|
||||
},
|
||||
|
||||
_getRefreshErrorMessage(error) {
|
||||
const message =
|
||||
(error && error.data && error.data.message) ||
|
||||
(error && error.message) ||
|
||||
String(error);
|
||||
return _t("Could not reload form. %(message)s", {message});
|
||||
},
|
||||
|
||||
/**
|
||||
* Override of save button handler.
|
||||
*
|
||||
* After a successful save, stores a timestamp to avoid immediate auto-refresh
|
||||
* triggered by our own write (bus notification). Failed saves leave the
|
||||
* timestamp unchanged so refresh suppression does not apply incorrectly.
|
||||
*
|
||||
* @param {Object} params - Save options
|
||||
* @returns {Promise<Boolean|undefined>} Result of the core save (truthy when save succeeded)
|
||||
*/
|
||||
async saveButtonClicked(params) {
|
||||
const result = await super.saveButtonClicked(params);
|
||||
if (result) {
|
||||
this._lastLocalSave = Date.now();
|
||||
}
|
||||
return result;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import {
|
||||
getLoadedRecordIds,
|
||||
hasAnyLoadedIdInRecIds,
|
||||
} from "../utils/get_loaded_record_ids.esm";
|
||||
import {KanbanController} from "@web/views/kanban/kanban_controller";
|
||||
import {onWillUnmount} from "@odoo/owl";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
import {_t} from "@web/core/l10n/translation";
|
||||
|
||||
patch(KanbanController.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
// Bus_service is async; useService("bus_service") breaks (SERVICES_METADATA).
|
||||
this.busService = this.env.services.bus_service;
|
||||
this.notificationService = useService("notification");
|
||||
this._isRefreshInFlight = false;
|
||||
this._hasRefreshQueued = false;
|
||||
|
||||
this._boundBusHandler = this._onWebRefreshNotification.bind(this);
|
||||
this.busService.subscribe("web.refresh_view", this._boundBusHandler);
|
||||
|
||||
onWillUnmount(() => {
|
||||
if (this.busService && this._boundBusHandler) {
|
||||
this.busService.unsubscribe("web.refresh_view", this._boundBusHandler);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a web.refresh_view bus notification for this kanban.
|
||||
* Called once per notification; coalesces concurrent refreshes via _queueRefresh.
|
||||
*
|
||||
* @param {Object} payload - Notification payload {model, view_types, rec_ids}
|
||||
*/
|
||||
async _onWebRefreshNotification(payload) {
|
||||
if (!this.model || !this.model.root) {
|
||||
return;
|
||||
}
|
||||
if (this._shouldRefreshView(payload)) {
|
||||
await this._queueRefresh("refreshList");
|
||||
}
|
||||
},
|
||||
|
||||
async _queueRefresh(methodName) {
|
||||
if (this._isRefreshInFlight) {
|
||||
this._hasRefreshQueued = true;
|
||||
return;
|
||||
}
|
||||
this._isRefreshInFlight = true;
|
||||
try {
|
||||
do {
|
||||
this._hasRefreshQueued = false;
|
||||
await this[methodName]();
|
||||
} while (this._hasRefreshQueued);
|
||||
} finally {
|
||||
this._isRefreshInFlight = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether a refresh notification is relevant to this kanban.
|
||||
*
|
||||
* Returns true when all of the following hold:
|
||||
* - model matches current kanban model
|
||||
* - requested view types include "kanban" (or none specified)
|
||||
* - at least one loaded record id is in rec_ids (or none specified)
|
||||
*
|
||||
* @param {Object} payload - Notification payload
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_shouldRefreshView(payload) {
|
||||
const {model, view_types = [], rec_ids = []} = payload;
|
||||
|
||||
if (this.props.resModel !== model) {
|
||||
return false;
|
||||
}
|
||||
if (view_types.length > 0 && !view_types.includes("kanban")) {
|
||||
return false;
|
||||
}
|
||||
if (rec_ids.length > 0) {
|
||||
const loadedIds = getLoadedRecordIds(this.model.root);
|
||||
if (!hasAnyLoadedIdInRecIds(loadedIds, rec_ids)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh the kanban with actual data from server.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async refreshList() {
|
||||
if (!this.model || !this.model.root) {
|
||||
return;
|
||||
}
|
||||
|
||||
const list = this.model.root;
|
||||
|
||||
try {
|
||||
await list.load();
|
||||
} catch (error) {
|
||||
this.notificationService.add(this._getRefreshErrorMessage(error), {
|
||||
type: "danger",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.model && this.model.root) {
|
||||
this.render(true);
|
||||
}
|
||||
},
|
||||
|
||||
_getRefreshErrorMessage(error) {
|
||||
const message =
|
||||
(error && error.data && error.data.message) ||
|
||||
(error && error.message) ||
|
||||
String(error);
|
||||
return _t("Could not reload kanban. %(message)s", {message});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import {
|
||||
getLoadedRecordIds,
|
||||
hasAnyLoadedIdInRecIds,
|
||||
} from "../utils/get_loaded_record_ids.esm";
|
||||
import {ConfirmationDialog} from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import {ListController} from "@web/views/list/list_controller";
|
||||
import {onWillUnmount} from "@odoo/owl";
|
||||
import {patch} from "@web/core/utils/patch";
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
import {_t} from "@web/core/l10n/translation";
|
||||
|
||||
patch(ListController.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
// Bus_service is async; useService("bus_service") breaks (SERVICES_METADATA).
|
||||
this.busService = this.env.services.bus_service;
|
||||
this.notificationService = useService("notification");
|
||||
this._isRefreshInFlight = false;
|
||||
this._hasRefreshQueued = false;
|
||||
|
||||
this._boundBusHandler = this._onWebRefreshNotification.bind(this);
|
||||
this.busService.subscribe("web.refresh_view", this._boundBusHandler);
|
||||
|
||||
onWillUnmount(() => {
|
||||
if (this.busService && this._boundBusHandler) {
|
||||
this.busService.unsubscribe("web.refresh_view", this._boundBusHandler);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle a web.refresh_view bus notification for this list.
|
||||
* Called once per notification; coalesces concurrent refreshes via _queueRefresh.
|
||||
*
|
||||
* @param {Object} payload - Notification payload {model, view_types, rec_ids}
|
||||
*/
|
||||
async _onWebRefreshNotification(payload) {
|
||||
if (!this.model || !this.model.root) {
|
||||
return;
|
||||
}
|
||||
if (this._shouldRefreshView(payload)) {
|
||||
await this._queueRefresh("refreshList");
|
||||
}
|
||||
},
|
||||
|
||||
async _queueRefresh(methodName) {
|
||||
if (this._isRefreshInFlight) {
|
||||
this._hasRefreshQueued = true;
|
||||
return;
|
||||
}
|
||||
this._isRefreshInFlight = true;
|
||||
try {
|
||||
do {
|
||||
this._hasRefreshQueued = false;
|
||||
await this[methodName]();
|
||||
} while (this._hasRefreshQueued);
|
||||
} finally {
|
||||
this._isRefreshInFlight = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether a refresh notification is relevant to this list.
|
||||
*
|
||||
* Returns true when all of the following hold:
|
||||
* - model matches current list model
|
||||
* - requested view types include "list" or "tree" (or none specified)
|
||||
* - at least one loaded record id is in rec_ids (or none specified)
|
||||
*
|
||||
* @param {Object} payload - Notification payload
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_shouldRefreshView(payload) {
|
||||
const {model, view_types = [], rec_ids = []} = payload;
|
||||
|
||||
if (this.props.resModel !== model) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
view_types.length > 0 &&
|
||||
!view_types.includes("list") &&
|
||||
!view_types.includes("tree")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (rec_ids.length > 0) {
|
||||
const loadedIds = getLoadedRecordIds(this.model.root);
|
||||
if (!hasAnyLoadedIdInRecIds(loadedIds, rec_ids)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh the list with actual data from server.
|
||||
* If there is an edited record, asks the user to save or cancel.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async refreshList() {
|
||||
if (!this.model || !this.model.root) {
|
||||
return;
|
||||
}
|
||||
|
||||
const list = this.model.root;
|
||||
|
||||
if (list.editedRecord) {
|
||||
const confirmed = await this._confirmListRefresh();
|
||||
|
||||
if (!confirmed) {
|
||||
// User declined: drop coalesced refreshes queued during the dialog.
|
||||
this._hasRefreshQueued = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await list.editedRecord.save();
|
||||
} catch (error) {
|
||||
this.notificationService.add(this._getSaveErrorMessage(error), {
|
||||
type: "danger",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await list.load();
|
||||
} catch (error) {
|
||||
this.notificationService.add(this._getReloadErrorMessage(error), {
|
||||
type: "danger",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.model && this.model.root) {
|
||||
this.render(true);
|
||||
}
|
||||
},
|
||||
|
||||
async _confirmListRefresh() {
|
||||
return await new Promise((resolve) => {
|
||||
this.dialogService.add(ConfirmationDialog, {
|
||||
title: _t("List is being refreshed from backend"),
|
||||
body: _t("You have unsaved edits. Save them before refreshing?"),
|
||||
confirm: () => resolve(true),
|
||||
cancel: () => resolve(false),
|
||||
confirmLabel: _t("Save & Refresh"),
|
||||
cancelLabel: _t("Cancel"),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_getSaveErrorMessage(error) {
|
||||
const message =
|
||||
(error && error.data && error.data.message) ||
|
||||
(error && error.message) ||
|
||||
String(error);
|
||||
return _t("Could not save record. %(message)s", {message});
|
||||
},
|
||||
|
||||
_getReloadErrorMessage(error) {
|
||||
const message =
|
||||
(error && error.data && error.data.message) ||
|
||||
(error && error.message) ||
|
||||
String(error);
|
||||
return _t("Could not reload list. %(message)s", {message});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
/** @odoo-module */
|
||||
|
||||
/**
|
||||
* Get IDs of records currently loaded in list-like root models.
|
||||
* Supports both plain and grouped datasets.
|
||||
*
|
||||
* @param {Object} root - View root model (list/kanban)
|
||||
* @returns {Array<Number>}
|
||||
*/
|
||||
export function getLoadedRecordIds(root) {
|
||||
if (root.isGrouped) {
|
||||
const recordIds = [];
|
||||
const collectIds = (groups) => {
|
||||
for (const group of groups) {
|
||||
if (group.list && group.list.records) {
|
||||
recordIds.push(...group.list.records.map((record) => record.resId));
|
||||
}
|
||||
if (group.groups) {
|
||||
collectIds(group.groups);
|
||||
}
|
||||
}
|
||||
};
|
||||
collectIds(root.groups);
|
||||
return recordIds;
|
||||
}
|
||||
return root.records.map((record) => record.resId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether any loaded record id is present in the notification id list.
|
||||
* Uses a Set for O(n + m) membership checks instead of O(n * m) with includes.
|
||||
*
|
||||
* @param {Number[]} loadedIds - IDs currently visible in the view
|
||||
* @param {Number[]} rec_ids - IDs from the bus payload
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function hasAnyLoadedIdInRecIds(loadedIds, rec_ids) {
|
||||
const recIdSet = new Set(rec_ids);
|
||||
return loadedIds.some((id) => recIdSet.has(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a single record id is in the notification id list.
|
||||
* Uses a Set for O(m) build + O(1) lookup vs repeated includes.
|
||||
*
|
||||
* @param {Number|undefined|false} resId - Current record id (e.g. form root)
|
||||
* @param {Number[]} rec_ids - IDs from the bus payload
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
export function isResIdInRecIds(resId, rec_ids) {
|
||||
if (!resId) {
|
||||
return false;
|
||||
}
|
||||
return new Set(rec_ids).has(resId);
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/** @odoo-module */
|
||||
/* global QUnit */
|
||||
|
||||
import "cx_web_refresh_from_backend/static/src/views/form/form_controller_patch.esm";
|
||||
import "cx_web_refresh_from_backend/static/src/views/kanban/kanban_controller_patch.esm";
|
||||
import "cx_web_refresh_from_backend/static/src/views/list/list_controller_patch.esm";
|
||||
|
||||
import {
|
||||
editInput,
|
||||
getFixture,
|
||||
makeDeferred,
|
||||
nextTick,
|
||||
} from "@web/../tests/helpers/utils";
|
||||
import {
|
||||
makeView,
|
||||
makeViewInDialog,
|
||||
setupViewRegistries,
|
||||
} from "@web/../tests/views/helpers";
|
||||
|
||||
let serverData = null;
|
||||
let target = null;
|
||||
|
||||
/**
|
||||
* Simulate a web.refresh_view notification on the patched controller.
|
||||
*
|
||||
* The unit tests exercise the controller filtering and refresh logic, so they
|
||||
* can call the public notification handler directly instead of reproducing the
|
||||
* bus service internals.
|
||||
*
|
||||
* @param {Object} controller - Patched view controller instance
|
||||
* @param {Object} payload - {model, view_types, rec_ids}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function triggerRefresh(controller, payload) {
|
||||
return controller._onWebRefreshNotification(payload);
|
||||
}
|
||||
|
||||
QUnit.module("cx_web_refresh_from_backend", (hooks) => {
|
||||
hooks.beforeEach(() => {
|
||||
serverData = {
|
||||
models: {
|
||||
"res.partner": {
|
||||
fields: {
|
||||
name: {string: "Name", type: "char"},
|
||||
},
|
||||
records: [
|
||||
{id: 1, name: "Partner 1"},
|
||||
{id: 2, name: "Partner 2"},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
setupViewRegistries();
|
||||
target = getFixture();
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"form: refresh runs only for matching notifications",
|
||||
async function (assert) {
|
||||
const form = await makeView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: '<form><field name="name"/></form>',
|
||||
});
|
||||
|
||||
let refreshCalls = 0;
|
||||
form.refreshForm = async () => {
|
||||
refreshCalls++;
|
||||
};
|
||||
|
||||
triggerRefresh(form, {
|
||||
model: "res.users",
|
||||
view_types: ["form"],
|
||||
rec_ids: [1],
|
||||
});
|
||||
triggerRefresh(form, {
|
||||
model: "res.partner",
|
||||
view_types: ["list"],
|
||||
rec_ids: [1],
|
||||
});
|
||||
triggerRefresh(form, {
|
||||
model: "res.partner",
|
||||
view_types: ["form"],
|
||||
rec_ids: [2],
|
||||
});
|
||||
triggerRefresh(form, {
|
||||
model: "res.partner",
|
||||
view_types: ["form"],
|
||||
rec_ids: [1],
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(refreshCalls, 1);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"form in dialog: matching notification is ignored",
|
||||
async function (assert) {
|
||||
const form = await makeViewInDialog({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: '<form><field name="name"/></form>',
|
||||
});
|
||||
|
||||
let refreshCalls = 0;
|
||||
form.refreshForm = async () => {
|
||||
refreshCalls++;
|
||||
};
|
||||
|
||||
triggerRefresh(form, {
|
||||
model: "res.partner",
|
||||
view_types: ["form"],
|
||||
rec_ids: [1],
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(refreshCalls, 0);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test(
|
||||
"form: dirty form reloads from backend without confirmation dialog",
|
||||
async function (assert) {
|
||||
const form = await makeView({
|
||||
type: "form",
|
||||
resModel: "res.partner",
|
||||
serverData,
|
||||
resId: 1,
|
||||
arch: '<form><field name="name"/></form>',
|
||||
});
|
||||
|
||||
await form.model.root.switchMode("edit");
|
||||
await editInput(
|
||||
target,
|
||||
".o_field_widget[name='name'] input",
|
||||
"Changed Name"
|
||||
);
|
||||
|
||||
triggerRefresh(form, {
|
||||
model: "res.partner",
|
||||
view_types: ["form"],
|
||||
rec_ids: [1],
|
||||
});
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
assert.containsNone(
|
||||
target,
|
||||
".modal",
|
||||
"backend refresh must not open a confirmation dialog"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("list: burst notifications are coalesced", async function (assert) {
|
||||
const list = await makeView({
|
||||
type: "list",
|
||||
resModel: "res.partner",
|
||||
serverData,
|
||||
arch: '<list><field name="name"/></list>',
|
||||
});
|
||||
|
||||
const deferred = makeDeferred();
|
||||
let refreshCalls = 0;
|
||||
list.refreshList = async () => {
|
||||
refreshCalls++;
|
||||
if (refreshCalls === 1) {
|
||||
await deferred;
|
||||
}
|
||||
};
|
||||
|
||||
const payload = {model: "res.partner", view_types: ["list"], rec_ids: [1]};
|
||||
triggerRefresh(list, payload);
|
||||
triggerRefresh(list, payload);
|
||||
triggerRefresh(list, payload);
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(
|
||||
refreshCalls,
|
||||
1,
|
||||
"only one refresh should run while in flight"
|
||||
);
|
||||
|
||||
deferred.resolve();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(
|
||||
refreshCalls,
|
||||
2,
|
||||
"one additional refresh should run after in-flight refresh finishes"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test("kanban: burst notifications are coalesced", async function (assert) {
|
||||
const kanban = await makeView({
|
||||
type: "kanban",
|
||||
resModel: "res.partner",
|
||||
serverData,
|
||||
arch: '<kanban><templates><t t-name="card"><div><field name="name"/></div></t></templates></kanban>',
|
||||
});
|
||||
|
||||
const deferred = makeDeferred();
|
||||
let refreshCalls = 0;
|
||||
kanban.refreshList = async () => {
|
||||
refreshCalls++;
|
||||
if (refreshCalls === 1) {
|
||||
await deferred;
|
||||
}
|
||||
};
|
||||
|
||||
const payload = {model: "res.partner", view_types: ["kanban"], rec_ids: [1]};
|
||||
triggerRefresh(kanban, payload);
|
||||
triggerRefresh(kanban, payload);
|
||||
triggerRefresh(kanban, payload);
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(
|
||||
refreshCalls,
|
||||
1,
|
||||
"only one refresh should run while in flight"
|
||||
);
|
||||
|
||||
deferred.resolve();
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
|
||||
assert.strictEqual(
|
||||
refreshCalls,
|
||||
2,
|
||||
"one additional refresh should run after in-flight refresh finishes"
|
||||
);
|
||||
});
|
||||
});
|
||||
4
addons/cx_web_refresh_from_backend/tests/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright 2025 Cetmix OÜ
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from . import test_reload_views
|
||||
@@ -0,0 +1,78 @@
|
||||
# Copyright 2025 Cetmix OÜ
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
from odoo.addons.base.tests.common import BaseCommon
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestReloadViews(BaseCommon):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.user_admin = cls.env.ref("base.user_admin")
|
||||
cls.user_demo = cls.env["res.users"].create(
|
||||
{
|
||||
"name": "Test User",
|
||||
"login": "test_refresh_user",
|
||||
"email": "test_refresh@example.com",
|
||||
}
|
||||
)
|
||||
|
||||
def test_reload_views_basic(self):
|
||||
"""Test basic reload_views call without parameters"""
|
||||
with patch.object(type(self.env["bus.bus"]), "_sendone") as mock_sendone:
|
||||
self.user_admin.reload_views(model="res.partner")
|
||||
|
||||
mock_sendone.assert_called_once()
|
||||
partner, channel, message = mock_sendone.call_args[0]
|
||||
self.assertEqual(partner, self.user_admin.partner_id)
|
||||
self.assertEqual(channel, "web.refresh_view")
|
||||
self.assertEqual(message["model"], "res.partner")
|
||||
self.assertEqual(message["view_types"], [])
|
||||
self.assertEqual(message["rec_ids"], [])
|
||||
|
||||
def test_reload_views_with_params(self):
|
||||
"""Test reload_views with view_types and rec_ids parameters"""
|
||||
with patch.object(type(self.env["bus.bus"]), "_sendone") as mock_sendone:
|
||||
self.user_admin.reload_views(
|
||||
model="res.partner",
|
||||
view_types=["form", "kanban"],
|
||||
rec_ids=[self.partner.id],
|
||||
)
|
||||
|
||||
mock_sendone.assert_called_once()
|
||||
message = mock_sendone.call_args[0][2]
|
||||
self.assertEqual(message["view_types"], ["form", "kanban"])
|
||||
self.assertEqual(message["rec_ids"], [self.partner.id])
|
||||
|
||||
def test_reload_views_recordset(self):
|
||||
"""Test reload_views on a multi-record user recordset.
|
||||
|
||||
Ensures that calling reload_views on a recordset sends one notification
|
||||
per user through _sendone.
|
||||
"""
|
||||
users = self.user_admin | self.user_demo
|
||||
|
||||
with patch.object(type(self.env["bus.bus"]), "_sendone") as mock_sendone:
|
||||
users.reload_views(model="res.partner")
|
||||
|
||||
self.assertEqual(mock_sendone.call_count, 2)
|
||||
|
||||
# Verify both users' partners are notified and payload is correct.
|
||||
notified_partners = set()
|
||||
for call in mock_sendone.call_args_list:
|
||||
partner, channel, message = call[0]
|
||||
notified_partners.add(partner)
|
||||
self.assertEqual(channel, "web.refresh_view")
|
||||
self.assertEqual(message["model"], "res.partner")
|
||||
self.assertEqual(message["view_types"], [])
|
||||
self.assertEqual(message["rec_ids"], [])
|
||||
self.assertEqual(len(notified_partners), 2)
|
||||
self.assertEqual(
|
||||
notified_partners,
|
||||
{self.user_admin.partner_id, self.user_demo.partner_id},
|
||||
)
|
||||
15
addons/ks_dashboard_ninja/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import controllers
|
||||
from . import common_lib
|
||||
from . import wizard
|
||||
|
||||
from odoo.api import Environment, SUPERUSER_ID
|
||||
|
||||
|
||||
def uninstall_hook(env):
|
||||
# env = Environment(cr, SUPERUSER_ID, {})
|
||||
for rec in env['ks_dashboard_ninja.board'].search([]):
|
||||
rec.ks_dashboard_client_action_id.unlink()
|
||||
rec.ks_dashboard_menu_id.unlink()
|
||||
191
addons/ks_dashboard_ninja/__manifest__.py
Normal file
@@ -0,0 +1,191 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': 'Dashboard Ninja with AI',
|
||||
|
||||
'summary': """
|
||||
Ksolves Dashboard Ninja gives you a wide-angle view of your business that you might have missed. Get smart visual data with interactive and engaging dashboards for your Odoo ERP. Odoo Dashboard, CRM Dashboard, Inventory Dashboard, Sales Dashboard, Account Dashboard, Invoice Dashboard, Revamp Dashboard, Best Dashboard, Odoo Best Dashboard, Odoo Apps Dashboard, Best Ninja Dashboard, Analytic Dashboard, Pre-Configured Dashboard, Create Dashboard, Beautiful Dashboard, Customized Robust Dashboard, Predefined Dashboard, Multiple Dashboards, Advance Dashboard, Beautiful Powerful Dashboards, Chart Graphs Table View, All In One Dynamic Dashboard, Accounting Stock Dashboard, Pie Chart Dashboard, Modern Dashboard, Dashboard Studio, Dashboard Builder, Dashboard Designer, Odoo Studio. Revamp your Odoo Dashboard like never before! It is one of the best dashboard odoo apps in the market.
|
||||
""",
|
||||
|
||||
'description': """
|
||||
Dashboard Ninja v18.0,
|
||||
Odoo Dashboard,
|
||||
Dashboard,
|
||||
Dashboards,
|
||||
Odoo apps,
|
||||
Dashboard app,
|
||||
HR Dashboard,
|
||||
Sales Dashboard,
|
||||
inventory Dashboard,
|
||||
Lead Dashboard,
|
||||
Opportunity Dashboard,
|
||||
CRM Dashboard,
|
||||
POS,
|
||||
POS Dashboard,
|
||||
Connectors,
|
||||
Web Dynamic,
|
||||
Report Import/Export,
|
||||
Date Filter,
|
||||
HR,
|
||||
Sales,
|
||||
Theme,
|
||||
Tile Dashboard,
|
||||
Dashboard Widgets,
|
||||
Dashboard Manager,
|
||||
Debranding,
|
||||
Customize Dashboard,
|
||||
Graph Dashboard,
|
||||
Charts Dashboard,
|
||||
Invoice Dashboard,
|
||||
Project management,
|
||||
ksolves,
|
||||
ksolves apps,
|
||||
Ksolves India Ltd.
|
||||
Ksolves India Limited,
|
||||
odoo dashboard apps
|
||||
odoo dashboard app
|
||||
odoo dashboard module
|
||||
odoo modules
|
||||
dashboards
|
||||
powerful dashboards
|
||||
beautiful odoo dashboard
|
||||
odoo dynamic dashboard
|
||||
all in one dashboard
|
||||
multiple dashboard menu
|
||||
odoo dashboard portal
|
||||
beautiful odoo dashboard
|
||||
odoo best dashboard
|
||||
dashboard for management
|
||||
Odoo custom dashboard
|
||||
odoo dashboard management
|
||||
odoo dashboard apps
|
||||
create odoo dashboard
|
||||
odoo dashboard extension
|
||||
odoo dashboard module
|
||||
""",
|
||||
|
||||
'author': 'Ksolves India Ltd.',
|
||||
|
||||
'license': 'OPL-1',
|
||||
|
||||
'currency': 'EUR',
|
||||
|
||||
'price': '518.62',
|
||||
|
||||
'website': 'https://store.ksolves.com/',
|
||||
|
||||
'maintainer': 'Ksolves India Ltd.',
|
||||
|
||||
'live_test_url': 'https://ksdndemo18.kappso.com/web/demo_login',
|
||||
|
||||
'category': 'Services',
|
||||
'version': '18.0.1.1.7',
|
||||
|
||||
'support': 'sales@ksolves.com',
|
||||
|
||||
'images': ['static/description/output.gif'],
|
||||
|
||||
'depends': ['base', 'web', 'base_setup', 'bus', 'base_geolocalize', 'mail'],
|
||||
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'security/ks_security_groups.xml',
|
||||
'data/ks_default_data.xml',
|
||||
'data/ks_mail_cron.xml',
|
||||
'data/dn_data.xml',
|
||||
'data/sequence.xml',
|
||||
'views/res_settings.xml',
|
||||
'views/ks_dashboard_ninja_view.xml',
|
||||
'views/ks_dashboard_ninja_item_view.xml',
|
||||
'views/ks_dashboard_group_by.xml',
|
||||
'views/ks_dashboard_csv_group_by.xml',
|
||||
'views/ks_dashboard_action.xml',
|
||||
'views/ks_import_dashboard_view.xml',
|
||||
'wizard/ks_create_dashboard_wiz_view.xml',
|
||||
'wizard/ks_duplicate_dashboard_wiz_view.xml',
|
||||
'views/ks_ai_dashboard.xml',
|
||||
'views/ks_whole_ai_dashboard.xml',
|
||||
'views/ks_key_fetch.xml',
|
||||
'views/webExtend.xml'
|
||||
],
|
||||
|
||||
'demo': ['demo/ks_dashboard_ninja_demo.xml'],
|
||||
|
||||
'assets': {
|
||||
'ks_dashboard_ninja.ks_dashboard_lib': [
|
||||
'/ks_dashboard_ninja/static/lib/css/gridstack.min.css',
|
||||
'/ks_dashboard_ninja/static/lib/js/gridstack-h5.js',
|
||||
'/ks_dashboard_ninja/static/lib/js/pdfmake.min.js',
|
||||
'/ks_dashboard_ninja/static/lib/js/vfs_fonts.js',
|
||||
'ks_dashboard_ninja/static/lib/js/Animated.js',
|
||||
'ks_dashboard_ninja/static/lib/js/worldLow.js',
|
||||
'ks_dashboard_ninja/static/lib/js/map.js',
|
||||
'ks_dashboard_ninja/static/lib/js/index.js',
|
||||
'ks_dashboard_ninja/static/lib/js/pdfmake.js',
|
||||
'ks_dashboard_ninja/static/lib/js/percent.js',
|
||||
'ks_dashboard_ninja/static/lib/js/pdf.min.js',
|
||||
'ks_dashboard_ninja/static/lib/js/print.min.js',
|
||||
'ks_dashboard_ninja/static/lib/js/Dataviz.js',
|
||||
'ks_dashboard_ninja/static/lib/js/Material.js',
|
||||
'ks_dashboard_ninja/static/lib/js/Moonrise.js',
|
||||
'ks_dashboard_ninja/static/lib/js/xy.js',
|
||||
'ks_dashboard_ninja/static/lib/js/radar.js',
|
||||
],
|
||||
'web.assets_backend': [
|
||||
'web/static/lib/jquery/jquery.js',
|
||||
'ks_dashboard_ninja/static/src/scss/variable.scss',
|
||||
'ks_dashboard_ninja/static/src/css/ks_dashboard_ninja.scss',
|
||||
'ks_dashboard_ninja/static/src/css/ks_dashboard_ninja_item.css',
|
||||
'ks_dashboard_ninja/static/src/css/ks_icon_container_modal.css',
|
||||
'ks_dashboard_ninja/static/src/css/ks_dashboard_item_theme.css',
|
||||
'ks_dashboard_ninja/static/src/css/ks_input_bar.css',
|
||||
'ks_dashboard_ninja/static/src/css/ks_ai_dash.css',
|
||||
'ks_dashboard_ninja/static/src/css/ks_dn_filter.css',
|
||||
'ks_dashboard_ninja/static/src/css/ks_toggle_icon.css',
|
||||
'ks_dashboard_ninja/static/src/css/ks_flower_view.css',
|
||||
'ks_dashboard_ninja/static/src/css/ks_map_view.css',
|
||||
'ks_dashboard_ninja/static/src/css/ks_funnel_view.css',
|
||||
'ks_dashboard_ninja/static/src/css/ks_dashboard_options.css',
|
||||
'ks_dashboard_ninja/static/src/css/ks_dashboard_ninja_pro.css',
|
||||
'ks_dashboard_ninja/static/src/css/ks_to_do_item.css',
|
||||
'ks_dashboard_ninja/static/src/scss/common.scss',
|
||||
'/ks_dashboard_ninja/static/src/scss/explainAi.scss',
|
||||
'/ks_dashboard_ninja/static/src/scss/chat_with_ai.scss',
|
||||
'/ks_dashboard_ninja/static/src/scss/Generate-ai.scss',
|
||||
'/ks_dashboard_ninja/static/src/scss/ks_ai_dashboard.scss',
|
||||
'ks_dashboard_ninja/static/src/css/style.css',
|
||||
'ks_dashboard_ninja/static/src/js/ks_global_functions.js',
|
||||
'ks_dashboard_ninja/static/lib/js/index.js',
|
||||
'ks_dashboard_ninja/static/lib/js/pdfmake.js',
|
||||
'ks_dashboard_ninja/static/lib/js/percent.js',
|
||||
'ks_dashboard_ninja/static/lib/js/pdf.min.js',
|
||||
'ks_dashboard_ninja/static/lib/js/print.min.js',
|
||||
'ks_dashboard_ninja/static/lib/js/Dataviz.js',
|
||||
'ks_dashboard_ninja/static/lib/js/Material.js',
|
||||
'ks_dashboard_ninja/static/lib/js/Moonrise.js',
|
||||
'ks_dashboard_ninja/static/lib/js/exporting.js',
|
||||
'ks_dashboard_ninja/static/lib/js/pdfmake.js',
|
||||
'ks_dashboard_ninja/static/lib/js/percent.js',
|
||||
'ks_dashboard_ninja/static/src/js/ks_global_functions.js',
|
||||
'ks_dashboard_ninja/static/lib/js/xy.js',
|
||||
'ks_dashboard_ninja/static/lib/js/radar.js',
|
||||
'ks_dashboard_ninja/static/src/js/domainfix.js',
|
||||
'ks_dashboard_ninja/static/src/js/chart_buttons_patch.js',
|
||||
'ks_dashboard_ninja/static/src/xml/**/*',
|
||||
'ks_dashboard_ninja/static/src/css/ks_radial_chart.css',
|
||||
'ks_dashboard_ninja/static/src/js/ks_ai_dash_action.js',
|
||||
'ks_dashboard_ninja/static/src/components/**/*',
|
||||
'ks_dashboard_ninja/static/src/widgets/**/*',
|
||||
'ks_dashboard_ninja/static/src/js/charts_render_global_functions.js',
|
||||
'ks_dashboard_ninja/static/src/js/cookies.js',
|
||||
'ks_dashboard_ninja/static/src/scss/form_views.scss',
|
||||
'ks_dashboard_ninja/static/src/scss/modal.scss',
|
||||
'ks_dashboard_ninja/static/src/odoo_base_extend/**/*',
|
||||
],
|
||||
},
|
||||
|
||||
'external_dependencies': {
|
||||
'python': ['pandas', 'xlrd', 'openpyxl', 'gTTS', 'SQLAlchemy']
|
||||
},
|
||||
|
||||
'uninstall_hook': 'uninstall_hook',
|
||||
}
|
||||
2
addons/ks_dashboard_ninja/common_lib/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import ks_date_filter_selections
|
||||
from . import filter_tools
|
||||
20
addons/ks_dashboard_ninja/common_lib/filter_tools.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import json
|
||||
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
def replace_company_domain(domain, company_id, company_ids):
|
||||
domain = safe_eval(domain) if isinstance(domain, str) else domain
|
||||
new_domain = []
|
||||
for condition in domain:
|
||||
if isinstance(condition, tuple) and len(condition) >= 3:
|
||||
if condition[1] in ('in', 'not in') and isinstance(condition[2], list) and '%MYCOMPANY' in condition[2]:
|
||||
new_condition = (condition[0], condition[1], [y for x in condition[2] for y in (company_ids if x == '%MYCOMPANY' else [x])])
|
||||
elif condition[2] == '%MYCOMPANY':
|
||||
new_condition = (condition[0], condition[1], company_id)
|
||||
else:
|
||||
new_condition = condition
|
||||
new_domain.append(new_condition)
|
||||
else:
|
||||
new_domain.append(condition)
|
||||
return json.dumps(new_domain)
|
||||
@@ -0,0 +1,343 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import os
|
||||
import os.path
|
||||
from datetime import timedelta
|
||||
|
||||
import pytz
|
||||
from dateutil import rrule
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from odoo import _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.fields import datetime
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
def ks_get_date(ks_date_filter_selection, self, type):
|
||||
try:
|
||||
timezone = self._context.get('tz')
|
||||
except Exception as e:
|
||||
timezone = self.env.user.tz
|
||||
|
||||
if not timezone:
|
||||
ks_tzone = os.environ.get('TZ')
|
||||
if ks_tzone:
|
||||
timezone = ks_tzone
|
||||
elif os.path.exists('/etc/timezone'):
|
||||
ks_tzone = open('/etc/timezone').read()
|
||||
timezone = ks_tzone[0:-1]
|
||||
try:
|
||||
datetime.now(pytz.timezone(timezone))
|
||||
except Exception as e:
|
||||
raise ValidationError(_("Please set the local timezone."))
|
||||
|
||||
else:
|
||||
raise ValidationError(_("Please set the local timezone."))
|
||||
|
||||
series = ks_date_filter_selection
|
||||
if ks_date_filter_selection in ['t_fiscal_year', 'n_fiscal_year', 'ls_fiscal_year']:
|
||||
function_name = globals()["ks_date_series_" + series.split("_")[0]]
|
||||
return function_name(series.split("_")[1], timezone, type,self)
|
||||
else:
|
||||
function_name = globals()["ks_date_series_" + series.split("_")[0]]
|
||||
return function_name(series.split("_")[1],timezone, type,self)
|
||||
|
||||
def ks_date_series_td(ks_date_selection, timezone, type, self=None):
|
||||
ks_function_name = globals()["ks_get_date_range_from_td_" + ks_date_selection]
|
||||
return ks_function_name(timezone, type, self)
|
||||
|
||||
def ks_get_date_range_from_td_year(timezone, type,self):
|
||||
ks_date_data = {}
|
||||
date = datetime.now(pytz.timezone(timezone))
|
||||
year = date.year
|
||||
start_date = datetime(year, 1, 1)
|
||||
end_date = date
|
||||
if type == 'date':
|
||||
ks_date_data["selected_start_date"] = datetime.strptime(start_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
ks_date_data["selected_end_date"] = datetime.strptime(end_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
else:
|
||||
ks_date_data["selected_start_date"] = ks_convert_into_utc(start_date, timezone)
|
||||
ks_date_data["selected_end_date"] = ks_convert_into_utc(end_date, timezone)
|
||||
return ks_date_data
|
||||
|
||||
def ks_get_date_range_from_td_month(timezone, type,self):
|
||||
ks_date_data = {}
|
||||
|
||||
date = datetime.now(pytz.timezone(timezone))
|
||||
year = date.year
|
||||
month = date.month
|
||||
start_date = datetime(year, month, 1)
|
||||
end_date = date
|
||||
if type == 'date':
|
||||
ks_date_data["selected_start_date"] = datetime.strptime(start_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
ks_date_data["selected_end_date"] = datetime.strptime(end_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
else:
|
||||
ks_date_data["selected_start_date"] = ks_convert_into_utc(start_date, timezone)
|
||||
ks_date_data["selected_end_date"] = ks_convert_into_utc(end_date, timezone)
|
||||
return ks_date_data
|
||||
def ks_get_date_range_from_td_week(timezone, type,self):
|
||||
ks_date_data = {}
|
||||
lang = self.env['res.lang']._lang_get(self.env.user.lang)
|
||||
week_start = lang.week_start
|
||||
start_Date = rrule.weekday(int(week_start) - 1)
|
||||
start_date = datetime.today() + relativedelta(weekday=start_Date(-1))
|
||||
end_date = datetime.now(pytz.timezone(timezone))
|
||||
start_date = datetime.strptime(start_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
if type == 'date':
|
||||
ks_date_data["selected_start_date"] = start_date
|
||||
end_date = datetime.strptime(end_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
ks_date_data["selected_end_date"] = end_date
|
||||
else:
|
||||
ks_date_data["selected_start_date"] = ks_convert_into_utc(start_date, timezone)
|
||||
ks_date_data["selected_end_date"] = ks_convert_into_utc(end_date, timezone)
|
||||
return ks_date_data
|
||||
def ks_get_date_range_from_td_quarter(timezone, type,self):
|
||||
ks_date_data = {}
|
||||
date = datetime.now(pytz.timezone(timezone))
|
||||
year = date.year
|
||||
quarter = int((date.month - 1) / 3) + 1
|
||||
start_date = datetime(year, 3 * quarter - 2, 1)
|
||||
end_date = date
|
||||
if type == 'date':
|
||||
ks_date_data["selected_start_date"] = datetime.strptime(start_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
ks_date_data["selected_end_date"] = datetime.strptime(end_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
else:
|
||||
ks_date_data["selected_start_date"] = ks_convert_into_utc(start_date, timezone)
|
||||
ks_date_data["selected_end_date"] = ks_convert_into_utc(end_date, timezone)
|
||||
return ks_date_data
|
||||
|
||||
|
||||
# Last Specific Days Ranges : 7, 30, 90, 365
|
||||
def ks_date_series_l(ks_date_selection, timezone, type, self=None):
|
||||
ks_date_data = {}
|
||||
date_filter_options = {
|
||||
'day': 0,
|
||||
'week': 7,
|
||||
'month': 30,
|
||||
'quarter': 90,
|
||||
'year': 365,
|
||||
'past': False,
|
||||
'future': False
|
||||
}
|
||||
end_time = datetime.strptime(datetime.now(pytz.timezone(timezone)).strftime("%Y-%m-%d 23:59:59"),
|
||||
'%Y-%m-%d %H:%M:%S')
|
||||
start_time = datetime.strptime((datetime.now(pytz.timezone(timezone)) - timedelta(
|
||||
days=date_filter_options[ks_date_selection])).strftime("%Y-%m-%d 00:00:00"), '%Y-%m-%d %H:%M:%S')
|
||||
if type == 'date':
|
||||
ks_date_data["selected_end_date"] = datetime.strptime(end_time.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
ks_date_data["selected_start_date"] = datetime.strptime(start_time.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
else:
|
||||
ks_date_data["selected_end_date"] = ks_convert_into_utc(end_time, timezone)
|
||||
ks_date_data["selected_start_date"] = ks_convert_into_utc(start_time, timezone)
|
||||
|
||||
return ks_date_data
|
||||
|
||||
|
||||
# Current Date Ranges : Week, Month, Quarter, year
|
||||
def ks_date_series_t(ks_date_selection, timezone, type, self=None):
|
||||
ks_function_name = globals()["ks_get_date_range_from_" + ks_date_selection]
|
||||
return ks_function_name("current", timezone, type,self)
|
||||
|
||||
|
||||
# Previous Date Ranges : Week, Month, Quarter, year
|
||||
def ks_date_series_ls(ks_date_selection, timezone, type,self=None):
|
||||
ks_function_name = globals()["ks_get_date_range_from_" + ks_date_selection]
|
||||
return ks_function_name("previous", timezone, type,self)
|
||||
|
||||
|
||||
# Next Date Ranges : Day, Week, Month, Quarter, year
|
||||
def ks_date_series_n(ks_date_selection, timezone, type,self=None):
|
||||
ks_function_name = globals()["ks_get_date_range_from_" + ks_date_selection]
|
||||
return ks_function_name("next", timezone, type, self)
|
||||
|
||||
|
||||
def ks_get_date_range_from_day(date_state, timezone, type,self):
|
||||
ks_date_data = {}
|
||||
|
||||
date = datetime.now(pytz.timezone(timezone))
|
||||
|
||||
if date_state == "previous":
|
||||
date = date - timedelta(days=1)
|
||||
elif date_state == "next":
|
||||
date = date + timedelta(days=1)
|
||||
start_date = datetime(date.year, date.month, date.day)
|
||||
end_date = datetime(date.year, date.month, date.day) + timedelta(days=1, seconds=-1)
|
||||
if type == 'date':
|
||||
ks_date_data["selected_start_date"] = datetime.strptime(start_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
ks_date_data["selected_end_date"] = datetime.strptime(end_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
else:
|
||||
ks_date_data["selected_start_date"] = ks_convert_into_utc(start_date,timezone)
|
||||
ks_date_data["selected_end_date"] = ks_convert_into_utc(end_date,timezone)
|
||||
return ks_date_data
|
||||
|
||||
|
||||
def ks_get_date_range_from_week(date_state, timezone, type,self):
|
||||
ks_date_data = {}
|
||||
|
||||
# date = datetime.now(pytz.timezone(timezone))
|
||||
# ks_week = 0
|
||||
lang = self.env['res.lang']._lang_get(self.env.user.lang)
|
||||
week_start = lang.week_start
|
||||
start_Date = rrule.weekday(int(week_start) - 1)
|
||||
start_date = datetime.today() + relativedelta(weekday=start_Date(-1))
|
||||
if date_state == "previous":
|
||||
start_date = datetime.today() - relativedelta(weeks=1, weekday=start_Date(-1))
|
||||
elif date_state == "next":
|
||||
start_date = datetime.today() - relativedelta(weeks=-1, weekday=start_Date(-1))
|
||||
|
||||
start_date = datetime.strptime(start_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
if type == 'date':
|
||||
ks_date_data["selected_start_date"] = start_date
|
||||
end_date = start_date + timedelta(days=6, hours=23, minutes=59, seconds=59, milliseconds=59)
|
||||
ks_date_data["selected_end_date"] = end_date
|
||||
else:
|
||||
ks_date_data["selected_start_date"] = ks_convert_into_utc(start_date, timezone)
|
||||
end_date = start_date + timedelta(days=6, hours=23, minutes=59, seconds=59, milliseconds=59)
|
||||
ks_date_data["selected_end_date"] = ks_convert_into_utc(end_date, timezone)
|
||||
return ks_date_data
|
||||
|
||||
def ks_get_date_range_from_month(date_state, timezone, type,self):
|
||||
ks_date_data = {}
|
||||
|
||||
date = datetime.now(pytz.timezone(timezone))
|
||||
year = date.year
|
||||
month = date.month
|
||||
|
||||
if date_state == "previous":
|
||||
month -= 1
|
||||
if month == 0:
|
||||
month = 12
|
||||
year -= 1
|
||||
elif date_state == "next":
|
||||
month += 1
|
||||
if month == 13:
|
||||
month = 1
|
||||
year += 1
|
||||
|
||||
end_year = year
|
||||
end_month = month
|
||||
if month == 12:
|
||||
end_year += 1
|
||||
end_month = 1
|
||||
else:
|
||||
end_month += 1
|
||||
start_date = datetime(year, month, 1)
|
||||
end_date = datetime(end_year, end_month, 1) - timedelta(seconds=1)
|
||||
if type == 'date':
|
||||
ks_date_data["selected_start_date"] = datetime.strptime(start_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
ks_date_data["selected_end_date"] = datetime.strptime(end_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
else:
|
||||
ks_date_data["selected_start_date"] = ks_convert_into_utc(start_date, timezone)
|
||||
ks_date_data["selected_end_date"] = ks_convert_into_utc(end_date, timezone)
|
||||
return ks_date_data
|
||||
|
||||
|
||||
def ks_get_date_range_from_quarter(date_state, timezone, type,self):
|
||||
ks_date_data = {}
|
||||
|
||||
date = datetime.now(pytz.timezone(timezone))
|
||||
year = date.year
|
||||
quarter = int((date.month - 1) / 3) + 1
|
||||
|
||||
if date_state == "previous":
|
||||
quarter -= 1
|
||||
if quarter == 0:
|
||||
quarter = 4
|
||||
year -= 1
|
||||
elif date_state == "next":
|
||||
quarter += 1
|
||||
if quarter == 5:
|
||||
quarter = 1
|
||||
year += 1
|
||||
|
||||
start_date = datetime(year, 3 * quarter - 2, 1)
|
||||
|
||||
month = 3 * quarter
|
||||
remaining = int(month / 12)
|
||||
end_date = datetime(year + remaining, month % 12 + 1, 1) - timedelta(seconds=1)
|
||||
if type == 'date':
|
||||
ks_date_data["selected_start_date"] = datetime.strptime(start_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
ks_date_data["selected_end_date"] = datetime.strptime(end_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
else:
|
||||
ks_date_data["selected_start_date"] = ks_convert_into_utc(start_date, timezone)
|
||||
ks_date_data["selected_end_date"] = ks_convert_into_utc(end_date, timezone)
|
||||
return ks_date_data
|
||||
|
||||
|
||||
def ks_get_date_range_from_year(date_state, timezone, type,self):
|
||||
ks_date_data = {}
|
||||
|
||||
date = datetime.now(pytz.timezone(timezone))
|
||||
year = date.year
|
||||
|
||||
if date_state == "previous":
|
||||
year -= 1
|
||||
elif date_state == "next":
|
||||
year += 1
|
||||
start_date = datetime(year, 1, 1)
|
||||
end_date = datetime(year + 1, 1, 1) - timedelta(seconds=1)
|
||||
if type == 'date':
|
||||
ks_date_data["selected_start_date"] = datetime.strptime(start_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
ks_date_data["selected_end_date"] = datetime.strptime(end_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
else:
|
||||
ks_date_data["selected_start_date"] = ks_convert_into_utc(start_date, timezone)
|
||||
ks_date_data["selected_end_date"] = ks_convert_into_utc(end_date, timezone)
|
||||
return ks_date_data
|
||||
|
||||
def ks_get_date_range_from_past(date_state, self_tz, type, self):
|
||||
ks_date_data = {}
|
||||
date = datetime.now(pytz.timezone(self_tz))
|
||||
if type == 'date':
|
||||
ks_date_data["selected_end_date"] = datetime.strptime(date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
else:
|
||||
ks_date_data["selected_end_date"] = ks_convert_into_utc(date, self_tz)
|
||||
ks_date_data["selected_start_date"] = False
|
||||
return ks_date_data
|
||||
|
||||
|
||||
def ks_get_date_range_from_pastwithout(date_state, self_tz, type,self):
|
||||
ks_date_data = {}
|
||||
date = datetime.now(pytz.timezone(self_tz))
|
||||
hour = date.hour + 1
|
||||
date = date - timedelta(hours=hour)
|
||||
date = datetime.strptime(date.strftime("%Y-%m-%d 23:59:59"), '%Y-%m-%d %H:%M:%S')
|
||||
ks_date_data["selected_start_date"] = False
|
||||
if type == 'date':
|
||||
ks_date_data["selected_end_date"] = datetime.strptime(date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
else:
|
||||
ks_date_data["selected_end_date"] = ks_convert_into_utc(date, self_tz)
|
||||
return ks_date_data
|
||||
|
||||
|
||||
def ks_get_date_range_from_future(date_state, self_tz, type,self):
|
||||
ks_date_data = {}
|
||||
date = datetime.now(pytz.timezone(self_tz))
|
||||
ks_date_data["selected_end_date"] = False
|
||||
if type == 'date':
|
||||
ks_date_data["selected_start_date"] = date.strptime(date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
else:
|
||||
ks_date_data["selected_start_date"] = ks_convert_into_utc(date,self_tz)
|
||||
return ks_date_data
|
||||
|
||||
|
||||
def ks_get_date_range_from_futurestarting(date_state, self_tz, type,self):
|
||||
ks_date_data = {}
|
||||
date = datetime.now(pytz.timezone(self_tz))
|
||||
date = date + timedelta(days=1)
|
||||
start_date = datetime.strptime(date.strftime("%Y-%m-%d 00:00:00"), '%Y-%m-%d %H:%M:%S')
|
||||
if type == 'date':
|
||||
ks_date_data["selected_start_date"] = datetime.strptime(start_date.strftime("%Y-%m-%d"), '%Y-%m-%d')
|
||||
ks_date_data["selected_end_date"] = False
|
||||
else:
|
||||
ks_date_data["selected_start_date"] = ks_convert_into_utc(start_date, self_tz)
|
||||
ks_date_data["selected_end_date"] = False
|
||||
return ks_date_data
|
||||
|
||||
def ks_convert_into_utc(datetime, timezone):
|
||||
ks_tz = timezone and pytz.timezone(timezone) or pytz.UTC
|
||||
return ks_tz.localize(datetime.replace(tzinfo=None), is_dst=False).astimezone(pytz.UTC).replace(tzinfo=None)
|
||||
|
||||
def ks_convert_into_local(datetime, timezone):
|
||||
ks_tz = timezone and pytz.timezone(timezone) or pytz.UTC
|
||||
return pytz.UTC.localize(datetime.replace(tzinfo=None), is_dst=False).astimezone(ks_tz).replace(tzinfo=None)
|
||||
4
addons/ks_dashboard_ninja/controllers/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import ks_chart_export
|
||||
from . import ks_list_export
|
||||
from . import ks_dashboard_export
|
||||
from . import ks_domain_fix
|
||||
131
addons/ks_dashboard_ninja/controllers/ks_chart_export.py
Normal file
@@ -0,0 +1,131 @@
|
||||
|
||||
import re
|
||||
import datetime
|
||||
import io
|
||||
import json
|
||||
import operator
|
||||
import logging
|
||||
|
||||
from odoo.addons.web.controllers.export import ExportXlsxWriter
|
||||
from odoo.tools.translate import _
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
from odoo import http
|
||||
from odoo.http import content_disposition, request
|
||||
from odoo.tools.misc import xlwt
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import pycompat
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class KsChartExport(http.Controller):
|
||||
|
||||
def base(self, data):
|
||||
params = json.loads(data)
|
||||
if not params.get('chart_data'):
|
||||
raise ValidationError("Chart data not present")
|
||||
header,chart_data = operator.itemgetter('header','chart_data')(params)
|
||||
chart_data = json.loads(chart_data)
|
||||
|
||||
if isinstance(chart_data['labels'], list):
|
||||
chart_data['labels'] = [str(label) for label in chart_data['labels']]
|
||||
|
||||
chart_data['labels'].insert(0,'Measure')
|
||||
columns_headers = chart_data['labels']
|
||||
import_data = []
|
||||
excel_fields = []
|
||||
|
||||
for dataset in chart_data['datasets']:
|
||||
dataset['data'].insert(0, dataset['label'])
|
||||
import_data.append(dataset['data'])
|
||||
|
||||
for i in range(len(columns_headers)):
|
||||
ks_type_obj = {}
|
||||
if (len(import_data)):
|
||||
if isinstance(import_data[0][i],float):
|
||||
ks_type_obj['type'] = 'float'
|
||||
else:
|
||||
ks_type_obj['type'] = ''
|
||||
excel_fields.append((ks_type_obj))
|
||||
|
||||
return request.make_response(self.from_data(excel_fields, columns_headers, import_data),
|
||||
headers=[('Content-Disposition',
|
||||
content_disposition(self.filename(header))),
|
||||
('Content-Type', self.content_type)],
|
||||
# cookies={'fileToken': token}
|
||||
)
|
||||
|
||||
class KsChartExcelExport(KsChartExport, http.Controller):
|
||||
|
||||
# Excel needs raw data to correctly handle numbers and date values
|
||||
raw_data = True
|
||||
|
||||
@http.route('/ks_dashboard_ninja/export/chart_xls', type='http', auth="user")
|
||||
def index(self, data):
|
||||
try:
|
||||
return self.base(data)
|
||||
except Exception as exc:
|
||||
_logger.exception("Exception during request handling.")
|
||||
payload = json.dumps({
|
||||
'code': 200,
|
||||
'message': "Odoo Server Error",
|
||||
'data': http.serialize_exception(exc)
|
||||
})
|
||||
raise InternalServerError(payload) from exc
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
return 'application/vnd.ms-excel'
|
||||
|
||||
def filename(self, base):
|
||||
return base + '.xlsx'
|
||||
|
||||
def from_data(self, fields, columns_headers, rows):
|
||||
with ExportXlsxWriter(fields, columns_headers, len(rows)) as xlsx_writer:
|
||||
for row_index, row in enumerate(rows):
|
||||
for cell_index, cell_value in enumerate(row):
|
||||
xlsx_writer.write_cell(row_index + 1, cell_index, cell_value)
|
||||
|
||||
return xlsx_writer.value
|
||||
|
||||
|
||||
class KsChartCsvExport(KsChartExport, http.Controller):
|
||||
|
||||
@http.route('/ks_dashboard_ninja/export/chart_csv', type='http', auth="user")
|
||||
def index(self, data):
|
||||
try:
|
||||
return self.base(data)
|
||||
except Exception as exc:
|
||||
_logger.exception("Exception during request handling.")
|
||||
payload = json.dumps({
|
||||
'code': 200,
|
||||
'message': "Odoo Server Error",
|
||||
'data': http.serialize_exception(exc)
|
||||
})
|
||||
raise InternalServerError(payload) from exc
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
return 'text/csv;charset=utf8'
|
||||
|
||||
def filename(self, base):
|
||||
return base + '.csv'
|
||||
|
||||
def from_data(self, fields,columns_headers, rows):
|
||||
fp = io.BytesIO()
|
||||
writer = pycompat.csv_writer(fp, quoting=1)
|
||||
|
||||
writer.writerow(columns_headers)
|
||||
|
||||
for data in rows:
|
||||
row = []
|
||||
for d in data:
|
||||
# Spreadsheet apps tend to detect formulas on leading =, + and -
|
||||
if isinstance(d, str) and d.startswith(('=', '-', '+')):
|
||||
d = "'" + d
|
||||
|
||||
row.append(pycompat.to_text(d))
|
||||
writer.writerow(row)
|
||||
|
||||
return fp.getvalue()
|
||||
88
addons/ks_dashboard_ninja/controllers/ks_dashboard_export.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import io
|
||||
import json
|
||||
import operator
|
||||
import logging
|
||||
|
||||
# from odoo.addons.web.controllers.main import ExportFormat
|
||||
from odoo.addons.web.controllers.export import ExportXlsxWriter
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.http import content_disposition,request
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KsDashboardExport(http.Controller):
|
||||
|
||||
def base(self, data):
|
||||
params = json.loads(data)
|
||||
header, dashboard_data = operator.itemgetter('header', 'dashboard_data')(params)
|
||||
return request.make_response(self.from_data(dashboard_data),
|
||||
headers=[('Content-Disposition',
|
||||
content_disposition(self.filename(header))),
|
||||
('Content-Type', self.content_type)],
|
||||
# cookies={'fileToken': token}
|
||||
)
|
||||
|
||||
|
||||
class KsDashboardJsonExport(KsDashboardExport, http.Controller):
|
||||
|
||||
@http.route('/ks_dashboard_ninja/export/dashboard_json', type='http', auth="user")
|
||||
def index(self, data):
|
||||
try:
|
||||
return self.base(data)
|
||||
except Exception as exc:
|
||||
_logger.exception("Exception during request handling.")
|
||||
payload = json.dumps({
|
||||
'code': 200,
|
||||
'message': "Odoo Server Error",
|
||||
'data': http.serialize_exception(exc)
|
||||
})
|
||||
raise InternalServerError(payload) from exc
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
return 'text/csv;charset=utf8'
|
||||
|
||||
def filename(self, base):
|
||||
return base + '.json'
|
||||
|
||||
def from_data(self, dashboard_data):
|
||||
fp = io.StringIO()
|
||||
fp.write(json.dumps(dashboard_data))
|
||||
|
||||
return fp.getvalue()
|
||||
|
||||
class KsItemJsonExport(KsDashboardExport, http.Controller):
|
||||
|
||||
@http.route('/ks_dashboard_ninja/export/item_json', type='http', auth="user")
|
||||
def index(self, data):
|
||||
try:
|
||||
data = json.loads(data)
|
||||
item_id = data["item_id"]
|
||||
data['dashboard_data'] = request.env['ks_dashboard_ninja.board'].ks_export_item(item_id)
|
||||
data = json.dumps(data)
|
||||
return self.base(data)
|
||||
except Exception as exc:
|
||||
_logger.exception("Exception during request handling.")
|
||||
payload = json.dumps({
|
||||
'code': 200,
|
||||
'message': "Odoo Server Error",
|
||||
'data': http.serialize_exception(exc)
|
||||
})
|
||||
raise InternalServerError(payload) from exc
|
||||
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
return 'text/csv;charset=utf8'
|
||||
|
||||
def filename(self, base):
|
||||
return base + '.json'
|
||||
|
||||
def from_data(self, dashboard_data):
|
||||
fp = io.StringIO()
|
||||
fp.write(json.dumps(dashboard_data))
|
||||
|
||||
return fp.getvalue()
|
||||
21
addons/ks_dashboard_ninja/controllers/ks_domain_fix.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from odoo.addons.web.controllers.domain import Domain
|
||||
|
||||
from odoo import http, _
|
||||
from odoo.http import Controller, request
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
class ksdomainfix(Domain):
|
||||
# to validate our uid and mycompany based domain
|
||||
@http.route('/web/domain/validate', type='json', auth="user")
|
||||
def validate(self, model, domain):
|
||||
ks_uid_domain = str(domain)
|
||||
if ks_uid_domain and "%UID" in ks_uid_domain:
|
||||
ks_domain = ks_uid_domain.replace("%UID", str(request.env.user.id))
|
||||
return super().validate(model,safe_eval(ks_domain))
|
||||
elif ks_uid_domain and "%MYCOMPANY" in ks_uid_domain:
|
||||
ks_domain = ks_uid_domain.replace("%MYCOMPANY", str(request.env.company.id))
|
||||
return super().validate(model,safe_eval(ks_domain))
|
||||
else:
|
||||
return super().validate(model, domain)
|
||||
|
||||
217
addons/ks_dashboard_ninja/controllers/ks_list_export.py
Normal file
@@ -0,0 +1,217 @@
|
||||
import datetime
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import operator
|
||||
import os
|
||||
|
||||
import pytz
|
||||
from dateutil.parser import parse
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.http import content_disposition, request
|
||||
from odoo.tools import pycompat
|
||||
from odoo.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from werkzeug.exceptions import InternalServerError
|
||||
|
||||
from odoo import http
|
||||
from odoo.addons.web.controllers.export import ExportXlsxWriter
|
||||
from ..common_lib.ks_date_filter_selections import ks_get_date, ks_convert_into_local
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KsListExport(http.Controller):
|
||||
|
||||
def base(self, data):
|
||||
params = json.loads(data)
|
||||
# header,list_data = operator.itemgetter('header','chart_data')(params)
|
||||
header, list_data, item_id, ks_export_boolean, context, params = operator.itemgetter('header', 'chart_data',
|
||||
'ks_item_id',
|
||||
'ks_export_boolean',
|
||||
'context', 'params')(
|
||||
params)
|
||||
list_data = json.loads(list_data)
|
||||
if not list_data or not list_data.get('label', False):
|
||||
raise ValidationError("List data not present")
|
||||
if ks_export_boolean:
|
||||
item = request.env['ks_dashboard_ninja.item'].browse(int(item_id))
|
||||
ks_timezone = item._context.get('tz') or item.env.user.tz
|
||||
if not ks_timezone:
|
||||
ks_tzone = os.environ.get('TZ')
|
||||
if ks_tzone:
|
||||
ks_timezone = ks_tzone
|
||||
elif os.path.exists('/etc/timezone'):
|
||||
ks_tzone = open('/etc/timezone').read()
|
||||
ks_timezone = ks_tzone[0:-1]
|
||||
try:
|
||||
datetime.now(pytz.timezone(ks_timezone))
|
||||
except Exception as e:
|
||||
_logger.info('Please set the local timezone')
|
||||
|
||||
else:
|
||||
_logger.info('Please set the local timezone')
|
||||
orderby = item.ks_sort_by_field.id
|
||||
sort_order = item.ks_sort_by_order
|
||||
ks_start_date = context.get('ksDateFilterStartDate', False)
|
||||
ks_end_date = context.get('ksDateFilterEndDate', False)
|
||||
ksDateFilterSelection = context.get('ksDateFilterSelection', False)
|
||||
if context.get('allowed_company_ids', False):
|
||||
item = item.with_context(allowed_company_ids=context.get('allowed_company_ids'))
|
||||
if item.ks_data_calculation_type == 'query':
|
||||
query_start_date = item.ks_query_start_date
|
||||
query_end_date = item.ks_query_end_date
|
||||
ks_query = str(item.ks_custom_query)
|
||||
if ks_start_date and ks_end_date:
|
||||
ks_start_date = parse(ks_start_date)
|
||||
ks_end_date = parse(ks_end_date)
|
||||
item = item.with_context(ksDateFilterStartDate=ks_start_date)
|
||||
item = item.with_context(ksDateFilterEndDate=ks_end_date)
|
||||
item = item.with_context(ksDateFilterSelection=ksDateFilterSelection)
|
||||
|
||||
if item._context.get('ksDateFilterSelection', False):
|
||||
ks_date_filter_selection = item._context['ksDateFilterSelection']
|
||||
if ks_date_filter_selection == 'l_custom':
|
||||
item = item.with_context(ksDateFilterStartDate=ks_start_date)
|
||||
item = item.with_context(ksDateFilterEndDate=ks_end_date)
|
||||
item = item.with_context(ksIsDefultCustomDateFilter=False)
|
||||
|
||||
else:
|
||||
ks_date_filter_selection = item.ks_dashboard_ninja_board_id.ks_date_filter_selection
|
||||
item = item.with_context(ksDateFilterStartDate=item.ks_dashboard_ninja_board_id.ks_dashboard_start_date)
|
||||
item = item.with_context(ksDateFilterEndDate=item.ks_dashboard_ninja_board_id.ks_dashboard_end_date)
|
||||
item = item.with_context(ksDateFilterSelection=ks_date_filter_selection)
|
||||
item = item.with_context(ksIsDefultCustomDateFilter=True)
|
||||
|
||||
if ks_date_filter_selection not in ['l_custom', 'l_none']:
|
||||
ks_date_data = ks_get_date(ks_date_filter_selection, request, 'datetime')
|
||||
item = item.with_context(ksDateFilterStartDate=ks_date_data["selected_start_date"])
|
||||
item = item.with_context(ksDateFilterEndDate=ks_date_data["selected_end_date"])
|
||||
|
||||
item_domain = params.get('ks_domain_1', [])
|
||||
ks_chart_domain = item.ks_convert_into_proper_domain(item.ks_domain, item,item_domain)
|
||||
# list_data = item.ks_fetch_list_view_data(item,ks_chart_domain, ks_export_all=
|
||||
if list_data['type'] == 'ungrouped':
|
||||
list_data = item.ks_fetch_list_view_data(item, ks_chart_domain, ks_export_all=True)
|
||||
elif list_data['type'] == 'grouped':
|
||||
list_data = item.get_list_view_record(orderby, sort_order, ks_chart_domain, ks_export_all=True)
|
||||
elif item.ks_data_calculation_type == 'query':
|
||||
if ks_start_date or ks_end_date:
|
||||
query_start_date = ks_start_date
|
||||
query_end_date = ks_end_date
|
||||
ks_query_result = item.ks_get_list_query_result(ks_query, query_start_date, query_end_date, ks_offset=0,
|
||||
ks_export_all=True)
|
||||
list_data = item.ks_format_query_result(ks_query_result)
|
||||
|
||||
# chart_data['labels'].insert(0,'Measure')
|
||||
columns_headers = list_data['label']
|
||||
import_data = []
|
||||
excel_fields = []
|
||||
for dataset in list_data['data_rows']:
|
||||
if not list_data['type'] == 'grouped':
|
||||
for count, index in enumerate(dataset['ks_column_type']):
|
||||
if index == 'datetime':
|
||||
ks_converted_date = False
|
||||
date_string = dataset['data'][count]
|
||||
if dataset['data'][count]:
|
||||
ks_converted_date = ks_convert_into_local(datetime.datetime.strptime(date_string, '%m/%d/%y %H:%M:%S'),ks_timezone)
|
||||
dataset['data'][count] = ks_converted_date
|
||||
for ks_count, val in enumerate(dataset['data']):
|
||||
if isinstance(val, (float, int)):
|
||||
if val >= 0:
|
||||
try:
|
||||
ks_precision = item.sudo().env.ref('ks_dashboard_ninja.ks_dashboard_ninja_precision').digits
|
||||
except Exception as e:
|
||||
ks_precision = 2
|
||||
dataset['data'][ks_count] = item.env['ir.qweb.field.float'].sudo().value_to_html(val,
|
||||
{'precision': ks_precision})
|
||||
import_data.append(dataset['data'])
|
||||
for i in range(len(columns_headers)):
|
||||
ks_type_obj = {}
|
||||
if (len(import_data)):
|
||||
if isinstance(import_data[0][i], float):
|
||||
ks_type_obj['type'] = 'float'
|
||||
else:
|
||||
ks_type_obj['type'] = ''
|
||||
excel_fields.append((ks_type_obj))
|
||||
|
||||
return request.make_response(self.from_data(excel_fields, columns_headers, import_data),
|
||||
headers=[('Content-Disposition',
|
||||
content_disposition(self.filename(header))),
|
||||
('Content-Type', self.content_type)],
|
||||
# cookies={'fileToken': token}
|
||||
)
|
||||
|
||||
|
||||
class KsListExcelExport(KsListExport, http.Controller):
|
||||
|
||||
# Excel needs raw data to correctly handle numbers and date values
|
||||
raw_data = True
|
||||
|
||||
@http.route('/ks_dashboard_ninja/export/list_xls', type='http', auth="user")
|
||||
def index(self, data):
|
||||
try:
|
||||
return self.base(data)
|
||||
except Exception as exc:
|
||||
_logger.exception("Exception during request handling.")
|
||||
payload = json.dumps({
|
||||
'code': 200,
|
||||
'message': "Odoo Server Error",
|
||||
'data': http.serialize_exception(exc)
|
||||
})
|
||||
raise InternalServerError(payload) from exc
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
return 'application/vnd.ms-excel'
|
||||
|
||||
def filename(self, base):
|
||||
return base + '.xlsx'
|
||||
|
||||
def from_data(self, fields, columns_headers, rows):
|
||||
with ExportXlsxWriter(fields, columns_headers, len(rows)) as xlsx_writer:
|
||||
for row_index, row in enumerate(rows):
|
||||
for cell_index, cell_value in enumerate(row):
|
||||
xlsx_writer.write_cell(row_index + 1, cell_index, cell_value)
|
||||
|
||||
return xlsx_writer.value
|
||||
|
||||
|
||||
class KsListCsvExport(KsListExport, http.Controller):
|
||||
|
||||
@http.route('/ks_dashboard_ninja/export/list_csv', type='http', auth="user")
|
||||
def index(self, data):
|
||||
try:
|
||||
return self.base(data)
|
||||
except Exception as exc:
|
||||
_logger.exception("Exception during request handling.")
|
||||
payload = json.dumps({
|
||||
'code': 200,
|
||||
'message': "Odoo Server Error",
|
||||
'data': http.serialize_exception(exc)
|
||||
})
|
||||
raise InternalServerError(payload) from exc
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
return 'text/csv;charset=utf8'
|
||||
|
||||
def filename(self, base):
|
||||
return base + '.csv'
|
||||
|
||||
def from_data(self, fields, column_headers,rows):
|
||||
fp = io.BytesIO()
|
||||
writer = pycompat.csv_writer(fp, quoting=1)
|
||||
|
||||
writer.writerow(column_headers)
|
||||
|
||||
for data in rows:
|
||||
row = []
|
||||
for d in data:
|
||||
# Spreadsheet apps tend to detect formulas on leading =, + and -
|
||||
if isinstance(d, str) and d.startswith(('=', '-', '+')):
|
||||
d = "'" + d
|
||||
|
||||
row.append(pycompat.to_text(d))
|
||||
writer.writerow(row)
|
||||
|
||||
return fp.getvalue()
|
||||
9
addons/ks_dashboard_ninja/data/dn_data.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="config_dn_url" model="ir.config_parameter">
|
||||
<field name="key">ks_dashboard_ninja.url</field>
|
||||
<field name="value">https://dn16ai.kappso.com</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
614
addons/ks_dashboard_ninja/data/ks_default_data.xml
Normal file
@@ -0,0 +1,614 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<data>
|
||||
<!-- Default Templates -->
|
||||
<record id="ks_blank" model="ks_dashboard_ninja.board_template">
|
||||
<field name="name">Blank</field>
|
||||
<field name="ks_item_count">0</field>
|
||||
</record>
|
||||
|
||||
<record id="ks_template_1" model="ks_dashboard_ninja.board_template">
|
||||
<field name="name">Template 1</field>
|
||||
<field name="ks_gridstack_config">[
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_1", "data": {"x": 0, "y": 10, "w": 3, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_2", "data": {"x": 0, "y": 8, "w": 3, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_3", "data": {"x": 3, "y": 0, "w": 3, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_4", "data": {"x": 0, "y": 2, "w": 3, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_5", "data": {"x": 6, "y": 12, "w": 6, "h": 6}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_6", "data": {"x": 0, "y": 28, "w": 12, "h":
|
||||
4}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_7", "data": {"x": 0, "y": 43, "w": 5, "h":
|
||||
4}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_8", "data": {"x": 6, "y": 6, "w": 6, "h": 6}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_9", "data": {"x": 5, "y": 36, "w": 7, "h": 7}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_10", "data": {"x": 4, "y": 23, "w": 4, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_11", "data": {"x": 6, "y": 18, "w": 6, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_12", "data": {"x": 0, "y": 6, "w": 3, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_13", "data": {"x": 3, "y": 8, "w": 3, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_15", "data": {"x": 0, "y": 18, "w": 6, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_16", "data": {"x": 0, "y": 0, "w": 3, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_17", "data": {"x": 3, "y": 6, "w": 3, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_18", "data": {"x": 3, "y": 4, "w": 3, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_19", "data": {"x": 3, "y": 10, "w": 3, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_20", "data": {"x": 5, "y": 43, "w": 7, "h":
|
||||
4}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_21", "data": {"x": 0, "y": 12, "w": 6, "h":
|
||||
6}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_22", "data": {"x": 0, "y": 36, "w": 5, "h":
|
||||
7}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_23", "data": {"x": 0, "y": 32, "w": 12, "h":
|
||||
4}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_24", "data": {"x": 8, "y": 23, "w": 4, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_25", "data": {"x": 0, "y": 23, "w": 4, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_26", "data": {"x": 0, "y": 4, "w": 3, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_27", "data": {"x": 3, "y": 3, "w": 3, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_28", "data": {"x": 6, "y": 0, "w": 6, "h":
|
||||
6}}
|
||||
]
|
||||
</field>
|
||||
<field name="ks_item_count">7</field>
|
||||
</record>
|
||||
|
||||
<record id="ks_template_2" model="ks_dashboard_ninja.board_template">
|
||||
<field name="name">Template 2</field>
|
||||
<field name="ks_gridstack_config">[
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_1", "data": {"x": 0, "y": 0, "w": 2, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_2", "data": {"x": 4, "y": 0, "w": 2, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_3", "data": {"x": 2, "y": 0, "w": 2, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_4", "data": {"x": 8, "y": 0, "w": 2, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_5", "data": {"x": 4, "y": 18, "w": 8, "h": 5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_6", "data": {"x": 8, "y": 27, "w": 4, "h":
|
||||
6}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_7", "data": {"x": 0, "y": 18, "w": 4, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_8", "data": {"x": 4, "y": 27, "w": 4, "h":
|
||||
6}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_9", "data": {"x": 4, "y": 13, "w": 8, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_10", "data": {"x": 0, "y": 23, "w": 4, "h":
|
||||
4}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_11", "data": {"x": 0, "y": 4, "w": 4, "h":
|
||||
4}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_12", "data": {"x": 6, "y": 0, "w": 2, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_13", "data": {"x": 10, "y": 2, "w": 2, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_15", "data": {"x":0, "y": 33, "w": 6, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_16", "data": {"x": 2, "y": 2, "w": 2, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_17", "data": {"x": 8, "y": 2, "w": 2, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_18", "data": {"x": 6, "y": 2, "w": 2, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_19", "data": {"x": 0, "y": 2, "w": 2, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_20", "data": {"x": 4, "y": 8, "w": 8, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_21", "data": {"x": 0, "y": 13, "w": 4, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_22", "data": {"x": 4, "y": 23, "w": 8, "h":
|
||||
4}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_23", "data": {"x": 6, "y": 33, "w": 6, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_24", "data": {"x": 4, "y": 4, "w": 8, "h":
|
||||
4}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_25", "data": {"x": 0, "y": 8, "w": 4, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_26", "data": {"x": 4, "y": 2, "w": 2, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_27", "data": {"x": 10, "y": 2, "w": 2, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_28", "data": {"x": 0, "y": 27, "w": 4, "h":
|
||||
6}}
|
||||
]
|
||||
</field>
|
||||
<field name="ks_item_count">7</field>
|
||||
</record>
|
||||
|
||||
<record id="ks_template_3" model="ks_dashboard_ninja.board_template">
|
||||
<field name="name">Template 3</field>
|
||||
<field name="ks_gridstack_config">[
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_1", "data": {"x": 0, "y": 0, "w": 3, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_2", "data": {"x": 6, "y": 0, "w": 3, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_3", "data": {"x": 3, "y": 0, "w": 3, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_4", "data": {"x": 0, "y": 2, "w": 3, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_5", "data": {"x": 7, "y": 2, "w": 5, "h": 4}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_6", "data": {"x": 0, "y": 28, "w": 12, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_7", "data": {"x": 4, "y": 14, "w": 4, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_8", "data": {"x": 0, "y": 33, "w": 3, "h": 5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_9", "data": {"x": 8, "y": 23, "w": 4, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_10", "data": {"x": 8, "y": 14, "w": 4, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_11", "data": {"x": 0, "y": 23, "w": 4, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_12", "data": {"x": 9, "y": 0, "w": 3, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_13", "data": {"x": 3, "y": 2, "w": 4, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_15", "data": {"x":0, "y": 19, "w": 12, "h":
|
||||
4}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_16", "data": {"x": 0, "y": 8, "w": 3, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_17", "data": {"x": 3, "y": 4, "w": 4, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_18", "data": {"x": 0, "y": 12, "w": 3, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_19", "data": {"x": 0, "y": 4, "w": 3, "h": 2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_20", "data": {"x": 3, "y": 6, "w": 9, "h":
|
||||
4}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_21", "data": {"x": 0, "y": 14, "w": 4, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_22", "data": {"x": 6, "y": 33, "w": 6, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_23", "data": {"x": 0, "y": 19, "w": 12, "h":
|
||||
4}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_24", "data": {"x": 3, "y": 10, "w": 9, "h":
|
||||
4}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_25", "data": {"x": 4, "y": 23, "w": 4, "h":
|
||||
5}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_26", "data": {"x": 0, "y": 8, "w": 3, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_27", "data": {"x": 0, "y": 6, "w": 3, "h":
|
||||
2}},
|
||||
{"item_id":"ks_dashboard_ninja.ks_default_item_28", "data": {"x": 3, "y": 33, "w": 3, "h":
|
||||
5}}
|
||||
]
|
||||
</field>
|
||||
<field name="ks_item_count">7</field>
|
||||
</record>
|
||||
|
||||
|
||||
<!--Default items (7 right now) created here that will be used for default templates in future dashboards-->
|
||||
|
||||
<record id="ks_default_item_1" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Tile (layout 1)</field>
|
||||
<field name="ks_dashboard_item_type">ks_tile</field>
|
||||
<field name="ks_record_count_type">count</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_domain">[["id",">",150]]</field>
|
||||
<field name="ks_default_icon">bar-chart</field>
|
||||
<field name="ks_dashboard_item_theme">blue</field>
|
||||
<field name="ks_background_color">#FFE2E5,0.99</field>
|
||||
<field name="ks_font_color">#000000,0.99</field>
|
||||
<field name="ks_default_icon_color">#000000,0.99</field>
|
||||
<field name="ks_layout">layout1</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
|
||||
</record>
|
||||
|
||||
<record id="ks_default_item_2" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Tile (layout 3)</field>
|
||||
<field name="ks_dashboard_item_type">ks_tile</field>
|
||||
<field name="ks_record_count_type">count</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_default_icon">users</field>
|
||||
<field name="ks_dashboard_item_theme">red</field>
|
||||
<field name="ks_background_color">#FFF4DE,0.99</field>
|
||||
<field name="ks_font_color">#000000,0.99</field>
|
||||
<field name="ks_default_icon_color">#000000,0.99</field>
|
||||
<field name="ks_layout">layout3</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
|
||||
</record>
|
||||
|
||||
<record id="ks_default_item_3" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Tile (layout 2)</field>
|
||||
<field name="ks_dashboard_item_type">ks_tile</field>
|
||||
<field name="ks_record_count_type">count</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_domain">[["id","<",50]]</field>
|
||||
<field name="ks_default_icon">money</field>
|
||||
<field name="ks_dashboard_item_theme">green</field>
|
||||
<field name="ks_background_color">#DCFCE7,0.99</field>
|
||||
<field name="ks_font_color">#000000,0.99</field>
|
||||
<field name="ks_default_icon_color">#000000,0.99</field>
|
||||
<field name="ks_layout">layout2</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
|
||||
</record>
|
||||
|
||||
<record id="ks_default_item_4" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Tile (layout 5)</field>
|
||||
<field name="ks_dashboard_item_type">ks_tile</field>
|
||||
<field name="ks_record_count_type">count</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_domain">[["id","<",100]]</field>
|
||||
<field name="ks_default_icon">paper-plane</field>
|
||||
<field name="ks_dashboard_item_theme">yellow</field>
|
||||
<field name="ks_background_color">#F3E8FF,0.99</field>
|
||||
<field name="ks_font_color">#000000,0.99</field>
|
||||
<field name="ks_default_icon_color">#000000,0.99</field>
|
||||
<field name="ks_layout">layout5</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
|
||||
</record>
|
||||
|
||||
<record id="ks_default_item_5" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Bar Chart</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<!-- <field name="ks_chart_measure_field" eval="[ref('base.field_res_country__phone_code')]"/>-->
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__currency_id')"/>
|
||||
<field name="ks_domain">[["id","<",40]]</field>
|
||||
<field name="ks_chart_item_color">dark</field>
|
||||
<field name="ks_dashboard_item_type">ks_bar_chart</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
|
||||
</record>
|
||||
|
||||
|
||||
<record id="ks_default_item_6" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Line Chart</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<!-- <field name="ks_chart_measure_field" eval="[ref('base.field_res_country__phone_code')]"/>-->
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__currency_id')"/>
|
||||
<field name="ks_domain">[["id","<",10]]</field>
|
||||
<field name="ks_chart_item_color">dark</field>
|
||||
<field name="ks_dashboard_item_type">ks_line_chart</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
|
||||
</record>
|
||||
|
||||
<record id="ks_default_item_7" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Pie Chart</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__currency_id')"/>
|
||||
<field name="ks_domain">[["id","<",10]]</field>
|
||||
<field name="ks_dashboard_item_type">ks_pie_chart</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
|
||||
</record>
|
||||
<record id="ks_default_item_8" model="ks_dashboard_ninja.item">
|
||||
<field name="name">list view (Un-Grouped)</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_list_view_type">grouped</field>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__phone_code')"/>
|
||||
<field name="ks_list_view_group_fields" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_domain">[["id","<",10]]</field>
|
||||
<field name="ks_dashboard_item_type">ks_list_view</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_9" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Horizontal Bar</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__currency_id')"/>
|
||||
<field name="ks_domain">[["id","<",10]]</field>
|
||||
<field name="ks_chart_item_color">material</field>
|
||||
<field name="ks_dashboard_item_type">ks_horizontalBar_chart</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_10" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Polar Area</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__currency_id')"/>
|
||||
<field name="ks_domain">[["id","<",10]]</field>
|
||||
<field name="ks_chart_item_color">moonrise</field>
|
||||
<field name="ks_dashboard_item_type">ks_polarArea_chart</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_11" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Doughnut chart</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__name')"/>
|
||||
<field name="ks_domain">[["id","<",10]]</field>
|
||||
<field name="ks_chart_item_color">moonrise</field>
|
||||
<field name="ks_record_data_limit">100</field>
|
||||
<field name="ks_show_data_value">1</field>
|
||||
<field name="ks_unit_selection">monetary</field>
|
||||
<field name="ks_dashboard_item_type">ks_doughnut_chart</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_12" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Tile (layout 4)</field>
|
||||
<field name="ks_dashboard_item_type">ks_tile</field>
|
||||
<field name="ks_record_count_type">count</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_domain">[["id","<",50]]</field>
|
||||
<field name="ks_default_icon">shopping-cart</field>
|
||||
<field name="ks_dashboard_item_theme">red</field>
|
||||
<field name="ks_background_color">#FFE2E5,0.99</field>
|
||||
<field name="ks_font_color">#000000,0.99</field>
|
||||
<field name="ks_default_icon_color">#000000,0.99</field>
|
||||
<field name="ks_layout">layout4</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_13" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Tile (layout 6)</field>
|
||||
<field name="ks_dashboard_item_type">ks_tile</field>
|
||||
<field name="ks_record_count_type">count</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_domain">[["id","<",100]]</field>
|
||||
<field name="ks_default_icon">car</field>
|
||||
<field name="ks_dashboard_item_theme">red</field>
|
||||
<field name="ks_background_color">#FFF4DE,0.53</field>
|
||||
<field name="ks_font_color">#000000,0.70</field>
|
||||
<field name="ks_default_icon_color">#000000,0.99</field>
|
||||
<field name="ks_layout">layout6</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_14" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Pie Chart</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__currency_id')"/>
|
||||
<field name="ks_domain">[["id","<",10]]</field>
|
||||
<field name="ks_chart_item_color">dark</field>
|
||||
<field name="ks_dashboard_item_type">ks_pie_chart</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_15" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Area Chart</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__code')"/>
|
||||
<field name="ks_chart_item_color">default</field>
|
||||
<field name="ks_dashboard_item_type">ks_area_chart</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
</data>
|
||||
<record id="ks_default_item_16" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Kpi Ratio</field>
|
||||
<field name="ks_dashboard_item_type">ks_kpi</field>
|
||||
<field name="ks_record_count_type">count</field>
|
||||
<field name="ks_record_count_type_2">count</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_model_id_2" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_data_comparison">Ratio</field>
|
||||
<field name="ks_domain">[["id","<",100]]</field>
|
||||
<field name="ks_default_icon">user</field>
|
||||
<field name="ks_dashboard_item_theme">blue</field>
|
||||
<field name="ks_background_color">#DCFCE7,0.99</field>
|
||||
<field name="ks_font_color">#000000,0.99</field>
|
||||
<field name="ks_default_icon_color">#000000,0.99</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_17" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Kpi ( Percentage)</field>
|
||||
<field name="ks_dashboard_item_type">ks_kpi</field>
|
||||
<field name="ks_record_count_type">count</field>
|
||||
<field name="ks_record_count_type_2">count</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_model_id_2" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_domain">[["id","<",100]]</field>
|
||||
<field name="ks_data_comparison">Percentage</field>
|
||||
<field name="ks_default_icon">paper-plane</field>
|
||||
<field name="ks_dashboard_item_theme">red</field>
|
||||
<field name="ks_background_color">#F3E8FF,0.99</field>
|
||||
<field name="ks_font_color">#000000,0.99</field>
|
||||
<field name="ks_default_icon_color">#000000,0.99</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_18" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Kpi ( Number)</field>
|
||||
<field name="ks_dashboard_item_type">ks_kpi</field>
|
||||
<field name="ks_record_count_type">count</field>
|
||||
<field name="ks_record_count_type_2">count</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_model_id_2" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_target_view">Number</field>
|
||||
<field name="ks_goal_enable">1</field>
|
||||
<field name="ks_domain">[["id","<",100]]</field>
|
||||
<field name="ks_data_comparison">Sum</field>
|
||||
<field name="ks_default_icon">money</field>
|
||||
<field name="ks_dashboard_item_theme">green</field>
|
||||
<field name="ks_background_color">#F3E8FF,0.63</field>
|
||||
<field name="ks_font_color">#000000,0.99</field>
|
||||
<field name="ks_default_icon_color">#000000,0.99</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_19" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Kpi (sum)</field>
|
||||
<field name="ks_dashboard_item_type">ks_kpi</field>
|
||||
<field name="ks_record_count_type">count</field>
|
||||
<field name="ks_record_count_type_2">count</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_model_id_2" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_data_comparison">Sum</field>
|
||||
<field name="ks_domain">[["id","<",100]]</field>
|
||||
<field name="ks_default_icon">bar-chart</field>
|
||||
<field name="ks_dashboard_item_theme">yellow</field>
|
||||
<field name="ks_background_color">#FFF4DE,0.99</field>
|
||||
<field name="ks_font_color">#000000,0.99</field>
|
||||
<field name="ks_default_icon_color">#000000,0.99</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_20" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Bar Chart With Data Values</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__name')"/>
|
||||
<field name="ks_domain">[["id","<",40]]</field>
|
||||
<field name="ks_chart_item_color">default</field>
|
||||
<field name="ks_dashboard_item_type">ks_bar_chart</field>
|
||||
<field name="ks_show_data_value">1</field>
|
||||
<field name="ks_unit_selection">monetary</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
|
||||
</record>
|
||||
<record id="ks_default_item_21" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Semi Circle Pie Chart</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__name')"/>
|
||||
<field name="ks_semi_circle_chart">1</field>
|
||||
<field name="ks_chart_item_color">material</field>
|
||||
<field name="ks_record_data_limit">10</field>
|
||||
<field name="ks_show_data_value">1</field>
|
||||
<field name="ks_unit_selection">monetary</field>
|
||||
<field name="ks_dashboard_item_type">ks_pie_chart</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_22" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Horizontal Bar(sub-group)</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__name')"/>
|
||||
<field name="ks_chart_relation_sub_groupby" eval="ref('base.field_res_country__name')"/>
|
||||
<field name="ks_chart_item_color">default</field>
|
||||
<field name="ks_domain">[["id","<",10]]</field>
|
||||
<field name="ks_show_data_value">1</field>
|
||||
<field name="ks_unit_selection">monetary</field>
|
||||
<field name="ks_dashboard_item_type">ks_horizontalBar_chart</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_23" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Area Chart with data values</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__code')"/>
|
||||
<field name="ks_chart_item_color">material</field>
|
||||
<field name="ks_record_data_limit">25</field>
|
||||
<field name="ks_show_data_value">1</field>
|
||||
<field name="ks_unit_selection">monetary</field>
|
||||
<field name="ks_dashboard_item_type">ks_area_chart</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_24" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Line Chart with values</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__name')"/>
|
||||
<field name="ks_chart_item_color">moonrise</field>
|
||||
<field name="ks_record_data_limit">10</field>
|
||||
<field name="ks_show_data_value">1</field>
|
||||
<field name="ks_unit_selection">monetary</field>
|
||||
<field name="ks_dashboard_item_type">ks_line_chart</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_25" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Doughnut semi circle</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_measure_field" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__name')"/>
|
||||
<field name="ks_chart_item_color">default</field>
|
||||
<field name="ks_semi_circle_chart">1</field>
|
||||
<field name="ks_record_data_limit">25</field>
|
||||
<field name="ks_show_data_value">1</field>
|
||||
<field name="ks_unit_selection">monetary</field>
|
||||
<field name="ks_dashboard_item_type">ks_doughnut_chart</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_26" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Kpi 26(Average)</field>
|
||||
<field name="ks_dashboard_item_type">ks_kpi</field>
|
||||
<field name="ks_record_field" eval="ref('base.field_res_country__name')"/>
|
||||
<field name="ks_record_field_2" eval="ref('base.field_res_country__name')"/>
|
||||
<field name="ks_data_format">indian</field>
|
||||
<field name="ks_record_count_type">average</field>
|
||||
<field name="ks_record_count_type_2">average</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_model_id_2" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_target_view">Number</field>
|
||||
<field name="ks_goal_enable">1</field>
|
||||
<field name="ks_domain">[["id","<",100]]</field>
|
||||
<field name="ks_data_comparison">Sum</field>
|
||||
<field name="ks_default_icon">money</field>
|
||||
<field name="ks_dashboard_item_theme">blue</field>
|
||||
<field name="ks_background_color">#DCFCE7,0.99</field>
|
||||
<field name="ks_font_color">#000000,0.99</field>
|
||||
<field name="ks_default_icon_color">#000000,0.99</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_27" model="ks_dashboard_ninja.item">
|
||||
<field name="name">Kpi (previous)</field>
|
||||
<field name="ks_dashboard_item_type">ks_kpi</field>
|
||||
<field name="ks_record_count_type">count</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_domain">[["id","<",100]]</field>
|
||||
<field name="ks_previous_period">1</field>
|
||||
<field name="ks_date_filter_selection">t_week</field>
|
||||
<field name="ks_default_icon">money</field>
|
||||
<field name="ks_dashboard_item_theme">green</field>
|
||||
<field name="ks_background_color">#FFE2E5,0.59</field>
|
||||
<field name="ks_font_color">#000000,0.99</field>
|
||||
<field name="ks_default_icon_color">#000000,0.99</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_28" model="ks_dashboard_ninja.item">
|
||||
<field name="name">list view (grouped)</field>
|
||||
<field name="ks_chart_data_count_type">sum</field>
|
||||
<field name="ks_chart_groupby_type">relational_type</field>
|
||||
<field name="ks_model_id" eval="ref('base.model_res_country')"/>
|
||||
<field name="ks_chart_relation_groupby" eval="ref('base.field_res_country__name')"/>
|
||||
<field name="ks_list_view_type">ungrouped</field>
|
||||
<field name="ks_list_view_group_fields" eval="[(6, 0, [ref('base.field_res_country__phone_code')])]"/>
|
||||
<field name="ks_list_view_fields"
|
||||
eval="[(6, 0, [ref('base.field_res_country__phone_code'),ref('base.field_res_country__name')])]"/>
|
||||
<field name="ks_domain">[["id","<",10]]</field>
|
||||
<field name="ks_dashboard_item_type">ks_list_view</field>
|
||||
<field name="ks_company_id" eval="0"/>
|
||||
</record>
|
||||
<record id="ks_default_item_10_action" model="ks_dashboard_ninja.item_action">
|
||||
<field name="ks_dashboard_item_id" ref="ks_default_item_10"/>
|
||||
<field name="ks_chart_type">ks_bar_chart</field>
|
||||
<field name="ks_item_action_field" ref='base.field_res_country__phone_code'/>
|
||||
</record>
|
||||
<record id="ks_default_item_10_action1" model="ks_dashboard_ninja.item_action">
|
||||
<field name="ks_dashboard_item_id" ref="ks_default_item_10"/>
|
||||
<field name="ks_chart_type">ks_pie_chart</field>
|
||||
<field name="ks_item_action_field" ref='base.field_res_country__name'/>
|
||||
</record>
|
||||
|
||||
<!-- Default dashboard Data -->
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="ks_my_default_dashboard_board" model="ks_dashboard_ninja.board">
|
||||
<field name="name">My Dashboard</field>
|
||||
<field name="ks_dashboard_state">Locked</field>
|
||||
<field name="ks_dashboard_menu_name">My Dashboard</field>
|
||||
<field name="ks_dashboard_active">1</field>
|
||||
<field name="ks_dashboard_default_template" ref="ks_dashboard_ninja.ks_blank"/>
|
||||
<field name="ks_dashboard_group_access" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record forcecreate="True" id="ks_dashboard_ninja_precision" model="decimal.precision">
|
||||
<field name="name">Dashboard Ninja Decimal Precision</field>
|
||||
<field name="digits" eval="2"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
11
addons/ks_dashboard_ninja/data/ks_mail_cron.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="ir_cron_send_target_email" model="ir.cron">
|
||||
<field name="name">Kpi mail cron</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="model_id" ref="model_ks_dashboard_ninja_item"/>
|
||||
<field name="code">model.check_target()</field>
|
||||
<field name="state">code</field>
|
||||
</record>
|
||||
</odoo>
|
||||
11
addons/ks_dashboard_ninja/data/sequence.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="ks_dashboard_item_seq" model="ir.sequence">
|
||||
<field name="name">Dashboard Seq</field>
|
||||
<field name="code">ks_dashboard_ninja.item</field>
|
||||
<field name="padding">2</field>
|
||||
<field name="company_id" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
34
addons/ks_dashboard_ninja/demo/ks_dashboard_ninja_demo.xml
Normal file
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Three Default Demo Dashboard with Templates : Template1, Template2, Template3-->
|
||||
|
||||
<record id="demo_template1_dashboard" model="ks_dashboard_ninja.board">
|
||||
<field name="name">Template1 Dashboard</field>
|
||||
<field name="ks_dashboard_menu_name">Template1</field>
|
||||
<field name="ks_dashboard_top_menu_id" eval="ref('ks_dashboard_ninja.dashboards_menu_root')"/>
|
||||
<field name="ks_dashboard_default_template" eval="ref('ks_dashboard_ninja.ks_template_1')"/>
|
||||
<field name="ks_dashboard_active">1</field>
|
||||
<field name="ks_dashboard_group_access" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="demo_template2_dashboard" model="ks_dashboard_ninja.board">
|
||||
<field name="name">Template2 Dashboard</field>
|
||||
<field name="ks_dashboard_menu_name">Template2</field>
|
||||
<field name="ks_dashboard_top_menu_id" eval="ref('ks_dashboard_ninja.dashboards_menu_root')"/>
|
||||
<field name="ks_dashboard_default_template" eval="ref('ks_dashboard_ninja.ks_template_2')"/>
|
||||
<field name="ks_dashboard_active">1</field>
|
||||
<field name="ks_dashboard_group_access" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="demo_template3_dashboard" model="ks_dashboard_ninja.board">
|
||||
<field name="name">Template3 Dashboard</field>
|
||||
<field name="ks_dashboard_menu_name">Template3</field>
|
||||
<field name="ks_dashboard_top_menu_id" eval="ref('ks_dashboard_ninja.dashboards_menu_root')"/>
|
||||
<field name="ks_dashboard_default_template" eval="ref('ks_dashboard_ninja.ks_template_3')"/>
|
||||
<field name="ks_dashboard_active">1</field>
|
||||
<field name="ks_dashboard_group_access" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
3070
addons/ks_dashboard_ninja/i18n/en_US.po
Normal file
11
addons/ks_dashboard_ninja/models/Kpi_mail.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class KpSendMail(models.Model):
|
||||
_name = 'ks_dashboard_ninja.kpi_mail'
|
||||
_description = 'Dashboard Ninja Kpi mail'
|
||||
|
||||
|
||||
name = fields.Char(string="Email To:")
|
||||
17
addons/ks_dashboard_ninja/models/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from . import ks_dashboard_ninja
|
||||
from . import ks_dashboard_ninja_items
|
||||
from . import ks_item_action
|
||||
from . import ks_child_dashboard
|
||||
from . import ks_dashboard_filters
|
||||
from . import ks_dashboard_templates
|
||||
from . import ks_dn_to_do_item
|
||||
from . import ks_import_dashboard
|
||||
from . import Kpi_mail
|
||||
from . import res_settings
|
||||
from . import ks_ai_ninja_dashboard
|
||||
from . import ks_ai_whole_dashboard
|
||||
from . import ks_key_fetch
|
||||
from . import ks_chat_channel
|
||||
from . import base_model_extend
|
||||
|
||||
|
||||
34
addons/ks_dashboard_ninja/models/base_model_extend.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, api
|
||||
|
||||
|
||||
class BaseExtend(models.AbstractModel):
|
||||
_inherit = 'base'
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals):
|
||||
recs = super(BaseExtend, self).create(vals)
|
||||
if 'ir.' not in self._name and 'bus.' not in self._name and self.env.user.has_group('base.group_user'):
|
||||
# items = self.env['ks_dashboard_ninja.item'].search(
|
||||
# [['ks_model_id.model', '=', self._name]])
|
||||
# if items:
|
||||
# online_partners = self.env["bus.presence"].sudo().search([('status', '=', 'online')]).mapped('user_id.partner_id').ids
|
||||
# updates = [ for partner_id in online_partners]
|
||||
self.env['bus.bus']._sendone('ks_notification', 'Update: Dashboard Items', {'model': self._name})
|
||||
return recs
|
||||
|
||||
def write(self, vals):
|
||||
recs = super(BaseExtend, self).write(vals)
|
||||
if 'ir.' not in self._name and 'bus.' not in self._name and self.env.user.has_group('base.group_user') and 'res.partner' not in self._name:
|
||||
# items = self.env['ks_dashboard_ninja.item'].search(
|
||||
# [['ks_model_id.model', '=', self._name]])
|
||||
# if items:
|
||||
# online_partner = self.env["bus.presence"].search([('status', '=', 'online')]).mapped('user_id.partner_id').ids
|
||||
# updates = [[
|
||||
# (self._cr.dbname, 'res.partner', partner_id),
|
||||
# {'type': 'ks_notification', 'model': self._name},
|
||||
# {'id': self.id}
|
||||
# ] for partner_id in online_partner]
|
||||
self.env['bus.bus']._sendone('ks_notification', 'Update: Dashboard Items', {'model': self._name})
|
||||
return recs
|
||||
402
addons/ks_dashboard_ninja/models/ks_ai_ninja_dashboard.py
Normal file
@@ -0,0 +1,402 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
from urllib.parse import quote
|
||||
|
||||
import pandas as pd
|
||||
import requests
|
||||
from gtts import gTTS
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools import config
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KsDashboardNInjaAI(models.TransientModel):
|
||||
_name = 'ks_dashboard_ninja.arti_int'
|
||||
_description = 'AI Dashboard'
|
||||
|
||||
ks_type = fields.Selection([('ks_model', 'Model'), ('ks_keyword', 'Keywords')],
|
||||
string="Ks AI Type", default='ks_model')
|
||||
|
||||
ks_import_model_id = fields.Many2one('ir.model', string='Model ID',
|
||||
domain="[('access_ids','!=',False),('transient','=',False),"
|
||||
"('model','not ilike','base_import%'),'|',('model','not ilike','ir.%'),('model','=ilike','_%ir.%'),"
|
||||
"('model','not ilike','web_editor.%'),('model','not ilike','web_tour.%'),"
|
||||
"('model','!=','mail.thread'),('model','not ilike','ks_dash%'),('model','not ilike','ks_to%')]",
|
||||
help="Data source to fetch and read the data for the creation of dashboard items. ")
|
||||
|
||||
ks_import_model = fields.Many2one('ir.model', string='Model',
|
||||
domain="[('access_ids','!=',False),('transient','=',False),"
|
||||
"('model','not ilike','base_import%'),('model','not ilike','ir.%'),"
|
||||
"('model','not ilike','web_editor.%'),('model','not ilike','web_tour.%'),"
|
||||
"('model','!=','mail.thread'),('model','not ilike','ks_dash%'),('model','not ilike','ks_to%')]",
|
||||
help="Data source to fetch and read the data for the creation of dashboard items. ")
|
||||
ks_input_keywords = fields.Char("Ks Keywords")
|
||||
ks_model_show = fields.Boolean(default = False, compute='_compute_show_model')
|
||||
|
||||
@api.onchange('ks_input_keywords')
|
||||
def _compute_show_model(self):
|
||||
if self.ks_input_keywords and self.ks_type=="ks_keyword":
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('ks_dashboard_ninja.dn_api_key')
|
||||
url = self.env['ir.config_parameter'].sudo().get_param('ks_dashboard_ninja.url')
|
||||
if api_key and url:
|
||||
json_data = {'name': api_key,
|
||||
'type': self.ks_type,
|
||||
'keyword': self.ks_input_keywords
|
||||
}
|
||||
url = url + "/api/v1/ks_dn_keyword_gen"
|
||||
ks_response = requests.post(url, data=json_data)
|
||||
if json.loads(ks_response.text) == False:
|
||||
self.ks_model_show = True
|
||||
else:
|
||||
self.ks_model_show = False
|
||||
else:
|
||||
self.ks_model_show = False
|
||||
else:
|
||||
self.ks_model_show = False
|
||||
|
||||
@api.model
|
||||
def ks_get_keywords(self):
|
||||
url = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.url')
|
||||
if url:
|
||||
url = url + "/api/v1/ks_dn_get_keyword"
|
||||
ks_response = requests.post(url)
|
||||
if ks_response.status_code == 200:
|
||||
return json.loads(ks_response.text)
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def ks_do_action(self):
|
||||
headers = {"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Catch-Control": "no-cache",
|
||||
}
|
||||
|
||||
if self.ks_import_model_id:
|
||||
ks_model_name = self.ks_import_model_id.model
|
||||
ks_fields = self.env[ks_model_name].fields_get()
|
||||
ks_filtered_fields = {key: val for key, val in ks_fields.items() if val['type'] not in ['many2many', 'one2many', 'binary'] and'name' in val and val['name'] != 'id' and val['name'] != 'sequence' and val['store'] == True}
|
||||
ks_fields_name = {val['name']:val['type'] for val in ks_filtered_fields.values()}
|
||||
question = ("columns: "+ f"{ks_fields_name}")
|
||||
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.dn_api_key')
|
||||
url = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.url')
|
||||
if api_key and url:
|
||||
json_data = {'name': api_key,
|
||||
'question':question,
|
||||
'type': self.ks_type,
|
||||
'url': self.env['ir.config_parameter'].sudo().get_param('web.base.url'),
|
||||
'db_name': self.env.cr.dbname
|
||||
}
|
||||
url = url+"/api/v1/ks_dn_main_api"
|
||||
ks_ai_response = requests.post(url, data=json_data)
|
||||
if ks_ai_response.status_code == 200:
|
||||
ks_ai_response = json.loads(ks_ai_response.text)
|
||||
# create dummy dash to create items on the dashboard, later deleted it.
|
||||
ks_create_record = self.env['ks_dashboard_ninja.board'].create({
|
||||
'name': 'AI dashboard',
|
||||
'ks_dashboard_menu_name': 'AI menu',
|
||||
'ks_dashboard_default_template': self.env.ref('ks_dashboard_ninja.ks_blank', False).id,
|
||||
'ks_dashboard_top_menu_id': self.env['ir.ui.menu'].search([('name', '=', 'My Dashboards')])[0].id,
|
||||
})
|
||||
ks_dash_id = ks_create_record.id
|
||||
|
||||
ks_result = self.env['ks_dashboard_ninja.item'].create_ai_dash(ks_ai_response, ks_dash_id,
|
||||
ks_model_name)
|
||||
context = {'ks_dash_id': self._context['ks_dashboard_id'],
|
||||
'ks_dash_name': self.env['ks_dashboard_ninja.board'].search([
|
||||
('id','=',self._context['ks_dashboard_id'])]).name,'ks_delete_dash_id':ks_dash_id }
|
||||
|
||||
# return client action created through js for AI dashboard to render items on dummy dashboard
|
||||
if (ks_result == "success"):
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'name': 'Generate items with AI',
|
||||
'params': {'ks_dashboard_id': ks_create_record.id, 'explain_ai_whole': True},
|
||||
'tag': 'ks_ai_dashboard_ninja',
|
||||
'context': context,
|
||||
'target':'new'
|
||||
}
|
||||
else:
|
||||
self.env['ks_dashboard_ninja.board'].browse(ks_dash_id).unlink()
|
||||
raise ValidationError(_("Items didn't render because AI provides invalid response for this model.Please try again"))
|
||||
else:
|
||||
raise ValidationError(_("AI Responds with the following status:- %s") % ks_ai_response.text)
|
||||
else:
|
||||
raise ValidationError(_("Please enter URL and API Key in General Settings"))
|
||||
else:
|
||||
raise ValidationError(_("Please enter the Model"))
|
||||
|
||||
|
||||
|
||||
def ks_generate_item(self):
|
||||
if self.ks_input_keywords:
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.dn_api_key')
|
||||
url = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.url')
|
||||
if api_key and url:
|
||||
json_data = {'name': api_key,
|
||||
'type': self.ks_type,
|
||||
'keyword':self.ks_input_keywords
|
||||
}
|
||||
url = url + "/api/v1/ks_dn_keyword_gen"
|
||||
ks_response = requests.post(url, data=json_data)
|
||||
else:
|
||||
raise ValidationError(_("Please put API key and URL"))
|
||||
if json.loads(ks_response.text) != False and ks_response.status_code==200 :
|
||||
ks_ai_response = json.loads(ks_response.text)
|
||||
ks_dash_id = self._context['ks_dashboard_id']
|
||||
ks_model_name = ks_ai_response[0]['model']
|
||||
ks_result = self.env['ks_dashboard_ninja.item'].create_ai_dash(ks_ai_response, ks_dash_id,
|
||||
ks_model_name)
|
||||
if ks_result == "success":
|
||||
return{
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload',
|
||||
}
|
||||
else:
|
||||
raise ValidationError(_("Items didn't render, please try again!"))
|
||||
else:
|
||||
ks_model_name = self.ks_import_model.model
|
||||
ks_fields = self.env[ks_model_name].fields_get()
|
||||
ks_filtered_fields = {key: val for key, val in ks_fields.items() if
|
||||
val['type'] not in ['many2many', 'one2many', 'binary'] and 'name' in val and val[
|
||||
'name'] != 'id' and val['name'] != 'sequence' and val['store'] == True}
|
||||
ks_fields_name = {val['name']: val['type'] for val in ks_filtered_fields.values()}
|
||||
question = ("schema: " + f"{ks_fields_name}")
|
||||
model =("model:" + f"{ks_model_name}")
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.dn_api_key')
|
||||
url = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.url')
|
||||
if api_key and url:
|
||||
json_data = {'name': api_key,
|
||||
'question': self.ks_input_keywords,
|
||||
'type':self.ks_type,
|
||||
'schema':question,
|
||||
'model':model,
|
||||
'url': self.env['ir.config_parameter'].sudo().get_param('web.base.url'),
|
||||
'db_name': self.env.cr.dbname
|
||||
}
|
||||
url = url + "/api/v1/ks_dn_main_api"
|
||||
ks_ai_response = requests.post(url, data=json_data)
|
||||
if ks_ai_response.status_code == 200:
|
||||
ks_ai_response = json.loads(ks_ai_response.text)
|
||||
ks_dash_id = self._context['ks_dashboard_id']
|
||||
ks_model_name = (ks_ai_response[0]['model']).lower()
|
||||
if self.env['ir.model'].search([('model','=',ks_model_name)]).id or self.env['ir.model'].search([('name','=',ks_model_name)]).id:
|
||||
if self.env['ir.model'].search([('name','=',ks_model_name)]).id:
|
||||
ks_model_name = self.env['ir.model'].search([('name','=',ks_model_name)]).model
|
||||
else:
|
||||
ks_model_name = (ks_ai_response[0]['model']).lower()
|
||||
ks_result = self.env['ks_dashboard_ninja.item'].create_ai_dash(ks_ai_response, ks_dash_id,ks_model_name)
|
||||
if ks_result == "success":
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload',
|
||||
}
|
||||
else:
|
||||
raise ValidationError(_("Items didn't render, please try again!"))
|
||||
else:
|
||||
raise ValidationError(_("%s model does not exist.Please install")% ks_model_name)
|
||||
else:
|
||||
raise ValidationError(
|
||||
_("AI Responds with the following status:- %s") % ks_ai_response.text)
|
||||
|
||||
else:
|
||||
raise ValidationError(_("Please enter URL and API Key in General Settings"))
|
||||
else:
|
||||
raise ValidationError(_("Enter the input keywords to render the item"))
|
||||
|
||||
@api.model
|
||||
def ks_generate_analysis(self,ks_items_explain,ks_rest_items,dashboard_id):
|
||||
if ks_items_explain:
|
||||
result = []
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.dn_api_key')
|
||||
ks_url = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.url')
|
||||
words = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.ks_analysis_word_length')
|
||||
url = ks_url + "/api/v1/ks_dn_main_api"
|
||||
for i in range(0,len(ks_items_explain)):
|
||||
if api_key and url :
|
||||
json_data = {'name': api_key,
|
||||
'items':json.dumps(ks_items_explain[i]),
|
||||
'type':'ks_ai_explain',
|
||||
'url': self.env['ir.config_parameter'].sudo().get_param('web.base.url'),
|
||||
'db_name': self.env.cr.dbname,
|
||||
'words': words if words else 100
|
||||
}
|
||||
ks_response = requests.post(url, data=json_data)
|
||||
if ks_response.status_code == 200 and json.loads(ks_response.text):
|
||||
ks_ai_response = json.loads(ks_response.text)
|
||||
item = ks_ai_response[0]
|
||||
if item['analysis'] or item['insights']:
|
||||
try:
|
||||
self.env['ks_dashboard_ninja.item'].browse(item['id']).write({
|
||||
'ks_ai_analysis': item['analysis']+'ks_gap'+item['insights']
|
||||
})
|
||||
result.append(True)
|
||||
except:
|
||||
result
|
||||
else:
|
||||
result
|
||||
|
||||
else:
|
||||
result
|
||||
else:
|
||||
raise ValidationError(_("Please put API key and URL"))
|
||||
if len(result): #len(result)
|
||||
if self.env.context.get('explain_items_with_ai', False):
|
||||
self.env['ks_dashboard_ninja.board'].browse(dashboard_id).write({
|
||||
'ks_ai_explain_dash': False
|
||||
})
|
||||
else:
|
||||
self.env['ks_dashboard_ninja.board'].browse(dashboard_id).write({
|
||||
'ks_ai_explain_dash': True
|
||||
})
|
||||
return True
|
||||
else:
|
||||
raise ValidationError(_("AI Responds with the wrong analysis. Please try again "))
|
||||
elif ks_rest_items:
|
||||
if self.env.context.get('explain_items_with_ai', False):
|
||||
self.env['ks_dashboard_ninja.board'].browse(dashboard_id).write({
|
||||
'ks_ai_explain_dash': False
|
||||
})
|
||||
else:
|
||||
self.env['ks_dashboard_ninja.board'].browse(dashboard_id).write({
|
||||
'ks_ai_explain_dash': True
|
||||
})
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_ai_explain(self, item_id):
|
||||
print(item_id)
|
||||
res = self.env['ks_dashboard_ninja.item'].browse(item_id).ks_ai_analysis
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def ks_switch_default_dashboard(self,dashboard_id):
|
||||
self.env['ks_dashboard_ninja.board'].browse(dashboard_id).write({
|
||||
'ks_ai_explain_dash':False
|
||||
})
|
||||
return True
|
||||
@api.model
|
||||
def ks_generatetext_to_speech(self,item_id):
|
||||
if (item_id):
|
||||
try:
|
||||
ks_text = self.env['ks_dashboard_ninja.item'].browse(item_id).ks_ai_analysis
|
||||
if ks_text:
|
||||
language = 'en'
|
||||
ks_myobj = gTTS(text=ks_text, lang=language, slow=False)
|
||||
audio_data = io.BytesIO()
|
||||
ks_myobj.write_to_fp(audio_data)
|
||||
audio_data.seek(0)
|
||||
binary_data = audio_data.read()
|
||||
wav_file = base64.b64encode( binary_data).decode('UTF-8')
|
||||
data = {"snd": wav_file}
|
||||
return json.dumps(data)
|
||||
else:
|
||||
return False
|
||||
except Exception as e:
|
||||
_logger.error(e)
|
||||
raise ValidationError(_("Some problem in audio generation."))
|
||||
|
||||
else:
|
||||
return False
|
||||
|
||||
@api.model
|
||||
def ks_gen_chat_res(self,**kwargs):
|
||||
ks_question = kwargs.get('ks_question')
|
||||
url = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.url') + "/api/v1/get_sql_query"
|
||||
data = {
|
||||
"question": ks_question,
|
||||
}
|
||||
try:
|
||||
ks_response = requests.post(url,data=data)
|
||||
if (ks_response.status_code == 200):
|
||||
ks_response = json.loads(ks_response.text)['response']['Query']
|
||||
return self.ks_gen_dataframe(ks_response,ks_question)
|
||||
else:
|
||||
_logger.error('Unexpected error occurs')
|
||||
return False
|
||||
except Exception as e:
|
||||
_logger.error(e)
|
||||
return False
|
||||
|
||||
|
||||
|
||||
def ks_gen_dataframe(self,ks_query,question):
|
||||
host = config.get('db_host', False)
|
||||
user = quote(config.get('db_user', False))
|
||||
port = config.get('db_port', False) or 5432
|
||||
password = quote(config.get('db_password', False))
|
||||
db = config.get('db_name', False) or self.env.cr.dbname
|
||||
if not all([host, user, port, password, db]):
|
||||
_logger.error('some credentials are missing')
|
||||
return False
|
||||
else:
|
||||
sql_uri = f"postgresql+psycopg2://{user}:{password}@{host}:{port}/{db}"
|
||||
ks_fixed_url = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.url') + "/api/v1/get_fixed_query"
|
||||
try:
|
||||
df = pd.read_sql(ks_query, sql_uri)
|
||||
except Exception as e:
|
||||
ks_query_data = {
|
||||
'query':ks_query,
|
||||
'error':e
|
||||
}
|
||||
fixed_query = requests.post(ks_fixed_url, data=ks_query_data)
|
||||
if fixed_query.status_code == 200:
|
||||
ks_corrected_query = fixed_query.text
|
||||
df = pd.read_sql(ks_corrected_query, sql_uri)
|
||||
else:
|
||||
_logger.error('Error in generating Dataframe')
|
||||
return False
|
||||
if any(df.dtypes == 'datetime64[ns]'):
|
||||
datetime_columns = [col for col in df.columns if df[col].dtype == 'datetime64[ns]']
|
||||
df[datetime_columns] = df[datetime_columns].astype(str)
|
||||
|
||||
# Convert DataFrame to JSON
|
||||
if len(df) >= 100:
|
||||
df = df.head(100)
|
||||
partial_data = True
|
||||
|
||||
df_json = df.to_json(orient='records')
|
||||
|
||||
ans = "As dataframe having more data to analyse we are not showing dataframe summary"
|
||||
# Generate answer
|
||||
if len(df) < 13:
|
||||
ks_ans_url = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.url') + "/api/v1/get_answer"
|
||||
ks_ans_data = {'df':df.to_dict(orient='records'),'question':question}
|
||||
ans = requests.post(ks_ans_url, json = ks_ans_data)
|
||||
if ans.status_code == 200:
|
||||
ans = ans.text
|
||||
response_json = {
|
||||
"Dataframe": df_json,
|
||||
"Answer": ans,
|
||||
}
|
||||
else:
|
||||
_logger.error('Error in generating answer')
|
||||
return False
|
||||
else:
|
||||
response_json = {
|
||||
"Dataframe": df_json,
|
||||
"Answer": ans,
|
||||
}
|
||||
return response_json
|
||||
92
addons/ks_dashboard_ninja/models/ks_ai_whole_dashboard.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo import fields, models, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KsAIDashboardninja(models.TransientModel):
|
||||
_name = 'ks_dashboard_ninja.ai_dashboard'
|
||||
_description = 'AI Dashboard'
|
||||
|
||||
ks_import_model_id = fields.Many2one('ir.model', string='Model',
|
||||
domain="[('access_ids','!=',False),('transient','=',False),"
|
||||
"('model','not ilike','base_import%'),('model','not ilike','ir.%'),"
|
||||
"('model','not ilike','web_editor.%'),('model','not ilike','web_tour.%'),"
|
||||
"('model','!=','mail.thread'),('model','not ilike','ks_dash%'),('model','not ilike','ks_to%')]",
|
||||
help="Data source to fetch and read the data for the creation of dashboard items. ", required=True)
|
||||
|
||||
ks_dash_name = fields.Char(string="Dashboard Name", required=True, size=35)
|
||||
ks_menu_name = fields.Char(string="Menu Name", required=True, size=35)
|
||||
ks_top_menu_id = fields.Many2one('ir.ui.menu',
|
||||
domain="[('parent_id','=',False)]",
|
||||
string="Show Under Menu", required=True,
|
||||
default=lambda self: self.env['ir.ui.menu'].search(
|
||||
[('name', '=', 'My Dashboards')])[0])
|
||||
ks_template = fields.Many2one('ks_dashboard_ninja.board_template',
|
||||
default=lambda self: self.env.ref('ks_dashboard_ninja.ks_blank',
|
||||
False),
|
||||
string="Dashboard Template")
|
||||
|
||||
def ks_do_action(self):
|
||||
headers = {"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
"Catch-Control": "no-cache",
|
||||
}
|
||||
|
||||
if self.ks_import_model_id:
|
||||
ks_model_name = self.ks_import_model_id.model
|
||||
ks_fields = self.env[ks_model_name].fields_get()
|
||||
ks_filtered_fields = {key: val for key, val in ks_fields.items() if val['type'] not in ['many2many', 'one2many', 'binary'] and'name' in val and val['name'] != 'id' and val['name'] != 'sequence' and val['store'] == True}
|
||||
ks_fields_name = {val['name']:val['type'] for val in ks_filtered_fields.values()}
|
||||
question = ("columns: "+ f"{ks_fields_name}")
|
||||
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.dn_api_key')
|
||||
url = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.url')
|
||||
if api_key and url:
|
||||
json_data = {'name': api_key,
|
||||
'question':question,
|
||||
'url':self.env['ir.config_parameter'].sudo().get_param('web.base.url'),
|
||||
'db_name':self.env.cr.dbname
|
||||
}
|
||||
url = url+"/api/v1/ks_dn_main_api"
|
||||
ks_ai_response = requests.post(url, data=json_data)
|
||||
if ks_ai_response.status_code == 200:
|
||||
ks_ai_response = json.loads(ks_ai_response.text)
|
||||
ks_create_record = self.env['ks_dashboard_ninja.board'].create({
|
||||
'name': self.ks_dash_name,
|
||||
'ks_dashboard_menu_name': self.ks_menu_name,
|
||||
'ks_dashboard_default_template': self.ks_template.id,
|
||||
'ks_dashboard_top_menu_id': self.ks_top_menu_id.id,
|
||||
})
|
||||
ks_dash_id = ks_create_record.id
|
||||
|
||||
ks_result = self.env['ks_dashboard_ninja.item'].create_ai_dash(ks_ai_response, ks_dash_id,
|
||||
ks_model_name)
|
||||
|
||||
if (ks_result == "success"):
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload',
|
||||
}
|
||||
else:
|
||||
self.env['ks_dashboard_ninja.board'].browse(ks_dash_id).unlink()
|
||||
raise ValidationError(_("Items didn't render, please try again!"))
|
||||
else:
|
||||
raise ValidationError(_("AI Responds with the following status:- %s") % ks_ai_response.text)
|
||||
else:
|
||||
raise ValidationError(_("Please enter URL and API Key in General Settings"))
|
||||
else:
|
||||
raise ValidationError(_("Please enter the Model"))
|
||||
|
||||
|
||||
|
||||
|
||||
36
addons/ks_dashboard_ninja/models/ks_chat_channel.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo import models, fields, _
|
||||
|
||||
|
||||
class ChatChannel(models.Model):
|
||||
_inherit = 'discuss.channel'
|
||||
|
||||
ks_dashboard_board_id = fields.Many2one('ks_dashboard_ninja.board')
|
||||
ks_dashboard_item_id = fields.Many2one('ks_dashboard_ninja.item')
|
||||
|
||||
def ks_chat_wizard_channel_id(self, **kwargs):
|
||||
item_id = kwargs.get('item_id')
|
||||
dashboard_id = kwargs.get('dashboard_id')
|
||||
item_name = kwargs.get('item_name')
|
||||
dashboard_name = kwargs.get('dashboard_name')
|
||||
|
||||
channel = self.search([('ks_dashboard_item_id', '=', item_id)], limit=1)
|
||||
|
||||
if not channel:
|
||||
users = self.env['res.users'].search([('groups_id', 'in', self.env.ref('base.group_user').ids)]).mapped('partner_id.id')
|
||||
|
||||
channel = self.create({
|
||||
'name': f"{dashboard_name} - {item_name}",
|
||||
'ks_dashboard_board_id': dashboard_id,
|
||||
'ks_dashboard_item_id': item_id,
|
||||
'channel_member_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in users]
|
||||
})
|
||||
|
||||
notification = Markup('<div class="o_mail_notification">%s</div>') % _("created this channel.")
|
||||
channel.message_post(body=notification, message_type="notification", subtype_xmlid="mail.mt_comment")
|
||||
self.env.user._bus_send_store(channel)
|
||||
|
||||
return channel.id if channel else None
|
||||
26
addons/ks_dashboard_ninja/models/ks_child_dashboard.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class KsDashboardNinjaBoardItemAction(models.Model):
|
||||
_name = 'ks_dashboard_ninja.child_board'
|
||||
_description = 'Dashboard Ninja Child Board'
|
||||
|
||||
name = fields.Char()
|
||||
ks_dashboard_ninja_id = fields.Many2one("ks_dashboard_ninja.board", string="Select Dashboard")
|
||||
ks_gridstack_config = fields.Char('Item Configurations')
|
||||
# ks_board_active_user_ids = fields.Many2many('res.users')
|
||||
ks_active = fields.Boolean("Is Selected")
|
||||
ks_dashboard_menu_name = fields.Char(string="Menu Name", related='ks_dashboard_ninja_id.ks_dashboard_menu_name', store=True)
|
||||
board_type = fields.Selection([('default', 'Default'), ('child', 'Child')])
|
||||
company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company)
|
||||
ks_computed_group_access = fields.Many2many('res.groups', compute='_compute_ks_computed_group_access', store=True)
|
||||
|
||||
@api.depends('ks_dashboard_ninja_id', 'ks_dashboard_ninja_id.ks_dashboard_group_access')
|
||||
def _compute_ks_computed_group_access(self):
|
||||
for record in self:
|
||||
record.ks_computed_group_access = record.ks_dashboard_ninja_id.ks_dashboard_group_access
|
||||
|
||||
def write(self,vals):
|
||||
return super(KsDashboardNinjaBoardItemAction, self).write(vals)
|
||||
182
addons/ks_dashboard_ninja/models/ks_country_bounds.py
Normal file
@@ -0,0 +1,182 @@
|
||||
country = {
|
||||
'AF': ('Afghanistan', (60.5284298033, 29.318572496, 75.1580277851, 38.4862816432)),
|
||||
'AO': ('Angola', (11.6400960629, -17.9306364885, 24.0799052263, -4.43802336998)),
|
||||
'AL': ('Albania', (19.3044861183, 39.624997667, 21.0200403175, 42.6882473822)),
|
||||
'AE': ('United Arab Emirates', (51.5795186705, 22.4969475367, 56.3968473651, 26.055464179)),
|
||||
'AR': ('Argentina', (-73.4154357571, -55.25, -53.628348965, -21.8323104794)),
|
||||
'AM': ('Armenia', (43.5827458026, 38.7412014837, 46.5057198423, 41.2481285671)),
|
||||
'AQ': ('Antarctica', (-180.0, -90.0, 180.0, -63.2706604895)),
|
||||
'TF': ('Fr. S. and Antarctic Lands', (68.72, -49.775, 70.56, -48.625)),
|
||||
'AU': ('Australia', (113.338953078, -43.6345972634, 153.569469029, -10.6681857235)),
|
||||
'AT': ('Austria', (9.47996951665, 46.4318173285, 16.9796667823, 49.0390742051)),
|
||||
'AZ': ('Azerbaijan', (44.7939896991, 38.2703775091, 50.3928210793, 41.8606751572)),
|
||||
'BI': ('Burundi', (29.0249263852, -4.49998341229, 30.752262811, -2.34848683025)),
|
||||
'BE': ('Belgium', (2.51357303225, 49.5294835476, 6.15665815596, 51.4750237087)),
|
||||
'BJ': ('Benin', (0.772335646171, 6.14215770103, 3.79711225751, 12.2356358912)),
|
||||
'BF': ('Burkina Faso', (-5.47056494793, 9.61083486576, 2.17710778159, 15.1161577418)),
|
||||
'BD': ('Bangladesh', (88.0844222351, 20.670883287, 92.6727209818, 26.4465255803)),
|
||||
'BG': ('Bulgaria', (22.3805257504, 41.2344859889, 28.5580814959, 44.2349230007)),
|
||||
'BS': ('Bahamas', (-78.98, 23.71, -77.0, 27.04)),
|
||||
'BA': ('Bosnia and Herz.', (15.7500260759, 42.65, 19.59976, 45.2337767604)),
|
||||
'BY': ('Belarus', (23.1994938494, 51.3195034857, 32.6936430193, 56.1691299506)),
|
||||
'BZ': ('Belize', (-89.2291216703, 15.8869375676, -88.1068129138, 18.4999822047)),
|
||||
'BO': ('Bolivia', (-69.5904237535, -22.8729187965, -57.4983711412, -9.76198780685)),
|
||||
'BR': ('Brazil', (-73.9872354804, -33.7683777809, -34.7299934555, 5.24448639569)),
|
||||
'BN': ('Brunei', (114.204016555, 4.007636827, 115.450710484, 5.44772980389)),
|
||||
'BT': ('Bhutan', (88.8142484883, 26.7194029811, 92.1037117859, 28.2964385035)),
|
||||
'BW': ('Botswana', (19.8954577979, -26.8285429827, 29.4321883481, -17.6618156877)),
|
||||
'CF': ('Central African Rep.', (14.4594071794, 2.2676396753, 27.3742261085, 11.1423951278)),
|
||||
'CA': ('Canada', (-140.99778, 41.6751050889, -52.6480987209, 83.23324)),
|
||||
'CH': ('Switzerland', (6.02260949059, 45.7769477403, 10.4427014502, 47.8308275417)),
|
||||
'CL': ('Chile', (-75.6443953112, -55.61183, -66.95992, -17.5800118954)),
|
||||
'CN': ('China', (73.6753792663, 18.197700914, 135.026311477, 53.4588044297)),
|
||||
'CI': ('Ivory Coast', (-8.60288021487, 4.33828847902, -2.56218950033, 10.5240607772)),
|
||||
'CM': ('Cameroon', (8.48881554529, 1.72767263428, 16.0128524106, 12.8593962671)),
|
||||
'CD': ('Congo (Kinshasa)', (12.1823368669, -13.2572266578, 31.1741492042, 5.25608775474)),
|
||||
'CG': ('Congo (Brazzaville)', (11.0937728207, -5.03798674888, 18.4530652198, 3.72819651938)),
|
||||
'CO': ('Colombia', (-78.9909352282, -4.29818694419, -66.8763258531, 12.4373031682)),
|
||||
'CR': ('Costa Rica', (-85.94172543, 8.22502798099, -82.5461962552, 11.2171192489)),
|
||||
'CU': ('Cuba', (-84.9749110583, 19.8554808619, -74.1780248685, 23.1886107447)),
|
||||
'CY': ('Cyprus', (32.2566671079, 34.5718694118, 34.0048808123, 35.1731247015)),
|
||||
'CZ': ('Czech Rep.', (12.2401111182, 48.5553052842, 18.8531441586, 51.1172677679)),
|
||||
'DE': ('Germany', (5.98865807458, 47.3024876979, 15.0169958839, 54.983104153)),
|
||||
'DJ': ('Djibouti', (41.66176, 10.9268785669, 43.3178524107, 12.6996385767)),
|
||||
'DK': ('Denmark', (8.08997684086, 54.8000145534, 12.6900061378, 57.730016588)),
|
||||
'DO': ('Dominican Rep.', (-71.9451120673, 17.598564358, -68.3179432848, 19.8849105901)),
|
||||
'DZ': ('Algeria', (-8.68439978681, 19.0573642034, 11.9995056495, 37.1183806422)),
|
||||
'EC': ('Ecuador', (-80.9677654691, -4.95912851321, -75.2337227037, 1.3809237736)),
|
||||
'EG': ('Egypt', (24.70007, 22.0, 36.86623, 31.58568)),
|
||||
'ER': ('Eritrea', (36.3231889178, 12.4554157577, 43.0812260272, 17.9983074)),
|
||||
'ES': ('Spain', (-9.39288367353, 35.946850084, 3.03948408368, 43.7483377142)),
|
||||
'EE': ('Estonia', (23.3397953631, 57.4745283067, 28.1316992531, 59.6110903998)),
|
||||
'ET': ('Ethiopia', (32.95418, 3.42206, 47.78942, 14.95943)),
|
||||
'FI': ('Finland', (20.6455928891, 59.846373196, 31.5160921567, 70.1641930203)),
|
||||
'FJ': ('Fiji', (-180.0, -18.28799, 180.0, -16.0208822567)),
|
||||
'FK': ('Falkland Is.', (-61.2, -52.3, -57.75, -51.1)),
|
||||
'FR': ('France', (-54.5247541978, 2.05338918702, 9.56001631027, 51.1485061713)),
|
||||
'GA': ('Gabon', (8.79799563969, -3.97882659263, 14.4254557634, 2.32675751384)),
|
||||
'GB': ('United Kingdom', (-7.57216793459, 49.959999905, 1.68153079591, 58.6350001085)),
|
||||
'GE': ('Georgia', (39.9550085793, 41.0644446885, 46.6379081561, 43.553104153)),
|
||||
'GH': ('Ghana', (-3.24437008301, 4.71046214438, 1.0601216976, 11.0983409693)),
|
||||
'GN': ('Guinea', (-15.1303112452, 7.3090373804, -7.83210038902, 12.5861829696)),
|
||||
'GM': ('Gambia', (-16.8415246241, 13.1302841252, -13.8449633448, 13.8764918075)),
|
||||
'GW': ('Guinea Bissau', (-16.6774519516, 11.0404116887, -13.7004760401, 12.6281700708)),
|
||||
'GQ': ('Eq. Guinea', (9.3056132341, 1.01011953369, 11.285078973, 2.28386607504)),
|
||||
'GR': ('Greece', (20.1500159034, 34.9199876979, 26.6041955909, 41.8269046087)),
|
||||
'GL': ('Greenland', (-73.297, 60.03676, -12.20855, 83.64513)),
|
||||
'GT': ('Guatemala', (-92.2292486234, 13.7353376327, -88.2250227526, 17.8193260767)),
|
||||
'GY': ('Guyana', (-61.4103029039, 1.26808828369, -56.5393857489, 8.36703481692)),
|
||||
'HN': ('Honduras', (-89.3533259753, 12.9846857772, -83.147219001, 16.0054057886)),
|
||||
'HR': ('Croatia', (13.6569755388, 42.47999136, 19.3904757016, 46.5037509222)),
|
||||
'HT': ('Haiti', (-74.4580336168, 18.0309927434, -71.6248732164, 19.9156839055)),
|
||||
'HU': ('Hungary', (16.2022982113, 45.7594811061, 22.710531447, 48.6238540716)),
|
||||
'ID': ('Indonesia', (95.2930261576, -10.3599874813, 141.03385176, 5.47982086834)),
|
||||
'IN': ('India', (68.1766451354, 7.96553477623, 97.4025614766, 35.4940095078)),
|
||||
'IE': ('Ireland', (-9.97708574059, 51.6693012559, -6.03298539878, 55.1316222195)),
|
||||
'IR': ('Iran', (44.1092252948, 25.0782370061, 63.3166317076, 39.7130026312)),
|
||||
'IQ': ('Iraq', (38.7923405291, 29.0990251735, 48.5679712258, 37.3852635768)),
|
||||
'IS': ('Iceland', (-24.3261840479, 63.4963829617, -13.609732225, 66.5267923041)),
|
||||
'IL': ('Israel', (34.2654333839, 29.5013261988, 35.8363969256, 33.2774264593)),
|
||||
'IT': ('Italy', (6.7499552751, 36.619987291, 18.4802470232, 47.1153931748)),
|
||||
'JM': ('Jamaica', (-78.3377192858, 17.7011162379, -76.1996585761, 18.5242184514)),
|
||||
'JO': ('Jordan', (34.9226025734, 29.1974946152, 39.1954683774, 33.3786864284)),
|
||||
'JP': ('Japan', (129.408463169, 31.0295791692, 145.543137242, 45.5514834662)),
|
||||
'KZ': ('Kazakhstan', (46.4664457538, 40.6623245306, 87.3599703308, 55.3852501491)),
|
||||
'KE': ('Kenya', (33.8935689697, -4.67677, 41.8550830926, 5.506)),
|
||||
'KG': ('Kyrgyzstan', (69.464886916, 39.2794632025, 80.2599902689, 43.2983393418)),
|
||||
'KH': ('Cambodia', (102.3480994, 10.4865436874, 107.614547968, 14.5705838078)),
|
||||
'KR': ('S. Korea', (126.117397903, 34.3900458847, 129.468304478, 38.6122429469)),
|
||||
'KW': ('Kuwait', (46.5687134133, 28.5260627304, 48.4160941913, 30.0590699326)),
|
||||
'LA': ('Laos', (100.115987583, 13.88109101, 107.564525181, 22.4647531194)),
|
||||
'LB': ('Lebanon', (35.1260526873, 33.0890400254, 36.6117501157, 34.6449140488)),
|
||||
'LR': ('Liberia', (-11.4387794662, 4.35575511313, -7.53971513511, 8.54105520267)),
|
||||
'LY': ('Libya', (9.31941084152, 19.58047, 25.16482, 33.1369957545)),
|
||||
'LK': ('Sri Lanka', (79.6951668639, 5.96836985923, 81.7879590189, 9.82407766361)),
|
||||
'LS': ('Lesotho', (26.9992619158, -30.6451058896, 29.3251664568, -28.6475017229)),
|
||||
'LT': ('Lithuania', (21.0558004086, 53.9057022162, 26.5882792498, 56.3725283881)),
|
||||
'LU': ('Luxembourg', (5.67405195478, 49.4426671413, 6.24275109216, 50.1280516628)),
|
||||
'LV': ('Latvia', (21.0558004086, 55.61510692, 28.1767094256, 57.9701569688)),
|
||||
'MA': ('Morocco', (-17.0204284327, 21.4207341578, -1.12455115397, 35.7599881048)),
|
||||
'MD': ('Moldova', (26.6193367856, 45.4882831895, 30.0246586443, 48.4671194525)),
|
||||
'MG': ('Madagascar', (43.2541870461, -25.6014344215, 50.4765368996, -12.0405567359)),
|
||||
'MX': ('Mexico', (-117.12776, 14.5388286402, -86.811982388, 32.72083)),
|
||||
'MK': ('Macedonia', (20.46315, 40.8427269557, 22.9523771502, 42.3202595078)),
|
||||
'ML': ('Mali', (-12.1707502914, 10.0963607854, 4.27020999514, 24.9745740829)),
|
||||
'MM': ('Myanmar', (92.3032344909, 9.93295990645, 101.180005324, 28.335945136)),
|
||||
'ME': ('Montenegro', (18.45, 41.87755, 20.3398, 43.52384)),
|
||||
'MN': ('Mongolia', (87.7512642761, 41.5974095729, 119.772823928, 52.0473660345)),
|
||||
'MZ': ('Mozambique', (30.1794812355, -26.7421916643, 40.7754752948, -10.3170960425)),
|
||||
'MR': ('Mauritania', (-17.0634232243, 14.6168342147, -4.92333736817, 27.3957441269)),
|
||||
'MW': ('Malawi', (32.6881653175, -16.8012997372, 35.7719047381, -9.23059905359)),
|
||||
'MY': ('Malaysia', (100.085756871, 0.773131415201, 119.181903925, 6.92805288332)),
|
||||
'NA': ('Namibia', (11.7341988461, -29.045461928, 25.0844433937, -16.9413428687)),
|
||||
'NC': ('New Caledonia', (164.029605748, -22.3999760881, 167.120011428, -20.1056458473)),
|
||||
'NE': ('Niger', (0.295646396495, 11.6601671412, 15.9032466977, 23.4716684026)),
|
||||
'NG': ('Nigeria', (2.69170169436, 4.24059418377, 14.5771777686, 13.8659239771)),
|
||||
'NI': ('Nicaragua', (-87.6684934151, 10.7268390975, -83.147219001, 15.0162671981)),
|
||||
'NL': ('Netherlands', (3.31497114423, 50.803721015, 7.09205325687, 53.5104033474)),
|
||||
'NO': ('Norway', (4.99207807783, 58.0788841824, 31.29341841, 80.6571442736)),
|
||||
'NP': ('Nepal', (80.0884245137, 26.3978980576, 88.1748043151, 30.4227169866)),
|
||||
'NZ': ('New Zealand', (166.509144322, -46.641235447, 178.517093541, -34.4506617165)),
|
||||
'OM': ('Oman', (52.0000098, 16.6510511337, 59.8080603372, 26.3959343531)),
|
||||
'PK': ('Pakistan', (60.8742484882, 23.6919650335, 77.8374507995, 37.1330309108)),
|
||||
'PA': ('Panama', (-82.9657830472, 7.2205414901, -77.2425664944, 9.61161001224)),
|
||||
'PE': ('Peru', (-81.4109425524, -18.3479753557, -68.6650797187, -0.0572054988649)),
|
||||
'PH': ('Philippines', (117.17427453, 5.58100332277, 126.537423944, 18.5052273625)),
|
||||
'PG': ('Papua New Guinea', (141.000210403, -10.6524760881, 156.019965448, -2.50000212973)),
|
||||
'PL': ('Poland', (14.0745211117, 49.0273953314, 24.0299857927, 54.8515359564)),
|
||||
'PR': ('Puerto Rico', (-67.2424275377, 17.946553453, -65.5910037909, 18.5206011011)),
|
||||
'KP': ('N. Korea', (124.265624628, 37.669070543, 130.780007359, 42.9853868678)),
|
||||
'PT': ('Portugal', (-9.52657060387, 36.838268541, -6.3890876937, 42.280468655)),
|
||||
'PY': ('Paraguay', (-62.6850571357, -27.5484990374, -54.2929595608, -19.3427466773)),
|
||||
'QA': ('Qatar', (50.7439107603, 24.5563308782, 51.6067004738, 26.1145820175)),
|
||||
'RO': ('Romania', (20.2201924985, 43.6884447292, 29.62654341, 48.2208812526)),
|
||||
'RU': ('Russia', (-180.0, 41.151416124, 180.0, 81.2504)),
|
||||
'RW': ('Rwanda', (29.0249263852, -2.91785776125, 30.8161348813, -1.13465911215)),
|
||||
'SA': ('Saudi Arabia', (34.6323360532, 16.3478913436, 55.6666593769, 32.161008816)),
|
||||
'SD': ('Sudan', (21.93681, 8.61972971293, 38.4100899595, 22.0)),
|
||||
'SS': ('S. Sudan', (23.8869795809, 3.50917, 35.2980071182, 12.2480077571)),
|
||||
'SN': ('Senegal', (-17.6250426905, 12.332089952, -11.4678991358, 16.5982636581)),
|
||||
'SB': ('Solomon Is.', (156.491357864, -10.8263672828, 162.398645868, -6.59933847415)),
|
||||
'SL': ('Sierra Leone', (-13.2465502588, 6.78591685631, -10.2300935531, 10.0469839543)),
|
||||
'SV': ('El Salvador', (-90.0955545723, 13.1490168319, -87.7235029772, 14.4241327987)),
|
||||
'SO': ('Somalia', (40.98105, -1.68325, 51.13387, 12.02464)),
|
||||
'RS': ('Serbia', (18.82982, 42.2452243971, 22.9860185076, 46.1717298447)),
|
||||
'SR': ('Suriname', (-58.0446943834, 1.81766714112, -53.9580446031, 6.0252914494)),
|
||||
'SK': ('Slovakia', (16.8799829444, 47.7584288601, 22.5581376482, 49.5715740017)),
|
||||
'SI': ('Slovenia', (13.6981099789, 45.4523163926, 16.5648083839, 46.8523859727)),
|
||||
'SE': ('Sweden', (11.0273686052, 55.3617373725, 23.9033785336, 69.1062472602)),
|
||||
'SZ': ('Swaziland', (30.6766085141, -27.2858794085, 32.0716654803, -25.660190525)),
|
||||
'SY': ('Syria', (35.7007979673, 32.312937527, 42.3495910988, 37.2298725449)),
|
||||
'TD': ('Chad', (13.5403935076, 7.42192454674, 23.88689, 23.40972)),
|
||||
'TG': ('Togo', (-0.0497847151599, 5.92883738853, 1.86524051271, 11.0186817489)),
|
||||
'TH': ('Thailand', (97.3758964376, 5.69138418215, 105.589038527, 20.4178496363)),
|
||||
'TJ': ('Tajikistan', (67.4422196796, 36.7381712916, 74.9800024759, 40.9602133245)),
|
||||
'TM': ('Turkmenistan', (52.5024597512, 35.2706639674, 66.5461503437, 42.7515510117)),
|
||||
'TL': ('East Timor', (124.968682489, -9.39317310958, 127.335928176, -8.27334482181)),
|
||||
'TT': ('Trinidad and Tobago', (-61.95, 10.0, -60.895, 10.89)),
|
||||
'TN': ('Tunisia', (7.52448164229, 30.3075560572, 11.4887874691, 37.3499944118)),
|
||||
'TR': ('Turkey', (26.0433512713, 35.8215347357, 44.7939896991, 42.1414848903)),
|
||||
'TW': ('Taiwan', (120.106188593, 21.9705713974, 121.951243931, 25.2954588893)),
|
||||
'TZ': ('Tanzania', (29.3399975929, -11.7209380022, 40.31659, -0.95)),
|
||||
'UG': ('Uganda', (29.5794661801, -1.44332244223, 35.03599, 4.24988494736)),
|
||||
'UA': ('Ukraine', (22.0856083513, 44.3614785833, 40.0807890155, 52.3350745713)),
|
||||
'UY': ('Uruguay', (-58.4270741441, -34.9526465797, -53.209588996, -30.1096863746)),
|
||||
'US': ('United States', (-171.791110603, 18.91619, -66.96466, 71.3577635769)),
|
||||
'UZ': ('Uzbekistan', (55.9289172707, 37.1449940049, 73.055417108, 45.5868043076)),
|
||||
'VE': ('Venezuela', (-73.3049515449, 0.724452215982, -59.7582848782, 12.1623070337)),
|
||||
'VN': ('Vietnam', (102.170435826, 8.59975962975, 109.33526981, 23.3520633001)),
|
||||
'VU': ('Vanuatu', (166.629136998, -16.5978496233, 167.844876744, -14.6264970842)),
|
||||
'PS': ('West Bank', (34.9274084816, 31.3534353704, 35.5456653175, 32.5325106878)),
|
||||
'YE': ('Yemen', (42.6048726743, 12.5859504257, 53.1085726255, 19.0000033635)),
|
||||
'ZA': ('South Africa', (16.3449768409, -34.8191663551, 32.830120477, -22.0913127581)),
|
||||
'ZM': ('Zambia', (21.887842645, -17.9612289364, 33.4856876971, -8.23825652429)),
|
||||
'ZW': ('Zimbabwe', (25.2642257016, -22.2716118303, 32.8498608742, -15.5077869605)),
|
||||
}
|
||||
|
||||
|
||||
def get_country_code(country_id):
|
||||
if country_id in country.keys():
|
||||
return country.get(country_id)
|
||||
else:
|
||||
return {}
|
||||
92
addons/ks_dashboard_ninja/models/ks_dashboard_filters.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.addons.ks_dashboard_ninja.common_lib.filter_tools import replace_company_domain
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class KsDashboardNinjaTemplate(models.Model):
|
||||
_name = 'ks_dashboard_ninja.board_defined_filters'
|
||||
_description = 'Dashboard Ninja Defined Filters'
|
||||
|
||||
name = fields.Char('Filter Label')
|
||||
ks_dashboard_board_id = fields.Many2one('ks_dashboard_ninja.board', string="Dashboard")
|
||||
ks_model_id = fields.Many2one('ir.model', string='Model',
|
||||
domain="[('access_ids','!=',False),('transient','=',False),"
|
||||
"('model','not ilike','base_import%'),'|',('model','not ilike','ir.%'),('model','=ilike','_%ir.%'),"
|
||||
"('model','not ilike','web_editor.%'),('model','not ilike','web_tour.%'),"
|
||||
"('model','!=','mail.thread'),('model','not ilike','ks_dash%'), ('model','not ilike','ks_to%')]",
|
||||
help="Data source to fetch and read the data for the creation of dashboard items. ")
|
||||
ks_domain = fields.Char(string="Domain", help="Define conditions for filter. ")
|
||||
ks_domain_temp = fields.Char(string="Domain Substitute")
|
||||
ks_model_name = fields.Char(related='ks_model_id.model', string="Model Name")
|
||||
display_type = fields.Selection([
|
||||
('line_section', "Section")], default=False, help="Technical field for UX purpose.")
|
||||
sequence = fields.Integer(default=10,
|
||||
help="Gives the sequence order when displaying a list of payment terms lines.")
|
||||
ks_is_active = fields.Boolean(string="Active")
|
||||
|
||||
@api.onchange('ks_domain')
|
||||
def ks_domain_onchange(self):
|
||||
for rec in self:
|
||||
if rec.ks_model_id:
|
||||
try:
|
||||
ks_domain = rec.ks_domain
|
||||
if ks_domain and "%UID" in ks_domain:
|
||||
ks_domain = ks_domain.replace('"%UID"', str(self.env.user.id))
|
||||
if ks_domain and "%MYCOMPANY" in ks_domain:
|
||||
ks_domain = replace_company_domain(ks_domain, self.env.company.id, self.env.companies.ids)
|
||||
self.env[rec.ks_model_id.model].search_count(safe_eval(ks_domain))
|
||||
except Exception as e:
|
||||
raise ValidationError(_("Something went wrong . Possibly it is due to wrong input type for domain"))
|
||||
|
||||
@api.constrains('ks_domain', 'ks_model_id')
|
||||
def ks_domain_check(self):
|
||||
for rec in self:
|
||||
if rec.ks_model_id and not rec.ks_domain:
|
||||
raise ValidationError(_("Domain can not be empty"))
|
||||
|
||||
|
||||
|
||||
class KsDashboardNinjaTemplate(models.Model):
|
||||
_name = 'ks_dashboard_ninja.board_custom_filters'
|
||||
_description = 'Dashboard Ninja Custom Filters'
|
||||
|
||||
name = fields.Char("Filter Label")
|
||||
ks_dashboard_board_id = fields.Many2one('ks_dashboard_ninja.board', string="Dashboard")
|
||||
ks_model_id = fields.Many2one('ir.model', string='Model',
|
||||
domain="[('access_ids','!=',False),('transient','=',False),"
|
||||
"('model','not ilike','base_import%'),'|',('model','not ilike','ir.%'),('model','=ilike','_%ir.%'),"
|
||||
"('model','not ilike','web_editor.%'),('model','not ilike','web_tour.%'),"
|
||||
"('model','!=','mail.thread'),('model','not ilike','ks_dash%'), ('model','not ilike','ks_to%')]",
|
||||
help="Data source to fetch and read the data for the creation of dashboard items. ")
|
||||
ks_domain_field_id = fields.Many2one('ir.model.fields',
|
||||
domain="[('model_id','=',ks_model_id),"
|
||||
"('name','!=','id'),('store','=',True),"
|
||||
"('ttype', 'in', ['boolean', 'char', "
|
||||
"'date', 'datetime', 'float', 'integer', 'html', 'many2many', "
|
||||
"'many2one', 'monetary', 'one2many', 'text', 'selection'])]",
|
||||
string="Domain Field")
|
||||
|
||||
@api.onchange('ks_model_id')
|
||||
def on_change_ks_model_id(self):
|
||||
self.ks_domain_field_id = False
|
||||
|
||||
|
||||
class KsDashboardNinjaTemplateFilters(models.Model):
|
||||
_name = 'ks_dashboard_ninja.favourite_filters'
|
||||
_description = 'Dashboard Ninja Favourite Filters'
|
||||
|
||||
name = fields.Char("Filter Label")
|
||||
ks_dashboard_board_id = fields.Many2one('ks_dashboard_ninja.board', string="Dashboard")
|
||||
ks_filter = fields.Char("Filter")
|
||||
ks_access_id = fields.Integer("Access Id")
|
||||
ks_filter_type = fields.Char(default='favourite')
|
||||
|
||||
_sql_constraints = [
|
||||
('name_uniq', 'UNIQUE (name)', 'The name of the filter must be unique!'),
|
||||
]
|
||||
|
||||
|
||||
1502
addons/ks_dashboard_ninja/models/ks_dashboard_ninja.py
Normal file
4632
addons/ks_dashboard_ninja/models/ks_dashboard_ninja_items.py
Normal file
43
addons/ks_dashboard_ninja/models/ks_dashboard_templates.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class KsDashboardNinjaTemplate(models.Model):
|
||||
_name = 'ks_dashboard_ninja.board_template'
|
||||
_description = 'Dashboard Ninja Template'
|
||||
|
||||
name = fields.Char()
|
||||
ks_gridstack_config = fields.Char()
|
||||
ks_item_count = fields.Integer()
|
||||
ks_template_type = fields.Selection([('ks_default', 'Predefined'), ('ks_custom', 'Custom')],
|
||||
string="Template Format")
|
||||
ks_dashboard_item_ids = fields.One2many('ks_dashboard_ninja.item', 'ks_dashboard_board_template_id',
|
||||
string="Template Type")
|
||||
ks_dashboard_board_id = fields.Many2one('ks_dashboard_ninja.board', string="Dashboard", help="""
|
||||
Items Configuration and their position in the dashboard will be copied from the selected dashboard
|
||||
and will be saved as template.
|
||||
""")
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for val in vals_list:
|
||||
if val.get('ks_template_type', False) and val.get('ks_dashboard_board_id', False):
|
||||
dashboard_id = self.env['ks_dashboard_ninja.board'].browse(val.get('ks_dashboard_board_id'))
|
||||
val['ks_gridstack_config'] = dashboard_id.ks_gridstack_config
|
||||
val['ks_item_count'] = len(dashboard_id.ks_dashboard_items_ids)
|
||||
val['ks_dashboard_item_ids'] = [(4, x.copy({'ks_dashboard_ninja_board_id': False}).id) for x in
|
||||
dashboard_id.ks_dashboard_items_ids]
|
||||
recs = super(KsDashboardNinjaTemplate, self).create(vals_list)
|
||||
return recs
|
||||
|
||||
def write(self, val):
|
||||
if val.get('ks_dashboard_board_id', False):
|
||||
dashboard_id = self.env['ks_dashboard_ninja.board'].browse(val.get('ks_dashboard_board_id'))
|
||||
val['ks_gridstack_config'] = dashboard_id.ks_gridstack_config
|
||||
val['ks_item_count'] = len(dashboard_id.ks_dashboard_items_ids)
|
||||
val['ks_dashboard_item_ids'] = [(6, 0,
|
||||
[x.copy({'ks_dashboard_ninja_board_id': False}).id for x in
|
||||
dashboard_id.ks_dashboard_items_ids])]
|
||||
recs = super(KsDashboardNinjaTemplate, self).write(val)
|
||||
return recs
|
||||
145
addons/ks_dashboard_ninja/models/ks_dn_to_do_item.py
Normal file
@@ -0,0 +1,145 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class KsDashboardNinjaItems(models.Model):
|
||||
_inherit = 'ks_dashboard_ninja.item'
|
||||
|
||||
ks_to_do_preview = fields.Char("To Do Preview", default="To Do Preview")
|
||||
ks_dn_header_lines = fields.One2many('ks_to.do.headers', 'ks_dn_item_id')
|
||||
ks_to_do_data = fields.Char(string="To Do Data in JSon", compute='ks_get_to_do_view_data', compute_sudo=False)
|
||||
ks_header_bg_color = fields.Char(string="Header Background Color", default="#8e24aa,0.99",
|
||||
help=' Select the background color with transparency. ')
|
||||
|
||||
@api.depends('ks_dn_header_lines', 'ks_dashboard_item_type')
|
||||
def ks_get_to_do_view_data(self):
|
||||
for rec in self:
|
||||
ks_to_do_data = rec._ksGetToDOData()
|
||||
rec.ks_to_do_data = ks_to_do_data
|
||||
|
||||
def _ksGetToDOData(self):
|
||||
ks_to_do_data = {
|
||||
'label': [],
|
||||
'ks_link': [],
|
||||
'ks_href_id': [],
|
||||
'ks_section_id': [],
|
||||
'ks_content': {},
|
||||
'ks_content_record_id': {},
|
||||
'ks_content_active': {}
|
||||
}
|
||||
|
||||
if self.ks_dn_header_lines:
|
||||
for ks_dn_header_line in self.ks_dn_header_lines:
|
||||
ks_to_do_header_label = ks_dn_header_line.ks_to_do_header[:]
|
||||
ks_to_do_data['label'].append(ks_to_do_header_label)
|
||||
ks_dn_header_line_id = str(ks_dn_header_line.id)
|
||||
if type(ks_dn_header_line.id).__name__ != 'int' and ks_dn_header_line.id.ref != None:
|
||||
ks_dn_header_line_id = ks_dn_header_line.id.ref
|
||||
if ' ' in ks_dn_header_line.ks_to_do_header:
|
||||
ks_temp = ks_dn_header_line.ks_to_do_header.replace(" ", "")
|
||||
ks_to_do_data['ks_link'].append('#' + ks_temp + ks_dn_header_line_id)
|
||||
ks_to_do_data['ks_href_id'].append(ks_temp + str(ks_dn_header_line.id))
|
||||
|
||||
elif ks_dn_header_line.ks_to_do_header[0].isdigit():
|
||||
ks_temp = ks_dn_header_line.ks_to_do_header.replace(
|
||||
ks_dn_header_line.ks_to_do_header[0], 'z')
|
||||
ks_to_do_data['ks_link'].append('#' + ks_temp + ks_dn_header_line_id)
|
||||
ks_to_do_data['ks_href_id'].append(ks_temp + str(ks_dn_header_line.id))
|
||||
else:
|
||||
ks_to_do_data['ks_link'].append('#' + ks_dn_header_line.ks_to_do_header + ks_dn_header_line_id)
|
||||
ks_to_do_data['ks_href_id'].append(ks_dn_header_line.ks_to_do_header + str(ks_dn_header_line.id))
|
||||
ks_to_do_data['ks_section_id'].append(str(ks_dn_header_line.id))
|
||||
if len(ks_dn_header_line.ks_to_do_description_lines):
|
||||
for ks_to_do_description_line in ks_dn_header_line.ks_to_do_description_lines:
|
||||
if ' ' in ks_dn_header_line.ks_to_do_header or ks_dn_header_line.ks_to_do_header[0].isdigit():
|
||||
if ks_to_do_data['ks_content'].get(ks_temp +
|
||||
str(ks_dn_header_line.id), False):
|
||||
|
||||
ks_to_do_data['ks_content'][ks_temp +
|
||||
str(ks_dn_header_line.id)].append(
|
||||
ks_to_do_description_line.ks_description)
|
||||
ks_to_do_data['ks_content_record_id'][ks_temp +
|
||||
str(ks_dn_header_line.id)].append(
|
||||
str(ks_to_do_description_line.id))
|
||||
ks_to_do_data['ks_content_active'][ks_temp +
|
||||
str(ks_dn_header_line.id)].append(
|
||||
str(ks_to_do_description_line.ks_active))
|
||||
else:
|
||||
ks_to_do_data['ks_content'][ks_temp +
|
||||
str(ks_dn_header_line.id)] = [
|
||||
ks_to_do_description_line.ks_description]
|
||||
ks_to_do_data['ks_content_record_id'][ks_temp +
|
||||
str(ks_dn_header_line.id)] = [
|
||||
str(ks_to_do_description_line.id)]
|
||||
ks_to_do_data['ks_content_active'][ks_temp +
|
||||
str(ks_dn_header_line.id)] = [
|
||||
str(ks_to_do_description_line.ks_active)]
|
||||
else:
|
||||
if ks_to_do_data['ks_content'].get(ks_dn_header_line.ks_to_do_header +
|
||||
str(ks_dn_header_line.id), False):
|
||||
|
||||
ks_to_do_data['ks_content'][ks_dn_header_line.ks_to_do_header +
|
||||
str(ks_dn_header_line.id)].append(
|
||||
ks_to_do_description_line.ks_description)
|
||||
ks_to_do_data['ks_content_record_id'][ks_dn_header_line.ks_to_do_header +
|
||||
str(ks_dn_header_line.id)].append(
|
||||
str(ks_to_do_description_line.id))
|
||||
ks_to_do_data['ks_content_active'][ks_dn_header_line.ks_to_do_header +
|
||||
str(ks_dn_header_line.id)].append(
|
||||
str(ks_to_do_description_line.ks_active))
|
||||
else:
|
||||
ks_to_do_data['ks_content'][ks_dn_header_line.ks_to_do_header +
|
||||
str(ks_dn_header_line.id)] = [
|
||||
ks_to_do_description_line.ks_description]
|
||||
ks_to_do_data['ks_content_record_id'][ks_dn_header_line.ks_to_do_header +
|
||||
str(ks_dn_header_line.id)] = [
|
||||
str(ks_to_do_description_line.id)]
|
||||
ks_to_do_data['ks_content_active'][ks_dn_header_line.ks_to_do_header +
|
||||
str(ks_dn_header_line.id)] = [
|
||||
str(ks_to_do_description_line.ks_active)]
|
||||
|
||||
ks_to_do_data = json.dumps(ks_to_do_data)
|
||||
else:
|
||||
ks_to_do_data = False
|
||||
return ks_to_do_data
|
||||
|
||||
|
||||
|
||||
|
||||
class KsToDoheaders(models.Model):
|
||||
_name = 'ks_to.do.headers'
|
||||
_description = "to do headers"
|
||||
|
||||
ks_dn_item_id = fields.Many2one('ks_dashboard_ninja.item')
|
||||
ks_to_do_header = fields.Char('Header')
|
||||
ks_to_do_description_lines = fields.One2many('ks_to.do.description', 'ks_to_do_header_id')
|
||||
|
||||
@api.constrains('ks_to_do_header')
|
||||
def ks_to_do_header_check(self):
|
||||
for rec in self:
|
||||
if rec.ks_to_do_header:
|
||||
ks_check = bool(re.match('^[A-Z, a-z,0-9,_]+$', rec.ks_to_do_header))
|
||||
if not ks_check:
|
||||
raise ValidationError(_("Special characters are not allowed only string and digits allow for section header"))
|
||||
|
||||
@api.onchange('ks_to_do_header')
|
||||
def ks_to_do_header_onchange(self):
|
||||
for rec in self:
|
||||
if rec.ks_to_do_header:
|
||||
ks_check = bool(re.match('^[A-Z, a-z,0-9,_]+$', rec.ks_to_do_header))
|
||||
if not ks_check:
|
||||
raise ValidationError(_("Special characters are not allowed only string and digits allow for section header"))
|
||||
|
||||
class KsToDODescription(models.Model):
|
||||
_name = 'ks_to.do.description'
|
||||
_description = 'to do description'
|
||||
|
||||
ks_to_do_header_id = fields.Many2one('ks_to.do.headers')
|
||||
ks_description = fields.Text('Description')
|
||||
ks_active = fields.Boolean('Active Description', default=True)
|
||||
34
addons/ks_dashboard_ninja/models/ks_import_dashboard.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo import fields, models, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KsDashboardNInjaImport(models.TransientModel):
|
||||
_name = 'ks_dashboard_ninja.import'
|
||||
_description = 'Import Dashboard'
|
||||
|
||||
ks_import_dashboard = fields.Binary(string="Upload Dashboard", attachment=True)
|
||||
ks_top_menu_id = fields.Many2one('ir.ui.menu', string="Show Under Menu", domain="[('parent_id','=',False)]",
|
||||
required=True,
|
||||
default=lambda self: self.env['ir.ui.menu'].search(
|
||||
[('name', '=', 'My Dashboards')]))
|
||||
|
||||
def ks_do_action(self):
|
||||
for rec in self:
|
||||
try:
|
||||
ks_result = base64.b64decode(rec.ks_import_dashboard)
|
||||
self.env['ks_dashboard_ninja.board'].ks_import_dashboard(ks_result, self.ks_top_menu_id)
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'reload',
|
||||
}
|
||||
except Exception as E:
|
||||
_logger.warning(E)
|
||||
raise ValidationError(_(str(E)))
|
||||
28
addons/ks_dashboard_ninja/models/ks_item_action.py
Normal file
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class KsDashboardNinjaBoardItemAction(models.TransientModel):
|
||||
_name = 'ks_ninja_dashboard.item_action'
|
||||
_description = 'Dashboard Ninja Item Actions'
|
||||
|
||||
name = fields.Char()
|
||||
ks_dashboard_item_ids = fields.Many2many("ks_dashboard_ninja.item", string="Dashboard Items")
|
||||
ks_action = fields.Selection([('move', 'Move'),
|
||||
('duplicate', 'Duplicate'),
|
||||
], string="Action")
|
||||
ks_dashboard_ninja_id = fields.Many2one("ks_dashboard_ninja.board", string="Select Dashboard")
|
||||
ks_dashboard_ninja_ids = fields.Many2many("ks_dashboard_ninja.board", string="Select Dashboards")
|
||||
|
||||
# Move or Copy item to another dashboard action
|
||||
|
||||
def action_item_move_copy_action(self):
|
||||
if self.ks_action == 'move':
|
||||
for item in self.ks_dashboard_item_ids:
|
||||
item.ks_dashboard_ninja_board_id = self.ks_dashboard_ninja_id
|
||||
elif self.ks_action == 'duplicate':
|
||||
# Using sudo here to allow creating same item without any security error
|
||||
for dashboard_id in self.ks_dashboard_ninja_ids:
|
||||
for item in self.ks_dashboard_item_ids:
|
||||
item.sudo().copy({'ks_dashboard_ninja_board_id': dashboard_id.id})
|
||||
34
addons/ks_dashboard_ninja/models/ks_key_fetch.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo import fields, models, _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KsAIDashboardFetch(models.TransientModel):
|
||||
_name = 'ks_dashboard_ninja.fetch_key'
|
||||
_description = 'Fetch API key'
|
||||
|
||||
ks_email_id = fields.Char(string="Email ID")
|
||||
ks_api_key =fields.Char(string="Generated AI API Key")
|
||||
ks_show_api_key = fields.Boolean(string="Show key",default=False)
|
||||
|
||||
def ks_fetch_details(self):
|
||||
url = self.env['ir.config_parameter'].sudo().get_param(
|
||||
'ks_dashboard_ninja.url')
|
||||
if url and self.ks_email_id:
|
||||
url = url + "/api/v1/ks_dn_fetch_api"
|
||||
json_data = {'email':self.ks_email_id}
|
||||
ks_ai_response = requests.post(url,data=json_data)
|
||||
if ks_ai_response.status_code == 200:
|
||||
ks_ai_response = json.loads(ks_ai_response.text)
|
||||
self.ks_api_key = ks_ai_response
|
||||
self.ks_show_api_key = True
|
||||
else:
|
||||
raise ValidationError(_("Error generates with following status %s"),ks_ai_response.status_code)
|
||||
57
addons/ks_dashboard_ninja/models/res_settings.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
|
||||
import requests
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo import fields, models, _
|
||||
|
||||
|
||||
class ResConfig(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
dn_api_key = fields.Char(string="Dashboard AI API Key",store=True,
|
||||
config_parameter='ks_dashboard_ninja.dn_api_key')
|
||||
enable_chart_zoom = fields.Boolean(string="Enable Zooming for charts", store=True,
|
||||
config_parameter='ks_dashboard_ninja.enable_chart_zoom')
|
||||
url = fields.Char(string="URL", store=True,
|
||||
config_parameter="ks_dashboard_ninja.url")
|
||||
ks_email_id = fields.Char(string="Email ID",store=True,config_parameter="ks_dashboard_ninja.ks_email_id")
|
||||
ks_analysis_word_length = fields.Selection([("50","50 words"),("100","100 words"),("150","150 words"),("200","200 words"),],default ="100", string="AI Analysis length", store=True,config_parameter="ks_dashboard_ninja.ks_analysis_word_length")
|
||||
def Open_wizard(self):
|
||||
if self.url and self.ks_email_id:
|
||||
try:
|
||||
url = self.url + "/api/v1/ks_dn_fetch_api"
|
||||
json_data = {'email':self.ks_email_id,
|
||||
'url':self.env['ir.config_parameter'].sudo().get_param('web.base.url'),
|
||||
'db_name':self.env.cr.dbname
|
||||
}
|
||||
ks_ai_response = requests.post(url,data=json_data)
|
||||
except Exception as e:
|
||||
raise ValidationError(_("Please enter correct URL"))
|
||||
if ks_ai_response.status_code == 200:
|
||||
try:
|
||||
ks_ai_response = json.loads(ks_ai_response.text)
|
||||
except Exception as e:
|
||||
ks_ai_response = False
|
||||
if ks_ai_response == "success":
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Success'),
|
||||
'message': 'API key sent on Email ID',
|
||||
'sticky': False,
|
||||
}
|
||||
}
|
||||
elif ks_ai_response == 'key already generated':
|
||||
raise ValidationError(
|
||||
_("key already generated.If you need assistance, feel free to contact at sales@ksolves.com"))
|
||||
else:
|
||||
raise ValidationError(_("Either you have entered wrong URL path or there is some problem in sending request. If you need assistance, feel free to contact at sales@ksolves.com"))
|
||||
else:
|
||||
raise ValidationError(_("Some problem in sending request.Please contact at sales@ksolves.com"))
|
||||
else:
|
||||
raise ValidationError(_("Please enter URL and Email ID"))
|
||||
|
||||
5
addons/ks_dashboard_ninja/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
xlrd==2.0.1
|
||||
openpyxl == 3.1.2
|
||||
gTTS == 2.5.1
|
||||
pandas==2.1.2
|
||||
SQLAlchemy==2.0.32
|
||||
36
addons/ks_dashboard_ninja/security/ir.model.access.csv
Normal file
@@ -0,0 +1,36 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_ks_dashboard_ninja_board,ks_dashboard_ninja.board,model_ks_dashboard_ninja_board,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_ninja_kpi_mail,ks_dashboard_ninja.kpi_mail,model_ks_dashboard_ninja_kpi_mail,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_ninja_item,ks_dashboard_ninja.item,model_ks_dashboard_ninja_item,base.group_user,1,1,1,1
|
||||
access_ks_to_do_headers,ks_to.do.headers,model_ks_to_do_headers,base.group_user,1,1,1,1
|
||||
access_ks_to_do_description,ks_to.do.description,model_ks_to_do_description,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_ninja_child_board,ks_dashboard_ninja.child_board,model_ks_dashboard_ninja_child_board,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_ninja_board_defined_filters,ks_dashboard_ninja.board_defined_filters,model_ks_dashboard_ninja_board_defined_filters,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_ninja_board_custom_filters,ks_dashboard_ninja.board_custom_filters,model_ks_dashboard_ninja_board_custom_filters,base.group_user,1,1,1,1
|
||||
|
||||
|
||||
access_ks_dashboard_ninja_board_template,ks_dashboard_ninja.board_template,model_ks_dashboard_ninja_board_template,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_ninja_item_goal,ks_dashboard_ninja_item_goal,model_ks_dashboard_ninja_item_goal,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_ninja_item_action,ks_dashboard_ninja_item_action,model_ks_dashboard_ninja_item_action,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_item_multiplier,ks_dashboard_item.multiplier,model_ks_dashboard_item_multiplier,base.group_user,1,1,1,1
|
||||
access_ks_ninja_dashboard_item_action,ks_ninja_dashboard.item_action,model_ks_ninja_dashboard_item_action,base.group_user,1,1,1,0
|
||||
access_ks_dashboard_group_by,ks.dashboard.group.by,model_ks_dashboard_group_by,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_csv_group_by,ks.dashboard.csv.group.by,model_ks_dashboard_csv_group_by,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_new,ks.dashboard.new,model_ks_dashboard_new,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_csv_new,ks.dashboard.csv.new,model_ks_dashboard_csv_new,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_ninja_import,ks_dashboard_ninja.import,model_ks_dashboard_ninja_import,base.group_system,1,1,1,0
|
||||
access_ir_actions_act_window_view,ir.actions.act_window.view,base.model_ir_actions_act_window_view,base.group_user,1,0,0,0
|
||||
access_ir_actions_act_window,ir.actions.act_window,base.model_ir_actions_act_window,base.group_user,1,0,0,0
|
||||
access_ir_actions_client,ir.actions.client,base.model_ir_actions_client,base.group_user,1,0,0,0
|
||||
access_ir_ui_menu,ir.ui.menu,base.model_ir_ui_menu,base.group_user,1,1,0,0
|
||||
|
||||
access_ir_model_group_user,ir.model,base.model_ir_model,base.group_user,1,0,0,0
|
||||
access_ir_model_fields_group_user,ir.model.fields,base.model_ir_model_fields,base.group_user,1,0,0,0
|
||||
|
||||
access_ir_model_ks_dashboard_wizard,ks_dashboard_wizard,model_ks_dashboard_wizard,base.group_user,1,1,1,1
|
||||
access_ir_model_ks_duplicate_dashboard_wizard,ks_duplicate_dashboard__wizard,model_ks_dashboard_duplicate_wizard,base.group_user,1,1,1,1
|
||||
access_ir_model_ks_delete_dashboard_wizard,ks_delete_dashboard__wizard,model_ks_dashboard_delete_wizard,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_ninja_arti_int,ks_dashboard_ninja.arti_int,model_ks_dashboard_ninja_arti_int,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_ninja_ai_dashboard,ks_dashboard_ninja.ai_dashboard,model_ks_dashboard_ninja_ai_dashboard,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_ninja_fetch_key,ks_dashboard_ninja.fetch_key,model_ks_dashboard_ninja_fetch_key,base.group_user,1,1,1,1
|
||||
access_ks_dashboard_ninja_favourite_filters,ks_dashboard_ninja.favourite_filters,model_ks_dashboard_ninja_favourite_filters,,1,1,1,1
|
||||
|
59
addons/ks_dashboard_ninja/security/ks_security_groups.xml
Normal file
@@ -0,0 +1,59 @@
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="ir_rule_ks_dashboard_item_company_restrictions" model="ir.rule">
|
||||
<field name="name">Dashboard Item Company Restriction: User Can only view their company and sub companies
|
||||
items.
|
||||
</field>
|
||||
<field name="model_id" ref="model_ks_dashboard_ninja_item"/>
|
||||
<field name="domain_force">
|
||||
['|',('ks_company_id','in', company_ids),('ks_company_id','=',False)]</field>
|
||||
<field name="perm_create" eval="True"/>
|
||||
<field name="perm_unlink" eval="True"/>
|
||||
<field name="perm_read" eval="True"/>
|
||||
<field name="perm_write" eval="True"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_ks_accessible_dashboards" model="ir.rule">
|
||||
<field name="name">Dashboard Record Level Groups Access: Show dashboards matching user's assigned groups.</field>
|
||||
<field name="model_id" ref="model_ks_dashboard_ninja_board"/>
|
||||
<field name="domain_force">['|', ('ks_dashboard_group_access', '=' , False), ('ks_dashboard_group_access','in', user.groups_id.ids)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]" />
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_ks_accessible_child_dashboards" model="ir.rule">
|
||||
<field name="name">Child Dashboard Record Level Groups Access: Show dashboards matching user's assigned groups.</field>
|
||||
<field name="model_id" ref="model_ks_dashboard_ninja_child_board"/>
|
||||
<field name="domain_force">['|', ('ks_computed_group_access', '=', False), ('ks_computed_group_access', 'in', user.groups_id.ids)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]" />
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_ks_admin_accessible_dashboards" model="ir.rule">
|
||||
<field name="name">Dashboard Record Level Groups Access: Show all dashboards to admin regardless of assigned groups.</field>
|
||||
<field name="model_id" ref="model_ks_dashboard_ninja_board"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[Command.link(ref('base.group_system'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_rule_ks_admin_accessible_child_dashboards" model="ir.rule">
|
||||
<field name="name">Child Dashboard Record Level Groups Access: Show all dashboards to admin regardless of assigned groups.</field>
|
||||
<field name="model_id" ref="model_ks_dashboard_ninja_child_board"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
<field name="groups" eval="[Command.link(ref('base.group_system'))]"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.module.category" id="ks_dashboard_ninja_security_groups">
|
||||
<field name="name">Dashboard Ninja Rights</field>
|
||||
</record>
|
||||
|
||||
<record model="res.groups" id="ks_dashboard_ninja_group_manager">
|
||||
<field name="name">Show Full Dashboard Features</field>
|
||||
<field name="category_id" ref="ks_dashboard_ninja.ks_dashboard_ninja_security_groups"/>
|
||||
</record>
|
||||
|
||||
<record id="base.group_system" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('ks_dashboard_ninja.ks_dashboard_ninja_group_manager'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
BIN
addons/ks_dashboard_ninja/static/description/DN-5.gif
Normal file
|
After Width: | Height: | Size: 2.1 MiB |
7
addons/ks_dashboard_ninja/static/description/css/bootstrap.min.css
vendored
Normal file
BIN
addons/ks_dashboard_ninja/static/description/icon.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 923 KiB |
|
After Width: | Height: | Size: 362 KiB |
|
After Width: | Height: | Size: 777 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 910 KiB |
BIN
addons/ks_dashboard_ninja/static/description/img/Ksolves.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 459 KiB |
|
After Width: | Height: | Size: 592 KiB |
BIN
addons/ks_dashboard_ninja/static/description/img/account.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
addons/ks_dashboard_ninja/static/description/img/account2.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
addons/ks_dashboard_ninja/static/description/img/account3.jpg
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
addons/ks_dashboard_ninja/static/description/img/account4.jpg
Normal file
|
After Width: | Height: | Size: 175 KiB |
BIN
addons/ks_dashboard_ninja/static/description/img/account5.jpg
Normal file
|
After Width: | Height: | Size: 168 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 608 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 298 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 7.5 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 730 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 282 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.19727 11.62L9.0006 7.81667C9.44977 7.3675 9.44977 6.6325 9.0006 6.18334L5.19727 2.38" fill="#E84A5F"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 218 B |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 663 B |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 902 B |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 148 KiB |
@@ -0,0 +1,4 @@
|
||||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3951 2.03003H9.69505C8.65505 2.03003 7.80505 2.87003 7.80505 3.91003V4.85003C7.80505 5.89003 8.64505 6.73003 9.68505 6.73003H14.3951C15.4351 6.73003 16.2751 5.89003 16.2751 4.85003V3.91003C16.2851 2.87003 15.4351 2.03003 14.3951 2.03003Z" fill="white"/>
|
||||
<path d="M17.2851 4.85001C17.2851 6.44001 15.9851 7.74001 14.3951 7.74001H9.69508C8.10508 7.74001 6.80508 6.44001 6.80508 4.85001C6.80508 4.29001 6.20508 3.94001 5.70508 4.20001C4.29508 4.95001 3.33508 6.44001 3.33508 8.15001V17.56C3.33508 20.02 5.34508 22.03 7.80508 22.03H16.2851C18.7451 22.03 20.7551 20.02 20.7551 17.56V8.15001C20.7551 6.44001 19.7951 4.95001 18.3851 4.20001C17.8851 3.94001 17.2851 4.29001 17.2851 4.85001ZM12.4251 16.98H8.04508C7.63508 16.98 7.29508 16.64 7.29508 16.23C7.29508 15.82 7.63508 15.48 8.04508 15.48H12.4251C12.8351 15.48 13.1751 15.82 13.1751 16.23C13.1751 16.64 12.8351 16.98 12.4251 16.98ZM15.0451 12.98H8.04508C7.63508 12.98 7.29508 12.64 7.29508 12.23C7.29508 11.82 7.63508 11.48 8.04508 11.48H15.0451C15.4551 11.48 15.7951 11.82 15.7951 12.23C15.7951 12.64 15.4551 12.98 15.0451 12.98Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |