Compare commits

...

297 Commits

Author SHA1 Message Date
72f76d8bd7 Tower: unpublish cetmix_tower_git — remove source from 18.0 branch
All checks were successful
addon-qualify / qualify (push) Successful in 13s
2026-05-11 06:33:26 +00:00
6138670817 Tower: unpublish cetmix_tower_yaml — remove source from 18.0 branch
Some checks failed
addon-qualify / qualify (push) Has been cancelled
2026-05-11 06:33:19 +00:00
b5e2d5b9b3 Tower: unpublish cetmix_tower_webhook — remove source from 18.0 branch
All checks were successful
addon-qualify / qualify (push) Successful in 12s
2026-05-11 06:32:03 +00:00
043d30e16e Tower: unpublish cetmix_tower_server_queue — remove source from 18.0 branch
Some checks failed
addon-qualify / qualify (push) Has been cancelled
2026-05-11 06:31:58 +00:00
92a74356f7 Tower: unpublish cetmix_tower_server — remove source from 18.0 branch
Some checks failed
addon-qualify / qualify (push) Has been cancelled
2026-05-11 06:31:49 +00:00
d4be3eed12 Tower: unpublish cetmix_tower_ovh — remove source from 18.0 branch
Some checks failed
addon-qualify / qualify (push) Has been cancelled
2026-05-11 06:31:42 +00:00
c5ff83ec82 Tower: unpublish cetmix_tower_aws — remove source from 18.0 branch
Some checks failed
addon-qualify / qualify (push) Has been cancelled
2026-05-11 06:31:35 +00:00
2eebf70d5c Tower: unpublish cetmix_tower — remove source from 18.0 branch
Some checks failed
addon-qualify / qualify (push) Has been cancelled
2026-05-11 06:31:29 +00:00
OdooSky v3
906e5ebd6d sync(branch 18.0): merge tag fixes for tk_construction_management + ks_dashboard_ninja
All checks were successful
addon-qualify / qualify (push) Successful in 13s
The Pillar 1 CI gate flagged drift between tag state and branch HEAD
on these two addons. Today (2026-05-09) we force-tagged fixes for:

- tk_construction_management/18.0.2.0.8: add name= to <app>, chatter
  migration to <chatter/>, chart NaN guard, scope .o_action_manager
  CSS rule, remove dasdsa debug logs.
- ks_dashboard_ninja/18.0.1.1.7: rename webpackChunk_am5 to
  webpackChunk_am5_ksdn so it does not collide with synconics_bi_dashboard.

Replicating the same content on the 18.0 branch HEAD so future pushes
do not silently revert these fixes.
2026-05-09 13:54:49 +02:00
OdooSky v3
a103d8129b ci: vendor qualify-addon.py (Pillar 1 self-contained)
All checks were successful
addon-qualify / qualify (push) Successful in 13s
2026-05-09 13:38:33 +02:00
OdooSky v3
13a0f0faa1 ci: addon qualification gate (Pillar 1)
Some checks failed
addon-qualify / qualify (push) Failing after 53s
2026-05-09 13:33:39 +02:00
888f87d8ec Tower: upload ks_dashboard_ninja 18.0.1.1.7 (was 18.0.1.1.7, via marketplace) 2026-05-08 21:13:33 +00:00
63c62699f5 Tower: upload tk_construction_management 18.0.2.0.8 (was 18.0.2.0.8, via marketplace) 2026-05-08 19:20:12 +00:00
c412640ca2 Tower: unpublish ks_dashboard_ninja — remove source from 18.0 branch 2026-05-07 12:21:13 +00:00
fd62a75b51 Tower: upload ks_dashboard_ninja 18.0.1.1.7 (was 18.0.1.1.7, via marketplace) 2026-05-07 12:17:21 +00:00
e50acbac83 Tower: upload cx_web_refresh_from_backend 18.0.1.0.0 (was 18.0.1.0.0, via marketplace) 2026-05-03 18:55:11 +00:00
ed5f0d6535 Tower: upload cetmix_tower_yaml 18.0.2.0.0 (was 18.0.2.0.0, via marketplace) 2026-05-03 18:55:03 +00:00
bf36bd383a Tower: upload cetmix_tower_webhook 18.0.1.0.1 (was 18.0.1.0.1, via marketplace) 2026-05-03 18:54:56 +00:00
ee7e3fb398 Tower: upload cetmix_tower_server_queue 18.0.2.0.0 (was 18.0.2.0.0, via marketplace) 2026-05-03 18:54:52 +00:00
c83da26305 Tower: upload cetmix_tower_server 18.0.2.0.0 (was 18.0.2.0.0, via marketplace) 2026-05-03 18:54:38 +00:00
5880120a84 Tower: upload cetmix_tower_ovh 18.0.1.0.1 (was 18.0.1.0.1, via marketplace) 2026-05-03 18:54:32 +00:00
207a122e37 Tower: upload cetmix_tower_git 18.0.1.0.2 (was 18.0.1.0.2, via marketplace) 2026-05-03 18:54:24 +00:00
e40caa55e8 Tower: upload cetmix_tower_aws 18.0.1.0.1 (was 18.0.1.0.1, via marketplace) 2026-05-03 18:54:19 +00:00
d4789aaed6 Tower: upload cetmix_tower 18.0.1.0.0 (was 18.0.1.0.0, via marketplace) 2026-05-03 18:54:12 +00:00
OdooSky Tower
7048450ad5 Cleanup: remove orphan addon source at_master_order (no tags reference it) 2026-05-02 13:20:08 +02:00
OdooSky Tower
a3b7a9a521 Cleanup: remove orphan addon source at_accounting (no tags reference it) 2026-05-02 13:20:06 +02:00
686c06f52c Tower: unpublish om_data_remove — remove source from 18.0 branch 2026-05-02 11:15:12 +00:00
99dd5ad688 Tower: upload om_data_remove 1.4 (via marketplace) 2026-05-02 06:09:09 +00:00
2d88df12c0 Tower: upload om_data_remove 1.4 (via marketplace) 2026-05-02 06:09:07 +00:00
bb3dd53fbd Tower: upload om_data_remove 1.4 (via marketplace) 2026-05-02 06:09:05 +00:00
d2c8473122 Tower: upload om_data_remove 1.4 (via marketplace) 2026-05-02 06:09:03 +00:00
f726994409 Tower: upload om_data_remove 1.4 (via marketplace) 2026-05-02 06:09:02 +00:00
5915c32ba7 Tower: upload om_data_remove 1.4 (via marketplace) 2026-05-02 06:08:59 +00:00
2d76c220f0 Tower: upload om_data_remove 1.4 (via marketplace) 2026-05-02 06:08:58 +00:00
005da073e8 Tower: upload om_data_remove 1.4 (via marketplace) 2026-05-02 06:08:57 +00:00
7a4a89cef9 Tower: upload om_data_remove 1.4 (via marketplace) 2026-05-02 06:08:55 +00:00
9295668143 Tower: upload om_data_remove 1.4 (via marketplace) 2026-05-02 06:08:54 +00:00
c53a0b6b30 Tower: upload om_data_remove 1.4 (via marketplace) 2026-05-02 06:08:52 +00:00
a498653c26 Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:57 +00:00
730cb8ddde Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:57 +00:00
33b1eeedf8 Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:56 +00:00
4915aaa882 Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:55 +00:00
ffd1dd0b18 Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:55 +00:00
c670be57f6 Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:54 +00:00
4ac0b04bca Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:53 +00:00
c5a4899c9f Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:53 +00:00
bbb6d4c35c Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:52 +00:00
0a9a96ae77 Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:51 +00:00
7a4b7d8e8f Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:50 +00:00
285e2e807a Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:50 +00:00
b3c96f6416 Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:49 +00:00
30f5ab37d2 Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:48 +00:00
de21d5cb55 Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:48 +00:00
d4dd28c402 Tower: upload at_master_order 18.0.10.0 (via marketplace) 2026-04-30 19:03:47 +00:00
45eaa252bd Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:50 +00:00
45222f6bb7 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:49 +00:00
d9715fff07 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:48 +00:00
5227767283 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:46 +00:00
f76d79a606 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:45 +00:00
14fd3e2f1e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:43 +00:00
e3ce2be1f4 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:40 +00:00
0329122548 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:39 +00:00
f31284a113 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:38 +00:00
dcccf8b034 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:37 +00:00
9751faf173 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:36 +00:00
0d94303107 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:35 +00:00
fcd0cfb26f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:34 +00:00
48863fc6d5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:33 +00:00
8ba35217bf Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:32 +00:00
afe168723d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:30 +00:00
72273b580f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:28 +00:00
095ed48ded Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:27 +00:00
3a18336cc4 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:26 +00:00
33c3ae3585 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:24 +00:00
aa7c94b0dc Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:22 +00:00
e30a37898b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:19 +00:00
88e1fa7965 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:19 +00:00
9f19b53414 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:18 +00:00
26fa9e2871 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:16 +00:00
825423b3af Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:14 +00:00
06a1e15a4f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:12 +00:00
23c6cf64cf Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:11 +00:00
ae9617dedd Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:10 +00:00
2884546072 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:09 +00:00
d5ae939266 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:08 +00:00
89047fe79c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:06 +00:00
d323267a28 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:05 +00:00
edf915943b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:04 +00:00
d7c9aee436 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:03 +00:00
e497c2b5d0 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:38:00 +00:00
582b9e1b9b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:59 +00:00
2b1c7afdf1 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:57 +00:00
882ba9247a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:55 +00:00
0671d0b2e9 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:55 +00:00
509006cce4 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:53 +00:00
44c55e900a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:51 +00:00
db83188b88 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:50 +00:00
aa4d44164c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:49 +00:00
ea9d3ff61f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:48 +00:00
f63dd818aa Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:47 +00:00
27f8b2541e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:46 +00:00
558a815535 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:44 +00:00
6d6add697b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:42 +00:00
bb235faf39 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:40 +00:00
3951faca64 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:39 +00:00
3db314cedb Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:36 +00:00
3e4faf0dc7 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:34 +00:00
a70964ad21 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:31 +00:00
d0a79ac5a6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:28 +00:00
04c6e4d523 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:27 +00:00
3d2cbfcb74 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:26 +00:00
320f8e5bbf Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:24 +00:00
4d495e5cc5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:21 +00:00
dbb7322fce Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:19 +00:00
ad78baae06 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:17 +00:00
b3f9c9f989 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:16 +00:00
773476029f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:14 +00:00
bdf313b39e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:13 +00:00
0b3fafc478 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:11 +00:00
ebe63f69ab Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:08 +00:00
a59bf97a36 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:05 +00:00
a64c72a5d4 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:04 +00:00
8b0f9d6361 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:02 +00:00
aa5439325d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:37:00 +00:00
b1dbab9942 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:59 +00:00
43ae1490f5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:57 +00:00
2a07cab00b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:56 +00:00
08fe99a3dd Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:55 +00:00
1dae9452af Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:54 +00:00
0c15c42d4f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:54 +00:00
7a0ffe51cd Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:53 +00:00
effc02f01b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:51 +00:00
e585583bf5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:49 +00:00
eab73a9964 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:47 +00:00
fe62898189 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:47 +00:00
dd454b6269 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:46 +00:00
3a469c333d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:44 +00:00
3fe3d5944a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:43 +00:00
42ecceac62 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:42 +00:00
f5a379f683 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:40 +00:00
834b292f73 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:39 +00:00
d29c58ac6c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:39 +00:00
1095317b23 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:38 +00:00
80ff9d095b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:36 +00:00
e110187874 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:35 +00:00
0786f81d63 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:34 +00:00
dcb6ca59fc Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:32 +00:00
3eb6ee6758 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:30 +00:00
97753affb8 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:29 +00:00
e19d374f2b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:28 +00:00
30a33e939f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:26 +00:00
d2e38977c2 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:25 +00:00
fa3cb047c8 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:24 +00:00
8441d1956d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:23 +00:00
633253de00 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:21 +00:00
9dfa1d4d03 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:20 +00:00
4350577ab7 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:20 +00:00
63becde73e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:19 +00:00
65dc01b15e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:17 +00:00
d0e0bb7b0c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:16 +00:00
26ce32efb8 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:14 +00:00
aa9004b2ef Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:12 +00:00
cd8b9c7975 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:11 +00:00
500026c640 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:10 +00:00
bd2ee6d072 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:09 +00:00
da58849aae Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:07 +00:00
570140673c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:05 +00:00
0e56aa652c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:04 +00:00
f3b0ba4632 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:36:02 +00:00
2462220921 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:59 +00:00
17efcd0fbc Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:57 +00:00
5d3db75b14 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:55 +00:00
ea03f85024 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:53 +00:00
be600989ba Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:51 +00:00
362fe7126a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:49 +00:00
439ebf4356 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:47 +00:00
9b50ec37a6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:45 +00:00
829a0fe36f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:44 +00:00
6fc36fd5c5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:44 +00:00
cbc31eaf55 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:43 +00:00
d63a402aaf Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:42 +00:00
31047a2670 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:41 +00:00
24adc03ab3 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:39 +00:00
4e051d6c52 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:38 +00:00
2ebb0a73c2 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:36 +00:00
906671a8b5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:35 +00:00
a401dc3abd Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:33 +00:00
124377f7c5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:31 +00:00
90836e2f2a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:31 +00:00
90d9a2b202 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:30 +00:00
c62d637f56 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:28 +00:00
ce90945990 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:25 +00:00
bb42154f35 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:24 +00:00
3fd4eb215e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:23 +00:00
543f423825 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:22 +00:00
3d2ad55cfa Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:20 +00:00
50cf4b4107 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:17 +00:00
8b6528cb8b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:14 +00:00
a55dafa6e0 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:12 +00:00
24c89d97e5 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:11 +00:00
a7c36be076 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:10 +00:00
2ef98a3897 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:09 +00:00
e21d4e66a9 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:07 +00:00
38c91ef94c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:05 +00:00
454e6936fb Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:03 +00:00
6d49162c09 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:35:00 +00:00
5bd5c7b906 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:59 +00:00
a4de8697da Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:58 +00:00
ce8f7f8711 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:55 +00:00
ca0ca65f14 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:54 +00:00
30f2cf9b0e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:54 +00:00
f68e2d23e7 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:53 +00:00
05c33d06c3 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:52 +00:00
46fca55d81 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:50 +00:00
4db469f273 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:49 +00:00
72a652a5c6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:48 +00:00
5523995594 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:46 +00:00
7b7bcf73e6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:45 +00:00
acb3564750 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:43 +00:00
bb3359a309 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:40 +00:00
8b1544647a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:38 +00:00
f727f3ab67 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:38 +00:00
a371ac9bdc Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:36 +00:00
f84c891e96 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:35 +00:00
c31947bfab Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:33 +00:00
d783d0d08d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:31 +00:00
7a9e3c56fe Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:30 +00:00
11770d4950 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:29 +00:00
0d150fa92e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:28 +00:00
2933122464 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:26 +00:00
5ab0886edf Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:25 +00:00
e2045b21e2 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:24 +00:00
596bd0a761 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:22 +00:00
2a19267d2f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:19 +00:00
50105c5ba6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:16 +00:00
434b1d46ab Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:15 +00:00
d4945d20ee Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:13 +00:00
6e05ee4c57 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:12 +00:00
33ba2de5dd Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:11 +00:00
bb075fe036 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:09 +00:00
60687c611a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:08 +00:00
776bcc1f7f Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:07 +00:00
61061bdee9 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:06 +00:00
77d467de70 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:04 +00:00
e4a63ec0e4 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:04 +00:00
77dc1762c0 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:03 +00:00
e2bf6c22f9 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:34:01 +00:00
f9b4e1582a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:59 +00:00
53a05d2240 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:58 +00:00
8512a1d196 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:57 +00:00
41a130c1d6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:55 +00:00
13b8322e3c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:54 +00:00
d18265433c Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:53 +00:00
16e661f8e8 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:51 +00:00
8a40edf6c6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:49 +00:00
831cbe1449 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:48 +00:00
aea1722933 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:48 +00:00
51a39b2e6e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:46 +00:00
21e6a6f3f6 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:43 +00:00
dbca7e34e1 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:42 +00:00
ac8400512b Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:41 +00:00
817b05fe06 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:40 +00:00
cb1c9a1ffa Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:39 +00:00
bc7dbb494a Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:38 +00:00
7827e2f224 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:36 +00:00
c4edb6b3af Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:35 +00:00
bdc66120f3 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:34 +00:00
636938581d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:33 +00:00
2c3cfc3978 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:32 +00:00
0236ef0809 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:31 +00:00
de9fd7b71d Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:29 +00:00
eaeb978c19 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:27 +00:00
2e55efb312 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:26 +00:00
b786492d5e Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:25 +00:00
6086717e66 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:24 +00:00
375466beea Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:23 +00:00
8dfe9e9874 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:21 +00:00
385ef060ed Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:20 +00:00
6c8b3cbd74 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:18 +00:00
90d1e47e03 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:17 +00:00
9700c192d0 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:16 +00:00
25596b4149 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:14 +00:00
0885b59bb3 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:14 +00:00
fcbe9a17db Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:12 +00:00
860fedb7e0 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:10 +00:00
85ada8e9b7 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:08 +00:00
6b98bf8be1 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:07 +00:00
55fb716490 Tower: upload at_accounting 18.0.1.7 (via marketplace) 2026-04-28 07:33:06 +00:00
Tower Deploy
ab214ac9dd Wipe addons/: full reset for clean re-upload 2026-04-27 11:20:55 +03:00
Tower Deploy
304da43eb8 Wipe test artifacts from 18.0 branch (test_correct_encode + web_responsive*); keep odoosky_demo 2026-04-27 10:59:55 +03:00
48b0b7a283 Tower: upload test_correct_encode 18.0.1.0.0 (via marketplace) 2026-04-27 07:38:22 +00:00
40c3e1d471 Tower: upload test_correct_encode 18.0.1.0.0 (via marketplace) 2026-04-27 07:38:22 +00:00
c1ecf1289d Tower: upload web_responsive 18.0.1.0.0 (via marketplace) 2026-04-27 06:42:13 +00:00
0741834b31 Tower: upload web_responsive 18.0.1.0.0 (via marketplace) 2026-04-27 06:42:13 +00:00
7a2debb3d7 Tower: upload web_responsive_test 18.0.0.1.0 (via marketplace) 2026-04-27 06:03:21 +00:00
6ef9d029eb Tower: upload web_responsive_test 18.0.0.1.0 (via marketplace) 2026-04-27 06:03:21 +00:00
Tower Deploy
96edc0c694 Seed addons/odoosky_demo (18.0.1.0.0) — platform smoke-test addon 2026-04-27 00:44:36 +03:00
778 changed files with 101762 additions and 0 deletions

393
.gitea/qualify-addon.py Normal file
View 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))

View 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

View 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.

View File

@@ -0,0 +1,4 @@
# Copyright 2025 Cetmix OÜ
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
from . import models

View 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,
}

View File

@@ -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 ""

View 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

View 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)

View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View 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).

View 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],
)
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View 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">[&quot;form&quot;, &quot;kanban&quot;]</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">&quot;sales_team.group_sale_salesman&quot;</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">&quot;res.users&quot;</span><span class="p">]</span><span class="o">.</span><span class="n">search</span><span class="p">([(</span><span class="s2">&quot;groups_id&quot;</span><span class="p">,</span> <span class="s2">&quot;in&quot;</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">&quot;crm.lead&quot;</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">&quot;kanban&quot;</span><span class="p">,</span> <span class="s2">&quot;form&quot;</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>

View File

@@ -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;
},
});

View File

@@ -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});
},
});

View File

@@ -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});
},
});

View File

@@ -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);
}

View File

@@ -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"
);
});
});

View 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

View File

@@ -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},
)

View 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()

View 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',
}

View File

@@ -0,0 +1,2 @@
from . import ks_date_filter_selections
from . import filter_tools

View 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)

View File

@@ -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)

View 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

View 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()

View 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()

View 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)

View 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()

View 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>

View 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","&gt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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","&lt;",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>

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View 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:")

View 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

View 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

View 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

View 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"))

View 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

View 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)

View 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 {}

View 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!'),
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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

View 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)

View 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)))

View 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})

View 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)

View 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"))

View 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

View 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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ks_dashboard_ninja_board ks_dashboard_ninja.board model_ks_dashboard_ninja_board base.group_user 1 1 1 1
3 access_ks_dashboard_ninja_kpi_mail ks_dashboard_ninja.kpi_mail model_ks_dashboard_ninja_kpi_mail base.group_user 1 1 1 1
4 access_ks_dashboard_ninja_item ks_dashboard_ninja.item model_ks_dashboard_ninja_item base.group_user 1 1 1 1
5 access_ks_to_do_headers ks_to.do.headers model_ks_to_do_headers base.group_user 1 1 1 1
6 access_ks_to_do_description ks_to.do.description model_ks_to_do_description base.group_user 1 1 1 1
7 access_ks_dashboard_ninja_child_board ks_dashboard_ninja.child_board model_ks_dashboard_ninja_child_board base.group_user 1 1 1 1
8 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
9 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
10 access_ks_dashboard_ninja_board_template ks_dashboard_ninja.board_template model_ks_dashboard_ninja_board_template base.group_user 1 1 1 1
11 access_ks_dashboard_ninja_item_goal ks_dashboard_ninja_item_goal model_ks_dashboard_ninja_item_goal base.group_user 1 1 1 1
12 access_ks_dashboard_ninja_item_action ks_dashboard_ninja_item_action model_ks_dashboard_ninja_item_action base.group_user 1 1 1 1
13 access_ks_dashboard_item_multiplier ks_dashboard_item.multiplier model_ks_dashboard_item_multiplier base.group_user 1 1 1 1
14 access_ks_ninja_dashboard_item_action ks_ninja_dashboard.item_action model_ks_ninja_dashboard_item_action base.group_user 1 1 1 0
15 access_ks_dashboard_group_by ks.dashboard.group.by model_ks_dashboard_group_by base.group_user 1 1 1 1
16 access_ks_dashboard_csv_group_by ks.dashboard.csv.group.by model_ks_dashboard_csv_group_by base.group_user 1 1 1 1
17 access_ks_dashboard_new ks.dashboard.new model_ks_dashboard_new base.group_user 1 1 1 1
18 access_ks_dashboard_csv_new ks.dashboard.csv.new model_ks_dashboard_csv_new base.group_user 1 1 1 1
19 access_ks_dashboard_ninja_import ks_dashboard_ninja.import model_ks_dashboard_ninja_import base.group_system 1 1 1 0
20 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
21 access_ir_actions_act_window ir.actions.act_window base.model_ir_actions_act_window base.group_user 1 0 0 0
22 access_ir_actions_client ir.actions.client base.model_ir_actions_client base.group_user 1 0 0 0
23 access_ir_ui_menu ir.ui.menu base.model_ir_ui_menu base.group_user 1 1 0 0
24 access_ir_model_group_user ir.model base.model_ir_model base.group_user 1 0 0 0
25 access_ir_model_fields_group_user ir.model.fields base.model_ir_model_fields base.group_user 1 0 0 0
26 access_ir_model_ks_dashboard_wizard ks_dashboard_wizard model_ks_dashboard_wizard base.group_user 1 1 1 1
27 access_ir_model_ks_duplicate_dashboard_wizard ks_duplicate_dashboard__wizard model_ks_dashboard_duplicate_wizard base.group_user 1 1 1 1
28 access_ir_model_ks_delete_dashboard_wizard ks_delete_dashboard__wizard model_ks_dashboard_delete_wizard base.group_user 1 1 1 1
29 access_ks_dashboard_ninja_arti_int ks_dashboard_ninja.arti_int model_ks_dashboard_ninja_arti_int base.group_user 1 1 1 1
30 access_ks_dashboard_ninja_ai_dashboard ks_dashboard_ninja.ai_dashboard model_ks_dashboard_ninja_ai_dashboard base.group_user 1 1 1 1
31 access_ks_dashboard_ninja_fetch_key ks_dashboard_ninja.fetch_key model_ks_dashboard_ninja_fetch_key base.group_user 1 1 1 1
32 access_ks_dashboard_ninja_favourite_filters ks_dashboard_ninja.favourite_filters model_ks_dashboard_ninja_favourite_filters 1 1 1 1

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

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