Compare commits

...

54 Commits
10.0 ... 16.0

Author SHA1 Message Date
Fabien BOURGEOIS a70b630c71 [ADD]Yaltik DSL : add HTML tags 2024-05-02 16:54:29 +02:00
Fabien BOURGEOIS 7d874226be [ADD]Yaltik DSL : add template tag 2024-04-16 11:08:44 +02:00
Fabien BOURGEOIS b4c272a3da [ADD]Yaltik DSL : Label Tag 2024-02-01 15:55:52 +01:00
Fabien BOURGEOIS bc19f41588 [ADD]Yaltik DSL : add ul / li tags 2024-01-29 15:14:09 +01:00
Fabien BOURGEOIS 48f5e75d8d [ADD]Title HTML balisis 2024-01-16 17:34:07 +01:00
Fabien BOURGEOIS e043acfca4 [ADD]BR Tag 2024-01-12 12:35:59 +01:00
Fabien BOURGEOIS 908ed5f723 [IMP]Yaltik DSL : add span tag 2024-01-12 12:29:48 +01:00
Fabien BOURGEOIS f6982b7d16 [IMP]Yaltik DSL v16 : act_window deprecated 2023-06-13 18:43:17 +02:00
Fabien BOURGEOIS e678fac46e [ADD]Yaltik DSL : add basic tags 2023-05-24 10:34:43 +02:00
Fabien BOURGEOIS 1d10e5a89a [IMP]Yaltik Bootstrap : replace MUK by OCA web addons 2023-03-06 18:19:21 +01:00
Fabien BOURGEOIS dbcfdc14e5 [MIG]Yaltik Bootstrap to v16 2023-01-24 14:23:50 +01:00
Fabien BOURGEOIS 85e8663218 [ADD]Yaltik DSL : add notebook/page tags 2023-01-10 14:32:25 +01:00
Fabien BOURGEOIS 747afe2f14 [IMP]Yaltik DSL : add delete fn 2022-12-15 16:50:51 +01:00
Fabien BOURGEOIS 1296e952af [ADD]Yaltik DSL : tests for yes_no filter 2022-12-09 11:56:38 +01:00
Fabien BOURGEOIS f1261c1085 [REM]Yaltik DSL : remove last coco files 2022-12-09 11:36:50 +01:00
Fabien BOURGEOIS cf6f40220a [MIG]Yaltik DSL : from coconut to pure py2 2022-12-09 11:24:16 +01:00
Fabien BOURGEOIS ae30f47886 [ADD]Yaltik DSL : add filter_yes_no 2022-11-25 22:59:04 +01:00
Fabien BOURGEOIS 1a4bed211a [ADD]Yaltik DSL : add separator (source) 2022-11-20 23:41:04 +01:00
Fabien BOURGEOIS 6db81ee501 [ADD]Yaltik DSL : add separator 2022-11-20 23:39:58 +01:00
Fabien BOURGEOIS 214503ae4f [MIG]Yaltik DSL : base conversion to Coconut 2022-11-17 15:49:41 +01:00
Fabien BOURGEOIS 40f6c88fee [FIX]Hy Odoo : from Hy v0.25.0 dfor adapted 2022-11-09 09:35:49 +01:00
Fabien BOURGEOIS b75fded6f5 [UPD]Hy Odoo : update dependencies 2022-11-09 09:01:09 +01:00
Fabien BOURGEOIS b08752d767 [ADD]Hy Odoo : from external to Odoo addon 2022-11-06 15:25:57 +01:00
Fabien BOURGEOIS 6b49ee69f4 [IMP]Yaltik DSL : suffix better than xml type 2022-11-02 10:39:12 +01:00
Fabien BOURGEOIS cc40759909 [IMP]Yaltik DSL : odoo add field_nval 2022-11-02 10:37:39 +01:00
Fabien BOURGEOIS c2ad72a3a6 [IMP]Yaltik DSL : allow other suffix for XML 2022-11-02 10:25:01 +01:00
Fabien BOURGEOIS 0cc980b7a9 [MIG]Yaltik DSL to 16.0 2022-11-02 09:59:30 +01:00
Fabien BOURGEOIS b6f62571b2 Revert "[MIG]v15 : manifest migrations"
This reverts commit 265453589b.
2022-04-18 08:41:46 +02:00
Fabien BOURGEOIS f5d90d83cf [FIX]DBFilter From Config : even on conf, do not cache other domains
Because user can come with its own selected first.
2022-04-18 08:40:36 +02:00
Fabien BOURGEOIS 8767b1aaeb [FIX]DBFilter From Config : do not cache outside of explicit dbfilter 2022-04-18 08:40:23 +02:00
Fabien BOURGEOIS 401b3b5ffc [FIX]DBFilter from config : when httprequest is none 2022-04-18 08:40:03 +02:00
Fabien BOURGEOIS d7aeafdc6f [FIX]DBFilter from config : fix http_host when module not used 2022-04-18 08:39:41 +02:00
Fabien BOURGEOIS 265453589b [MIG]v15 : manifest migrations 2022-04-18 08:38:15 +02:00
Fabien BOURGEOIS 2e501018d8 [IMP]DBFilter from config : allow empty then creation
Without reboot requirement (because of cache).
2022-04-16 15:28:10 +02:00
Fabien BOURGEOIS 0d40afd498 [ADD]DBFilter From Config module 2022-01-31 11:28:44 +01:00
Fabien BOURGEOIS 360832d876 [FIX]Yaltik DSL : remove useless external dependency 2021-02-22 19:29:34 +01:00
Fabien BOURGEOIS 264d1e37b2 [ADD]Yaltik DSL v12 2021-02-22 18:08:11 +01:00
Fabien BOURGEOIS c338738e2b [REM]Hy base removal (for external python package) 2020-03-27 00:34:20 +01:00
Fabien BOURGEOIS 4f6399c576 [IMP]Hy Base enhancements
* New ox-view-new shortcut to quickly create new records and views ;
* New ox-sheet XML field ;
* Improrve ox-field with mangling name attribute ;
* Idem for compute-fn macro for dependencies.
2020-03-26 22:32:16 +01:00
Fabien BOURGEOIS ebef43ad8a [ADD]Hy Base : new macros
* For computed fields ;
* For migrations.
2019-10-23 05:29:17 +02:00
Fabien BOURGEOIS d0a7b6a74e [IMP]Hy base : add Odoo debug logger 2019-09-22 15:29:29 +02:00
Fabien BOURGEOIS f6b892efd4 [IMP]Hy base : allow attrs as child as 2nd arg for xmln 2019-09-22 15:10:06 +02:00
Fabien BOURGEOIS f2c8818e43 [REF]Hy base : finalize fns over macros 2019-09-22 10:41:27 +02:00
Fabien BOURGEOIS aa7bfe9449 [REF]Hy Base XML better naming for child 2019-09-22 10:28:56 +02:00
Fabien BOURGEOIS 291d4fc394 [UPD]XML DSL v12 2019-09-22 10:13:48 +02:00
Fabien BOURGEOIS 095f4ce7d9 [IMP]Hy Base : favor defns instead of macros here 2019-09-22 10:12:41 +02:00
Fabien BOURGEOIS dd846361cf [IMP]Hy Base : new odoo macros 2019-09-18 21:10:00 +02:00
Fabien BOURGEOIS d46a51a330 [FIX]Hy Base : handle python2 case 2019-09-18 09:26:07 +02:00
Fabien BOURGEOIS 2a1a765222 [FIX]Hy base : actions-server code fix 2019-09-18 09:25:30 +02:00
Fabien BOURGEOIS 3b3846526e [IMP]Hy Base : new macro for server actions 2019-09-17 16:39:58 +02:00
Fabien BOURGEOIS b7c203ac0d [ADD]Hy base v12 2019-09-17 16:39:34 +02:00
Fabien BOURGEOIS 88e66e0fde [IMP]Yaltik Bootstrap : base debranding too 2018-12-14 08:26:17 +01:00
Fabien BOURGEOIS 08d143a5f6 [ADD]Yaltik bootstrap on 12.0 2018-12-13 19:11:19 +01:00
Fabien BOURGEOIS 4ca4d5169c [MIG]Make all addons as non-installable on 12 2018-12-13 19:04:02 +01:00
46 changed files with 4678 additions and 21 deletions

View File

@ -22,12 +22,12 @@
* For CSV published by Crédit Coopératif on their website customer space ;
* Adds option on bank import statement wizard ;
* Checks and processes file.""",
'version': '10.0.0.1.3',
'version': '12.0.0.1.3',
'category': 'Banking addons',
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'application': False,
'installable': True,
'installable': False,
'depends': ['account_bank_statement_import'],
'data': ['wizard/account_bank_statement_import_views.xml']
}

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Copyright 2022 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from . import override

View File

@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Copyright 2022 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
{
'name': 'DBFilter from configuration',
'summary': 'Database filtering from configuration',
'description': """ Database filtering from configuration :
- use Odoo own filtering mechanism fist ;
- then apply configuration : `domains_to_databases` : string in form of `database:domain1,domain2|database2:domain3`.
Needs to be added to `server_wide_modules` on configuration or `--load`,
for example `server_wide_modules = base,web,dbfilter_from_config`.
Also required : `proxy_mode` to `True`
Inspired from OCA `dbfilter_from_header`.""",
'version': '12.0.0.0.6',
'category': 'Yaltik',
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'application': False,
'installable': True,
'depends': ['base']
}

View File

@ -0,0 +1,64 @@
# -*- coding: utf-8 -*-
# Copyright 2022 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Override of databse filtering to allow new features """
from re import match
from json import loads
from odoo import http
from odoo.tools import config
db_filter_odoo = http.db_filter
# Cache usage
db_filter_dict = {}
all_hosts = []
db_filter_str = config.get('database_from_hosts', False)
if db_filter_str:
for entry in db_filter_str.split('|'):
database, hosts = entry.split(':')
hosts = hosts.split(',')
db_filter_dict[database] = hosts
all_hosts += hosts
http_host_resolv = {}
def db_filter(dbs, httprequest=None):
""" Override db_filter """
if db_filter_str:
httprequest = httprequest or http.request.httprequest
http_host = httprequest.environ.get('HTTP_HOST', '').split(':')[0]
# If in cache and not empty
if http_host in http_host_resolv and http_host_resolv.get(http_host):
dbs = http_host_resolv[http_host]
elif http_host in all_hosts:
for database, hosts in db_filter_dict.items():
for host in hosts:
if host == http_host:
dbs = [i for i in dbs if match(database, i)]
break
if http_host not in http_host_resolv:
http_host_resolv[http_host] = dbs
else:
dbs = db_filter_odoo(dbs, httprequest)
else:
dbs = db_filter_odoo(dbs, httprequest)
return dbs
if (config.get('proxy_mode')
and 'dbfilter_from_config' in config.get('server_wide_modules')):
http.db_filter = db_filter

View File

@ -20,12 +20,12 @@
'name': 'Grants follow-ups',
'summary': 'Grants follow-ups ',
'description': 'Grants follow-ups ',
'version': '10.0.1.0.0',
'version': '12.0.1.0.0',
'category': 'Membership',
'author': 'Fabien Bourgeois, Youssef ELOUAHBY',
'license': 'AGPL-3',
'application': True,
'installable': True,
'installable': False,
'depends': ['mail', 'document'],
'data': ['security/security.xml',
'security/ir.model.access.csv',

8
hy_odoo/Makefile Normal file
View File

@ -0,0 +1,8 @@
.PHONY: clean
clean:
find . -name '*.pyc' -delete
find . -name '__pycache__' -delete
.PHONY: test
test:
/opt/odoo/OCB/odoo-bin --test-tags hyodoo --workers 0 -d test -i hy_odoo --stop-after-init

7
hy_odoo/__init__.py Normal file
View File

@ -0,0 +1,7 @@
# Copyright 2020-2022 Fabien Bourgeois <fabien@yaltik.com>
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
import hy

19
hy_odoo/__manifest__.py Normal file
View File

@ -0,0 +1,19 @@
# Copyright 2019-2022 Fabien Bourgeois <fabien@yaltik.com>
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
{
'name': 'Hy Odoo',
'summary': 'Hy Odoo',
'description': """ Hy functions and macros for Odoo """,
'version': '16.0.0.8.9',
'category': 'Hy',
'author': 'Fabien Bourgeois',
'license': 'Other OSI approved licence',
'application': False,
'installable': True,
'depends': ['base'],
"external_dependencies": {'python' : ['hy==0.25.*', 'hyrule==0.2.*']}
}

22
hy_odoo/mgeneral.hy Normal file
View File

@ -0,0 +1,22 @@
;; Copyright 2021-2022 Fabien Bourgeois <fabien@yaltik.com>
;;
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
" Hy General Macros "
(defmacro instance? [type record] `(isinstance ~record ~type))
(defmacro hydict [dic]
"Generate dict with mangled keys, from HyDict list"
(setv mangled-dic
(list
(map
(fn [pair]
(if (even? (nth pair 0))
(hy.mangle (nth pair 1))
(nth pair 1)))
(enumerate dic))))
`{~@mangled-dic})

46
hy_odoo/modoo.hy Normal file
View File

@ -0,0 +1,46 @@
;; Copyright 2019-2022 Fabien Bourgeois <fabien@yaltik.com>
;;
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
" Odoo related macros "
(defmacro o-cmod [model-name] `(. cls env [~model-name]))
(defmacro o-mod [model-name] `(. self env [~model-name]))
(defmacro o-cref [ref-name] `((. cls env ref) ~ref-name))
(defmacro o-ref [ref-name] `((. self env ref) ~ref-name))
(defmacro hydm [hy-domain]
"Generate Odoo domain from Hy like tuple domain"
(setv op (second hy-domain)
field (hy.mangle (get hy-domain 2))
value (get hy-domain 3))
`#(~field ~op ~value))
; Odoo ORM macros
#_(defmacro/g! compute-fn [field dependencies body]
"Macro to make computed definition smoother"
(setv fname f"_compute_{(hy.mangle field)}" descr f"Computes {field}"
dependencies (list (map hy.mangle dependencies)))
(import hy.models [HySymbol]
`(defn [(.depends api ~@dependencies)] ~(HySymbol fname) [self]
~descr
~body)))
(defmacro compute-field [fname body]
"Takes fname Symbol and body to create computed field"
(setv fn-name f"_compute_{(hy.mangle fname)}")
`(setv ~fname (~@body :compute ~fn-name)))
; Backend macros
(defmacro __ [sentence] `((py "_") ~sentence))
(defmacro logger []
`(do
(import logging)
(setv _logger (.getLogger logging --name--))))
(defmacro pdb [] `(do (import [pdb [set-trace]]) (set-trace)))

35
hy_odoo/mtest.hy Normal file
View File

@ -0,0 +1,35 @@
;; Copyright 2021-2022 Fabien Bourgeois <fabien@yaltik.com>
;;
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
" Hy Odoo Tests Helpers and Macros "
(defmacro o-assert-equal [left right] `(.assertEqual self ~left ~right))
(defmacro o-assert-list-equal [left right] `(.assertListEqual self ~left ~right))
(defmacro o-assert-dict-equal [left right] `(.assertDictEqual self ~left ~right))
(defmacro o-assert-not-equal [left right] `(.assertNotEqual self ~left ~right))
(defmacro o-assert-true [val] `(.assertTrue self ~val))
(defmacro o-assert-false [val] `(.assertFalse self ~val))
(defmacro o-assert-is [left right] `(.assertIs self ~left ~right))
(defmacro o-assert-is-not [left right] `(.assertIsNot self ~left ~right))
(defmacro o-assert-is-none [val] `(.assertIsNone self ~val))
(defmacro o-assert-is-not-none [val] `(.assertIsNotNone self ~val))
(defmacro o-assert-in [left right] `(.assertIn self ~left ~right))
(defmacro o-assert-not-in [left right] `(.assertNotIn self ~left ~right))
(defmacro o-assert-is-instance [left right] `(.assertIsInstance self ~left ~right))
(defmacro o-assert-not-is-instance [left right] `(.assertNotIsInstance self ~left ~right))
(defmacro o-assert-raises [Error] `(.assertRaises self ~Error))
(defmacro o-assert-raises-regex [Error regexp] `(.assertRaisesRegex self ~Error ~regexp))
(defmacro odo-assert-raises [Error body]
"Macro to test Error with self.assertRaises and do block"
`(with [err (.assertRaises self ~Error)] ~body))
(defmacro o-test [name body]
"Macro for unit test case"
(setv fn-name (% "test-%s" (str name))
humanized-name (.replace (str name) "-" " ")
descr (bytes (% "Test %s" humanized-name)))
`(defn ~(HySymbol fn-name) [self] ~descr ~body))

175
hy_odoo/odoo.hy Normal file
View File

@ -0,0 +1,175 @@
;; Copyright 2019-2022 Fabien Bourgeois <fabien@yaltik.com>
;;
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
" Odoo macros and helpers "
(require hyrule.collections [assoc]
odoo.addons.hy-odoo.mgeneral [instance?])
(import os [path]
odoo.addons.hy-odoo.xml [xmlroot xmln])
; Global helpers
(defn strdm [hy-domain]
"Generate Odoo domain from Hy like tuple domain"
(do
(setv #(op field value) hy-domain
field (hy.mangle field)
value (when (string? value) f"'{value}'" value))
(return f"('{field}', '{op}', {value})")))
; XML helpers functions and macros
(defn odoo [children] (xmlroot {"tag" "odoo" "attrs" {} "children" children}))
(defn data [#* args]
"Special data node, allow optional args on data tag"
(when (= (len args) 1) (do (setv args (list args))
(.insert args 0 {})))
(xmln "data" #* args))
; Aliases
(defn function [#* args] (xmln "function" #*args))
(defn record [#* args] (xmln "record" #*args))
(defn form [#* args] (xmln "form" #*args))
(defn tree [#* args] (xmln "tree" #*args))
(defn search [#* args] (xmln "search" #*args))
; Actions
(defn act-window [#* args] (xmln "act_window" #*args))
(defn act-window-model [model attrs]
" Build new act_window from model and args"
(setv model-und (.replace model "." "_")
model-cap (.join " " (lfor w (.split model ".") (.capitalize w)))
xmlid f"{model-und}_view_action"
name f"{model-cap} Action"
cloned-attrs (.copy attrs)) ; Avoid side-effect
(.update cloned-attrs {"id" xmlid "name" name "res_model" model})
(act-window cloned-attrs))
(defn actions-server-code [xmlid name modelref code]
"Server actions of type code"
(record {"id" xmlid "model" "ir.actions.server"}
[(field-name name)
(field {"name" "model_id" "ref" modelref} [])
(field {"name" "state"} ["code"])
(field {"name" "code"} [code])]))
(defn client-action-multi [xmlid name model action]
"Client action multi (ir.values), with own xmlid, name, targeted model and
action"
(setv action f"'ir.actions.server,%d'%{action}")
(record {"id" xmlid "model" "ir.values"}
[(field-name name)
(field {"name" "key2" "eval" "'client_action_multi'"} [])
(field {"name" "model" "eval" (% "'%s'" model)} [])
(field {"name" "value" "eval" action})]))
; Menus
(defn menuitem [#* args] (xmln "menuitem" #*args))
(defn menuitem-model [model attrs]
" Build new menuitem from model and attrs"
(setv model-und (.replace model "." "_")
actionid f"{model-und}_view_action"
xmlid f"{model-und}_menu"
cloned-attrs (.copy attrs))
(.update cloned-attrs {"id" xmlid "action" actionid})
(menuitem cloned-attrs))
; Form aliases
(defn group [#* args] (xmln "group" #*args))
(defn header [#* args] (xmln "header" #*args))
(defn footer [#* args] (xmln "footer" #*args))
(defn sheet [#* args] (xmln "sheet" #*args))
(defn button [#* args] (xmln "button" #*args))
(defn p [#* args] (xmln "p" #*args))
(defn xpath [#* args] (xmln "xpath" #*args))
(defn attribute [name value] (xmln "attribute" {"name" name} [value]))
; Fields
(defn field [#* args]
"Special field allowing mangling name attribute"
(setv attrs (get args 0))
(when (and (instance? dict attrs) (in "name" attrs))
(assoc attrs "name" (hy.mangle (get attrs "name")))
(setv args (list args))
(assoc args 0 attrs)
(setv args (tuple args)))
(xmln "field" #*args))
(defn field-name [name] (field {"name" "name"} [name]))
(defn field-nval [name value] (field {"name" name} [value]))
(defn field-model [model] (field {"name" "model"} [model]))
(defn field-inherit [xmlid] (field {"name" "inherit_id" "ref" xmlid} []))
(defn field-arch [#* args] (field {"name" "arch" "type" "xml"} #*args))
; Search
(defn filter [#* args] (xmln "filter" #*args))
; Views
(defn view [xmlid children] (record {"id" xmlid "model" "ir.ui.view"} children))
(defn view-def [xmlid name model arch]
"View and first fields simplification with record xmlid, name, targeted model"
(view xmlid [(field-name name) (field-model model) (field-arch arch)]))
(defn view-new[view_type model arch]
"View : new view definition, based on type (form, tree, ...) and model ID"
(setv model-und (.replace model "." "_")
model-cap (.join " " (lfor w (.split model ".") (.capitalize w)))
xmlid f"{model-und}_view_{view_type}"
name f"{model-cap} {(.capitalize view_type)}")
(view-def :xmlid xmlid :name name :model model :arch arch))
(defn view-inherit [filename model inherit arch]
"Inherited View simplification with name of the record, xmlid for model and
inherited view"
(setv module (get (.split filename ".") 2)
inherited (get (.split inherit ".") 1)
xmlid f"{inherited}_inherit_{module}"
model-cap (.join " " (lfor w (.split model ".") (.capitalize w)))
name f"{model-cap} Adaptations")
(view xmlid [(field-name name)
(field-model model)
(field-inherit inherit)
(field-arch arch)]))
; Hy-related helpers
(defn modulehytopy [module-path hy-files]
"Transforms hy to py for translation purpose"
(import astor)
(import os.path [dirname]
io [open :as iopen]
hy.lex [hy-parse]
hy.compiler [hy-compile]
hy.errors [filtered-hy-exceptions])
(defn hytopy [source path]
"Hy source to Py source"
(setv hst (with [(filtered-hy-exceptions)] (hy-parse source :filename path))
-ast (with [(filtered-hy-exceptions)] (hy-compile hst "__main__" :filename path :source source)))
(.to-source (. astor code-gen) -ast))
(for [hy-file hy-files]
(setv hy-path (% "%s/%s.hy" #((dirname module-path) hy-file))
hy-source (with [o (iopen hy-path "r" :encoding "utf-8")] (.read o))
output-path (.replace hy-path ".hy" ".py")
content ["# Generate from Hy AST, for Babel translation purpose only."
"# For real source code, please see and use HY source."
(hytopy hy-source hy-path)])
(setv output-py (.join "\n" content))
(with [f (iopen output-path "w")] (.write f output-py))))
; Migrations
(defn generate-fn-name [filepath]
"Generate function name from filepath"
(setv version (.replace (get (.split (.dirname path filepath) "/") -1) "." "_")
pre-post (get (.split (.basename path filepath) "-") 0))
f"{pre-post}_{version}")

10
hy_odoo/tests/__init__.py Normal file
View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright 2022 Fabien Bourgeois <fabien@yaltik.com>
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
import hy
from . import test_xml, test_odoo

284
hy_odoo/tests/test_odoo.hy Normal file
View File

@ -0,0 +1,284 @@
;; Copyright 2022 Fabien Bourgeois <fabien@yaltik.com>
;;
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
" Odoo Helpers tests "
(require
hyrule.control [defmain]
odoo.addons.hy-odoo.mtest *)
(import unittest
functools [partial]
xml.etree.ElementTree :as ET
odoo.tests [tagged]
odoo.tests.common [TransactionCase]
odoo.addons.hy-odoo.xml [XMLDictElement]
odoo.addons.hy-odoo.odoo :as od)
(defclass [(tagged "hyodoo" "hyodoo.odoo")] TextOdooBase [TransactionCase]
"Odoo Helpers tests"
(defn test-odoo [self]
" Test odoo function "
(setv element (od.odoo []))
(o-assert-is-instance element (. ET Element))
(o-assert-equal (. element tag) "odoo"))
(defn test-data [self]
" Test data function "
(setv element (od.data []))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal element.tag "data")
(o-assert-equal element.attrs {})
(setv element (od.data {"one" "attribute"} []))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal element.tag "data")
(o-assert-equal element.attrs {"one" "attribute"}))
(defn test-aliases [self]
" Test simple aliases to xmln "
(setv element (od.record {"one" "attribute"} "A child"))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal element.tag "record")
(o-assert-equal element.attrs {"one" "attribute"})
(o-assert-equal element.children ["A child"])
(setv element (od.tree))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal element.tag "tree"))
(defn test-act-window-model [self]
" Test act_window function "
(setv element (od.act_window_model "sample.model" {"view_type" "form"}))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal element.tag "act_window")
(o-assert-equal (get (. element attrs) "view_type") "form")
(o-assert-equal (get (. element attrs) "id") "sample_model_view_action")
(o-assert-equal (get (. element attrs) "res_model") "sample.model")
(o-assert-equal (get (. element attrs) "name") "Sample Model Action"))
(defn test-menunitem-model [self]
" Test menuitem function "
(setv element (od.menuitem-model "sample.model" {"groups" "base.user_employee"}))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal (. element tag) "menuitem")
(o-assert-equal (get (. element attrs) "groups") "base.user_employee")
(o-assert-equal (get (. element attrs) "id") "sample_model_menu")
(o-assert-equal (get (. element attrs) "action") "sample_model_view_action"))
(defn test-attribute [self]
" Test attribute function "
(setv element (od.attribute "invisible" "1"))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal (. element tag) "attribute")
(o-assert-equal (get (. element attrs) "name") "invisible")
(o-assert-equal (. element children) ["1"]))
(defn test-fields [self]
" Test fields function "
(setv element (od.field {"one" "attribute"} "A child"))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal element.tag "field")
(setv element (od.field-name "A name"))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal (. element tag) "field")
(o-assert-equal (get (. element attrs) "name") "name")
(o-assert-equal (. element children) ["A name"])
(setv element (od.field-model "sample.model"))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal (. element tag) "field")
(o-assert-equal (get (. element attrs) "name") "model")
(o-assert-equal (. element children) ["sample.model"])
(setv element (od.field-inherit "module.xml_view"))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal (. element tag) "field")
(o-assert-equal (get (. element attrs) "name") "inherit_id")
(o-assert-equal (get (. element attrs) "ref") "module.xml_view")
(o-assert-false (. element children))
(setv element (od.field-arch))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal (. element tag) "field")
(o-assert-equal (get (. element attrs) "name") "arch")
(o-assert-equal (get (. element attrs) "type") "xml")
(o-assert-false (. element children)))
(defn test-view [self]
" Test view function "
(setv element (od.view "view_xmlid" []))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal (. element tag) "record")
(o-assert-equal (get (. element attrs) "id") "view_xmlid")
(o-assert-equal (get (. element attrs) "model") "ir.ui.view")
(o-assert-false (. element children)))
(defn test-view-def [self]
" Test view def function "
(setv element (od.view_def "view_xmlid" "View" "sample.model" []))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal (. element tag) "record")
(o-assert-equal (get (. element attrs) "id") "view_xmlid")
(o-assert-equal (get (. element attrs) "model") "ir.ui.view")
(o-assert-equal (len (. element children)) 3)
(setv child (get (. element children) 0))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "name")
(o-assert-equal (. child children) ["View"])
(setv child (get (. element children) 1))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "model")
(o-assert-equal (. child children) ["sample.model"])
(setv child (get (. element children) 2))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "arch")
(o-assert-equal (get (. child attrs) "type") "xml")
(o-assert-false (. child children)))
(defn test-view-new [self]
" Test view-new function "
(setv element (od.view-new "tree" "sample.model" []))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal (. element.tag) "record")
(o-assert-equal (get (. element attrs) "id") "sample_model_view_tree")
(o-assert-equal (get (. element attrs) "model") "ir.ui.view")
(o-assert-equal (len (. element children)) 3)
(setv child (get (. element children) 0))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "name")
(o-assert-equal (. child children) ["Sample Model Tree"])
(setv child (get (. element children) 1))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "model")
(o-assert-equal (. child children) ["sample.model"])
(setv child (get (. element children) 2))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "arch")
(o-assert-equal (get (. child attrs) "type") "xml")
(o-assert-false (. child children)))
(defn test_view_inherit [self]
" Test view_inherit function "
(setv element (od.view-inherit "odoo.addons.module" "sample.model" "parent.view" []))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal (. element tag) "record")
(o-assert-equal (get (. element attrs) "id") "view_inherit_module")
(o-assert-equal (get (. element attrs) "model") "ir.ui.view")
(o-assert-equal (len (. element children)) 4)
(setv child (get (. element children) 0))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "name")
(o-assert-equal (. child children) ["Sample Model Adaptations"])
(setv child (get (. element children) 1))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "model")
(o-assert-equal (. child children) ["sample.model"])
(setv child (get (. element children) 2))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "inherit_id")
(o-assert-false (. child children))
(setv child (get (. element children) 3))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "arch")
(o-assert-equal (get (. child attrs) "type") "xml")
(o-assert-false (. child children)))
(defn test-actions-server-code [self]
" Test actions-server-code function "
(setv element (od.actions-server-code "sample.xmlid" "Code" "sample.model"
"record.do_something()"))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal (. element tag) "record")
(o-assert-equal (get (. element attrs) "id") "sample.xmlid")
(o-assert-equal (get (. element attrs) "model") "ir.actions.server")
(o-assert-equal (len (. element children)) 4)
(setv child (get (. element children) 0))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "name")
(o-assert-equal (. child children) ["Code"])
(setv child (get (. element children) 1))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "model_id")
(o-assert-equal (get (. child attrs) "ref") "sample.model")
(o-assert-false (. child children))
(setv child (get (. element children) 2))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "state")
(o-assert-equal (. child children) ["code"])
(setv child (get (. element children) 3))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "code")
(o-assert-equal (. child children) ["record.do_something()"]))
(defn test-client-action-multi [self]
" Test client_action_multi function "
(setv element (od.client-action-multi "sample.xmlid" "Multi" "sample.model" "sample.action"))
(o-assert-is-instance element XMLDictElement)
(o-assert-equal (. element tag) "record")
(o-assert-equal (get (. element attrs) "id") "sample.xmlid")
(o-assert-equal (get (. element attrs) "model") "ir.values")
(o-assert-equal (len (. element children)) 4)
(setv child (get (. element children) 0))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "name")
(o-assert-equal (. child children) ["Multi"])
(setv child (get (. element children) 1))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "key2")
(o-assert-equal (get (. child attrs) "eval") "'client_action_multi'")
(o-assert-false (. child children))
(setv child (get (. element children) 2))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "model")
(o-assert-equal (get (. child attrs) "eval") "'sample.model'")
(setv child (get (. element children) 3))
(o-assert-is-instance child XMLDictElement)
(o-assert-equal (. child tag) "field")
(o-assert-equal (get (. child attrs) "name") "value")
(o-assert-equal (get (. child attrs) "eval") "'ir.actions.server,%d'%sample.action")))
(defmain [] (.main unittest))

120
hy_odoo/tests/test_xml.hy Normal file
View File

@ -0,0 +1,120 @@
;; Copyright 2019-2022 Fabien Bourgeois <fabien@yaltik.com>
;;
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
" XML Helpers tests "
(require
hyrule.control [defmain]
odoo.addons.hy-odoo.mtest *)
(import unittest
functools [partial]
xml.etree.ElementTree :as ET
os [unlink]
odoo.tests [tagged]
odoo.tests.common [TransactionCase]
odoo.addons.hy-odoo.xml [XMLDictElement xmln xmlroot xmlchild xml-write])
(defclass [(tagged "hyodoo" "hyodoo.xml")] TextXMLBase [TransactionCase]
"XML Helpers tests"
(defn test-xmln [self]
"Text xmln"
; XMLDictElement
(o-assert-is-instance (xmln) XMLDictElement)
; Tags
(o-assert-dict-equal (._asdict (xmln)) {"tag" "" "attrs" {} "children" []})
(o-assert-equal (. (xmln "a tag") tag) "a tag")
; Attrs
(o-assert-dict-equal (. (xmln :attrs {"a good" "one"}) attrs) {"a good" "one"})
; Childrens
(o-assert-list-equal (. (xmln :children [1 2 3]) children) [1 2 3])
(o-assert-list-equal (. (xmln :children "Some text") children) ["Some text"])
(with [(o-assert-raises-regex TypeError "Invalid arguments")] (xmln :children False))
; Ensure that only children after tags is managed
(setv element (xmln "tag" {"something" "inside"}))
(o-assert-is-instance (. element attrs) dict)
(o-assert-is-instance (. element children) list)
(o-assert-is-instance element XMLDictElement)
(setv element (xmln "tag" ["something" "inside"]))
(o-assert-is-instance (. element attrs) dict)
(o-assert-is-instance (. element children) list)
(o-assert-is-instance element XMLDictElement))
(defn test-xmlchild [self]
"Test xmlchild"
(setv parent (xmlroot {"tag" "root" "attrs" {} "children" []})
xmlc-par (partial xmlchild parent))
; Bad arguments
(with [(o-assert-raises-regex TypeError "Invalid arguments for xmlchild")] (xmlc-par False))
; Need XMLDictElement, not dict
(with [(o-assert-raises-regex TypeError "Invalid arguments for xmlchild")]
(xmlc-par [{"tag" "t" "attrs" {"a" "b"} "children" []}]))
(xmlc-par ["some text"])
(o-assert-equal (. parent text) "some text")
(xmlc-par[(xmln "t" {"a" "b"} [])])
(setv child (next (.iter parent "t")))
(o-assert-equal (. child tag) "t")
(o-assert-dict-equal (. child attrib) {"a" "b"})
(o-assert-list-equal (list child) [])
(xmlc-par [(xmln "t2" {1 2} [])])
(setv child (next (.iter parent "t2")))
(o-assert-dict-equal (. child attrib) {"1" "2"})
(xmlc-par [(xmln "tchildren" {} [(xmln "subchild" {} [])])])
(setv child (next (.iter parent "tchildren"))
subchildren (list child))
(o-assert-equal (len subchildren) 1)
(o-assert-equal (. (get subchildren 0) tag) "subchild"))
(defn test-xmlroot [self]
"Test xmlroot"
(setv root (xmlroot {"tag" "root" "attrs" {} "children" []}))
(o-assert-is-instance root (. ET Element))
(with [(o-assert-raises-regex TypeError "is not subscriptable")] (xmlroot False))
(with [(o-assert-raises-regex KeyError "tag")] (xmlroot {}))
(with [(o-assert-raises-regex KeyError "attrs")] (xmlroot {"tag" "root"})))
(defn test-xml-write [self]
"Test xml-write"
(setv children [(xmln "child1" {"attr" "value"} [])
(xmln "child2" {} "Some text")]
tree (xmlroot {"tag" "root" "attrs" {} "children" children})
xmlw (fn [p] (xml_write p tree)))
(o-assert-is-none (xmlw "/badpath"))
(o-assert-is-none (xmlw "/bad.ext"))
(xmlw __file__)
(setv filepath (.replace __file__ ".hy" "_views.xml"))
(with [output-file (open filepath "r")]
(setv output-xml (.read output-file))
(o-assert-in "<?xml version" output-xml)
(o-assert-in "<root>" output-xml)
(o-assert-in "<child1 attr=\"value\"/>" output-xml)
(o-assert-in "<child2>Some text</child2>" output-xml)
(unlink filepath))
(xml-write __file__ tree :pretty True :suffix "_data")
(setv filepath (.replace __file__ ".hy" "_data.xml"))
(with [output-file (open filepath "r")]
(setv output-xml (.read output-file))
(o-assert-in "<?xml version" output-xml)
(o-assert-in "<root>" output-xml)
(o-assert-in "<child1 attr=\"value\"/>" output-xml)
(o-assert-in "<child2>Some text</child2>" output-xml)
(unlink filepath))))
(defmain [] (.main unittest))

24
hy_odoo/utils.hy Normal file
View File

@ -0,0 +1,24 @@
;; Copyright 2021-2022 Fabien Bourgeois <fabien@yaltik.com>
;;
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
" Hy General Utils "
(require hyrule.collections [assoc])
(defn pick [keys record]
(reduce
(fn [acc key]
(do
(assoc acc key (get record key))
(return acc)))
keys
{}))
(defn pick-values [keys record]
(list
(map
(fn [key] (get record key))
keys)))

64
hy_odoo/xml.hy Normal file
View File

@ -0,0 +1,64 @@
;; Copyright 2019-2022 Fabien Bourgeois <fabien@yaltik.com>
;;
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at https://mozilla.org/MPL/2.0/.
" XML helpers and macros "
(require odoo.addons.hy-odoo.mgeneral [instance?]
odoo.addons.hy-odoo.modoo [pdb])
(import collections [namedtuple]
functools [partial]
os [path]
xml.dom [minidom]
xml.etree.ElementTree :as ET)
;; Types
(setv XMLDictElement (namedtuple "XMLDictElement" ["tag" "attrs" "children"]))
;; Helpers
(defn xmlroot [tree]
"Special process for root XML Node"
(setv rootel (.Element ET (get tree "tag") (get tree "attrs")))
(when (in "children" tree)
(xmlchild rootel (get tree "children")))
(return rootel))
(defn xmlchild [parent children]
"Handling of children (ie non root) XML Nodes with/o text and subchildren
(recursive)"
(cond (instance? str children) (setv (. parent text) children)
(instance? XMLDictElement children)
(do
(setv attrs (dfor [k v] (.items (. children attrs)) (str k) (str v)))
(setv new-parent (.SubElement ET parent (. children tag) attrs)
subchildren (. children children))
(when subchildren) (xmlchild new-parent subchildren))
(instance? list children) (list( map (partial xmlchild parent) children))
True (raise (TypeError "Invalid arguments for xmlchild"))))
(defn xmln [[tag ""] [attrs {}] [children []]]
"XMLDictElement building from dict object, with defaults"
(when (instance? list attrs) (setv children attrs attrs {}))
(setv xmldictel (partial XMLDictElement tag attrs)
inst-str? (instance? str children))
(when inst-str? (return (xmldictel [children])))
(when (instance? list children) (return (xmldictel children)))
(raise (TypeError "Invalid arguments for xmln")))
(defn xml-write [filepath tree [pretty True] [suffix "_views"]]
"Write XML file according to filepath and given tree"
(when (.endswith filepath ".hy")
(setv output-xml (.decode (.tostring ET
tree
:xml_declaration True
:encoding "utf-8") "utf-8")
output-path (.split (.abspath path filepath) "/")
(get output-path -1) (.replace (get output-path -1) ".hy" (% "%s.xml" suffix))
output-path (.join "/" output-path))
(when pretty
(setv output-xml (.toprettyxml (.parseString minidom output-xml) :indent " ")))
(with [output-file (open output-path "w")] (.write output-file output-xml))))

View File

@ -24,7 +24,7 @@
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'application': False,
'installable': True,
'installable': False,
'depends': ['note'],
'data': ['views/note_view.xml']
}

View File

@ -30,7 +30,7 @@
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'application': False,
'installable': True,
'installable': False,
'depends': ['note_base'],
'data': ['views/note_dates.xml']
}

View File

@ -24,7 +24,7 @@
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'application': False,
'installable': True,
'installable': False,
'depends': ['note_base'],
'data': ['views/note_priority.xml']
}

View File

@ -23,7 +23,7 @@
'category': 'Tools',
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'installable': True,
'installable': False,
'depends': ['note'],
'data': ['note_tags_view.xml']
}

View File

@ -23,7 +23,7 @@
'category': 'Tools',
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'installable': True,
'installable': False,
'depends': ['note_base', 'project'],
'data': ['note_task_view.xml']
}

View File

@ -34,7 +34,7 @@
<t t-esc="o.name">
</t>
""",
'installable': True,
'installable': False,
'application': True,
'data':[
'view/templates.xml',

View File

@ -18,11 +18,11 @@
{
'name': 'Email widget check syntax',
'summary': 'Extends Web Email widget to ensure valid syntax',
'version': '10.0.1.0.1',
'version': '12.0.1.0.1',
'category': 'Usability',
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'installable': True,
'installable': False,
'depends': [ 'web' ],
'data': [ 'view.xml' ]
}

View File

@ -18,11 +18,11 @@
{
'name': 'French format phone widget check',
'summary': 'Extends Web Char widget to handle french format phone',
'version': '10.0.1.0.2',
'version': '12.0.1.0.2',
'category': 'Usability',
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'installable': True,
'installable': False,
'depends': [ 'web' ],
'data': [ 'view.xml' ],
'qweb': ['static/src/xml/qweb.xml']

View File

@ -18,11 +18,11 @@
{
'name': 'URL widget check syntax',
'summary': 'Extends Web URL widget to ensure valid syntax',
'version': '10.0.1.0.1',
'version': '12.0.1.0.1',
'category': 'Usability',
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'installable': True,
'installable': False,
'depends': [ 'web' ],
'data': [ 'view.xml' ]
}

18
xml_dsl/__init__.py Normal file
View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from . import odoo

29
xml_dsl/__manifest__.py Normal file
View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Copyright 2019 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
{
'name': 'Odoo XML DSL base module and fns',
'summary': 'Odoo XML Domain Specific Language base module and functions',
'description': """ Odoo XML Domain Specific Language base module and functions """,
'version': '12.0.0.1.0',
'category': 'Yaltik',
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'application': False,
'installable': True,
'depends': ['base']
}

46
xml_dsl/base.py Normal file
View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
#
# Copyright 2019 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" XML helpers and macros """
from xml.etree import ElementTree as ET
def xmlroot(tree):
""" Special process for root XML Node """
rootel = ET.Element(tree['tag'], tree['attrs'])
children = tree['children']
if children:
xmlchild(rootel, children)
return rootel
def xmlchild(parent, children):
""" Handling of children (ie non root) XML Nodes with/o text and
subchildren (recursive) """
for child in children:
if isinstance(child, str):
parent.text = child
else:
attrs = {unicode(k): unicode(v) for k, v in child['attrs'].items()}
new_parent = ET.SubElement(parent, child['tag'], attrs)
subchildren = child['children']
if subchildren:
xmlchild(new_parent, subchildren)
def xmln(tag='', attrs=None, children=None, text=False):
""" XMLNode with default children, not attributes """
children = ([text] if text else children) or []
return {'tag': tag, 'attrs': attrs or {}, 'children': children}

81
xml_dsl/odoo.py Normal file
View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
#
# Copyright 2019 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Odoo XML DSL """
from . import base as x
def odoo(*args):
""" odoo tag shortcut """
return x.xmlroot(x.xmln('odoo', {}, *args))
def data(*args):
""" data tag shortcut """
return x.xmln('data', {}, *args)
def record(*args):
""" record tag shortcut """
return x.xmln('record', *args)
def view(xmlid, children):
""" view tag shortcut """
return record({'id': xmlid, 'model': 'ir.ui.view'}, children)
def view_def(xmlid, name, model, arch):
""" View and first fields simplification with record xmlid, name, targeted model """
return view(xmlid, [field_name(name), field_model(model), field_arch(arch)])
def view_inherit(name, model, inherit, arch):
""" Inherited View simplification with name of the record, xmlid for model
and inherited view """
module = __name__.split('.')[2]
inherited = inherit.split('.')[1]
xmlid = '%s_inherit_%s' % (inherited, module)
return view(xmlid, [field_name(name), field_model(model),
field_inherit(inherit), field_arch(arch)])
def actions_server_code(xmlid, name, modelref, code):
""" Server actions of type code """
return record({'id': xmlid, 'model': 'ir.actions.server'},
[field_name(name),
field({'name': 'model_id', 'ref': modelref}),
field({'name': 'state'}, ['code']),
field({'name': 'code'}, [code])])
def field(*args):
""" field tag shortcut """
return x.xmln('field', *args)
def field_name(name):
""" field name tag shortcut """
return field({'name': 'name'}, [name])
def field_model(model):
""" field model tag shortcut """
return field({'name': 'model'}, [model])
def field_inherit(xmlid):
""" field inherit tag shortcut """
return field({'name': 'inherit_id', 'ref': xmlid})
def field_arch(*args):
""" field arch tag shortcut """
return field({'name': 'arch', 'type': 'xml'}, *args)
def xml_write(mpath, filename, tree):
""" Write XML file according to filename and given tree """
import os.path
from xml.etree import ElementTree as ET
output_xml = ET.tostring(tree).decode('utf-8')
output_path = os.path.dirname(os.path.abspath(mpath))
fpath = u'%s/%s' % (output_path, filename)
with open(fpath, 'w') as xml_file:
xml_file.write(output_xml)

View File

@ -25,7 +25,7 @@
'description': """ Yaltik Backend Green theme, based upon great
DodgerBlue Backend Theme from OpenWorx :
https://github.com/Openworx/themes/tree/8.0/dodgerblue_backend_theme """,
'installable': True,
'installable': False,
'depends': ['base'],
'data': ['views.xml']
}

View File

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright 2018 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Copyright 2018-2023 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
{
'name': 'Yaltik bootstrap',
'summary': 'Yaltik bootstrap : common dependencies for all projects',
'description': """ Yaltik bootstrap : common dependencies for all projects :
* addons from OCA : FR localization, debranding ;
* new backend and responsive theme """,
'version': '16.0.0.1.1',
'category': 'Yaltik',
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'application': False,
'installable': True,
'depends': ['l10n_fr', 'l10n_fr_siret', 'l10n_fr_state', 'l10n_fr_department',
'web_theme_classic', 'web_responsive']
}

View File

@ -18,11 +18,11 @@
{
'name': 'CRM Actions',
'summary': 'Action management, instead of new activity, in CRM',
'version': '10.0.1.3.5',
'version': '12.0.1.3.5',
'category': 'Sales',
'author': 'Fabien BOURGEOIS - Yaltik',
'license': 'AGPL-3',
'installable': True,
'installable': False,
'application': True,
'depends': ['crm', 'calendar'],
'data': ['views/crm_action_views.xml',

31
yaltik_dsl/Makefile Normal file
View File

@ -0,0 +1,31 @@
.PHONY: setup
setup:
pip install --user -U coconut[watch,jobs,backports,mypy]
.PHONY: testdev
testdev: clean build test
.PHONY: testprod
testprod: clean prod test
.PHONY: clean
clean:
find . -name '*.pyc' -delete
find . -name '__pycache__' -delete
.PHONY: test
test:
python3 tests/test_xml_base.py
python3 tests/test_odoo.py
.PHONY: build
build:
coconut -t sys -j sys .
.PHONY: prod
prod:
coconut --notco -t 3.9 -j sys -f .
.PHONY: watch
watch:
coconut --strict -t sys -j sys -w .

18
yaltik_dsl/__init__.py Normal file
View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Copyright 2019-2020 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from . import src

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Copyright 2019-2024 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
{
'name': 'Yaltik Odoo DSL base module and fns',
'summary': 'Yaltik Odoo Domain Specific Language base module and functions',
'description': """ Yaltik Odoo Domain Specific Language base module and functions """,
'version': '16.0.0.5.13',
'category': 'Yaltik',
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'application': False,
'installable': True,
'depends': ['base']
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
# Copyright 2020 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from . import odoo_dsl

167
yaltik_dsl/src/odoo_dsl.py Normal file
View File

@ -0,0 +1,167 @@
# Copyright 2019-2024 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Odoo XML DSL """
from .xml_base import xmlroot, xmln
# XML helpers functions and macros
# Odoo root XML Node
odoo = lambda children: xmlroot({'tag': 'odoo', 'attrs': {}, 'children': children})
# Special data Node
def data(*args):
""" Allow optional args on data tag """
if len(args) == 1:
args = list(args)
args.insert(0, {})
return xmln('data', *args)
# Aliases
function = lambda *args: xmln('function', *args)
record = lambda *args: xmln('record', *args)
delete = lambda *args: xmln('delete', *args)
form = lambda *args: xmln('form', *args)
tree = lambda *args: xmln('tree', *args)
search = lambda *args: xmln('search', *args)
template = lambda *args: xmln('template', *args)
# Actions
def act_window_model(model, attrs):
""" Build new act_window from model and args """
xmlid = '%s_view_action' % (model.replace('.', '_'))
name = '%s Action' % ' '.join(map(lambda w: w.capitalize(), model.split('.')))
attrs_clone = attrs.copy() # Avoid side-effect
attrs_clone.update(
{'name': name, 'res_model': model, 'type': 'ir.actions.act_window'}
)
children = []
for key, val in attrs_clone.items():
if key.endswith('_id'):
children.append(field({'name': key, 'ref': val}))
elif key == 'sequence':
children.append(field({'name': key, 'eval': val}))
else:
children.append(field({'name': key}, [val]))
return record({'id': xmlid, 'model': 'ir.actions.act_window'}, children)
def action_server_code(xmlid, name, modelref, code):
""" Server actions of type code """
children = [field_name(name),
field({'name': 'model_id', 'ref': modelref}, []),
field({'name': 'state'}, ['code']),
field({'name': 'code'}, [code])]
return record({'id': xmlid, 'model': 'ir.actions.server'}, children)
def client_action_multi(xmlid, name, model, action):
""" Client action multi (ir.values), with own xmlid, name, targeted model
and action """
action = "'ir.actions.server,%d'%{}".format(action)
children = [field_name(name),
field({'name': 'key2', 'eval': "'client_action_multi'"}),
field({'name': 'model', 'eval': "'%s'" % model}),
field({'name': 'value', 'eval': action})]
return record({'id': xmlid, 'model': 'ir.values'}, children)
# Menus
menuitem = lambda *args: xmln('menuitem', *args)
def menuitem_model(model, attrs):
""" Build new menuitem from model and attrs """
model_und = model.replace('.', '_')
xmlid = '%s_menu' % model_und
actionid = '%s_view_action' % model_und
attrs_clone = attrs.copy() # Avoid side-effect
attrs_clone.update({'id': xmlid, 'action': actionid})
return menuitem(attrs_clone)
# Form aliases
group = lambda *args: xmln('group', *args)
header = lambda *args: xmln('header', *args)
footer = lambda *args: xmln('footer', *args)
sheet = lambda *args: xmln('sheet', *args)
button = lambda *args: xmln('button', *args)
p = lambda *args: xmln('p', *args)
i = lambda *args: xmln('i', *args)
t = lambda *args: xmln('t', *args)
div = lambda *args: xmln('div', *args)
span = lambda *args: xmln('span', *args)
br = lambda *args: xmln('br', *args)
h1 = lambda *args: xmln('h1', *args)
h2 = lambda *args: xmln('h1', *args)
h3 = lambda *args: xmln('h1', *args)
h4 = lambda *args: xmln('h1', *args)
h5 = lambda *args: xmln('h1', *args)
ul = lambda *args: xmln('ul', *args)
li = lambda *args: xmln('li', *args)
label = lambda *args: xmln('label', *args)
strong = lambda *args: xmln('strong', *args)
small = lambda *args: xmln('small', *args)
table = lambda *args: xmln('table', *args)
tr = lambda *args: xmln('tr', *args)
td = lambda *args: xmln('td', *args)
notebook = lambda *args: xmln('notebook', *args)
page = lambda *args: xmln('page', *args)
xpath = lambda *args: xmln('xpath', *args)
attribute = lambda name, value: xmln('attribute', {'name': name}, [value])
# Fields
field = lambda *args: xmln('field', *args)
field_name = lambda name: field({'name': 'name'}, [name])
field_nval = lambda name, value: field({'name': name}, [value])
field_model = lambda model: field({'name': 'model'}, [model])
field_inherit = lambda xmlid: field({'name': 'inherit_id', 'ref': xmlid}, [])
field_arch = lambda *args: field({'name': 'arch', 'type': 'xml'}, *args)
# Search
filter = lambda *args: xmln('filter', *args)
separator = lambda *args: xmln('separator', *args)
def filter_yes_no(field, str_yes=False, str_no=False):
""" Double filter for boolean : True and False """
res = []
if str_yes:
res.append(filter({'name': f'{field}_yes', 'string': str_yes,
'domain': f"[('{field}', '=', True)]"}))
if str_no:
res.append(filter({'name': f'{field}_no', 'string': str_no,
'domain': f"[('{field}', '=', False)]"}))
return res
# Views
view = lambda xmlid, children: record({'id': xmlid, 'model': 'ir.ui.view'}, children)
def view_def(xmlid, name, model, arch):
""" Shortcut for new view """
return view(xmlid, [field_name(name), field_model(model), field_arch(arch)])
def view_new(view_type, model, arch):
""" View : new view definition, based on type (form, tree, ...) and model ID """
model_und = model.replace('.', '_')
model_cap = ' '.join(map(lambda w: w.capitalize(), model.split('.')))
xmlid = "%s_view_%s" % (model_und, view_type)
name = ' '.join([model_cap, view_type.capitalize()])
return view_def(xmlid, name, model, arch)
def view_inherit(filename, model, inherit, arch):
""" Inherited View simplification with name of the record, xmlid for model
and inherited view """
module = filename.split('.')[2]
inherited = inherit.split('.')[1]
xmlid = '%s_inherit_%s' % (inherited, module)
model_cap = ' '.join(map(lambda w: w.capitalize(), model.split('.')))
name = '%s Adaptations' % model_cap
return view(xmlid, [field_name(name), field_model(model),
field_inherit(inherit), field_arch(arch)])

View File

@ -0,0 +1,73 @@
# Copyright 2019-2020 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" XML helpers and macros """
from os import path
import xml.etree.ElementTree as ET
from xml.dom import minidom
from collections import namedtuple
from functools import partial
XMLDictElement = namedtuple('XMLDictElement', ['tag', 'attrs', 'children'])
def xmlroot(tree):
""" Special process for root XML Node """
rootel = ET.Element(tree['tag'], tree['attrs'])
if 'children' in tree:
xmlchild(rootel, tree['children'])
return rootel
def xmlchild(parent, children):
""" Handling of children (ie non root) XML Nodes with/o text and
subchildren (recursive) """
if isinstance(children, str):
parent.text = children
elif isinstance(children, XMLDictElement):
attrs = {str(k): str(v) for [k, v] in children.attrs.items()}
new_parent = ET.SubElement(parent, children.tag, attrs)
subchildren = children.children
if subchildren:
xmlchild(new_parent, subchildren)
elif isinstance(children, list):
list(map(partial(xmlchild, parent), children))
else:
raise TypeError('Invalid arguments for xmlchild')
def xmln(tag='', attrs={}, children=[]):
""" XMLDictElement building from dict object, with defaults """
if isinstance(attrs, list):
children = attrs
attrs = {}
xmldictel = partial(XMLDictElement, tag, attrs)
if isinstance(children, str):
return xmldictel([children,])
if isinstance(children, list):
return xmldictel(children)
raise TypeError('Invalid arguments for xmln')
def xml_write(filepath, tree, pretty=True, suffix='_views'):
""" Write XML file according to filename and given tree """
if filepath.endswith('.py'): # if .pyc, no need to generate XML
output_xml = ET.tostring(tree)
if pretty:
output_xml = minidom.parseString(output_xml).toprettyxml(indent=' ')
output_path = path.abspath(filepath).split('/')
output_path[-1] = output_path[-1].replace('.py', '%s.xml' % suffix)
output_path = '/'.join(output_path)
with open(output_path, 'w') as output_file:
output_file.write(output_xml)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,271 @@
# Copyright 2020 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" Odoo Helpers tests """
import unittest
import xml.etree.ElementTree as ET
from src.xml_base import XMLDictElement
import src.odoo_dsl as od
class TestOdooBase(unittest.TestCase):
""" Odoo Helpers tests """
def test_odoo(self):
""" Test odoo function """
element = od.odoo([])
self.assertIsInstance(element, ET.Element)
self.assertEqual(element.tag, 'odoo')
def test_data(self):
""" Test data function """
element = od.data([])
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'data')
self.assertEqual(element.attrs, {})
element = od.data({"one": "attribute"}, [])
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'data')
self.assertEqual(element.attrs, {"one": "attribute"})
def test_aliases(self):
""" Test simple aliases to xmln """
element = od.record({"one": "attribute"}, 'A child')
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'record')
self.assertEqual(element.attrs, {"one": "attribute"})
self.assertEqual(element.children, ['A child'])
element = od.tree()
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'tree')
def test_act_window_model(self):
""" Test act_window function """
element = od.act_window_model('sample.model', {'view_type': 'form'})
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'act_window')
self.assertEqual(element.attrs['view_type'], 'form')
self.assertEqual(element.attrs['id'], 'sample_model_view_action')
self.assertEqual(element.attrs['res_model'], 'sample.model')
self.assertEqual(element.attrs['name'], 'Sample Model Action')
def test_menunitem_model(self):
""" Test menuitem function """
element = od.menuitem_model('sample.model', {'groups': 'base.user_employee'})
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'menuitem')
self.assertEqual(element.attrs['groups'], 'base.user_employee')
self.assertEqual(element.attrs['id'], 'sample_model_menu')
self.assertEqual(element.attrs['action'], 'sample_model_view_action')
def test_attribute(self):
""" Test attribute function """
element = od.attribute('invisible', "1")
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'attribute')
self.assertEqual(element.attrs['name'], 'invisible')
self.assertEqual(element.children, ["1"])
def test_fields(self):
""" Test fields function """
element = od.field({"one": "attribute"}, 'A child')
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'field')
element = od.field_name('A name')
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'field')
self.assertEqual(element.attrs['name'], 'name')
self.assertEqual(element.children, ['A name'])
element = od.field_model('sample.model')
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'field')
self.assertEqual(element.attrs['name'], 'model')
self.assertEqual(element.children, ['sample.model'])
element = od.field_inherit('module.xml_view')
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'field')
self.assertEqual(element.attrs['name'], 'inherit_id')
self.assertEqual(element.attrs['ref'], 'module.xml_view')
self.assertFalse(element.children)
element = od.field_arch()
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'field')
self.assertEqual(element.attrs['name'], 'arch')
self.assertEqual(element.attrs['type'], 'xml')
self.assertFalse(element.children)
def test_filter_yes_no(self):
""" Test Filter Yes No """
elements = od.filter_yes_no('some_field')
self.assertIsInstance(elements, list)
self.assertFalse(elements)
elements = od.filter_yes_no('some_field', 'Some field')
self.assertEqual(len(elements), 1)
self.assertIsInstance(elements[0], XMLDictElement)
self.assertEqual(elements[0].tag, 'filter')
self.assertEqual(elements[0].attrs['name'], 'some_field_yes')
self.assertEqual(elements[0].attrs['string'], 'Some field')
self.assertEqual(elements[0].attrs['domain'], "[('some_field', '=', True)]")
elements = od.filter_yes_no('some_field', 'Some field', 'Not some field')
self.assertEqual(len(elements), 2)
self.assertIsInstance(elements[0], XMLDictElement)
self.assertEqual(elements[0].tag, 'filter')
self.assertEqual(elements[0].attrs['name'], 'some_field_yes')
self.assertEqual(elements[0].attrs['string'], 'Some field')
self.assertEqual(elements[0].attrs['domain'], "[('some_field', '=', True)]")
self.assertIsInstance(elements[1], XMLDictElement)
self.assertEqual(elements[1].tag, 'filter')
self.assertEqual(elements[1].attrs['name'], 'some_field_no')
self.assertEqual(elements[1].attrs['string'], 'Not some field')
self.assertEqual(elements[1].attrs['domain'], "[('some_field', '=', False)]")
def test_view(self):
""" Test view function """
element = od.view('view_xmlid', [])
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'record')
self.assertEqual(element.attrs['id'], 'view_xmlid')
self.assertEqual(element.attrs['model'], 'ir.ui.view')
self.assertFalse(element.children)
def test_view_def(self):
""" Test view_def function """
element = od.view_def('view_xmlid', 'View', 'sample.model', [])
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'record')
self.assertEqual(element.attrs['id'], 'view_xmlid')
self.assertEqual(element.attrs['model'], 'ir.ui.view')
self.assertEqual((len(element.children)), 3)
self.assertIsInstance(element.children[0], XMLDictElement)
self.assertEqual(element.children[0].tag, 'field')
self.assertEqual(element.children[0].attrs['name'], 'name')
self.assertEqual(element.children[0].children, ['View'])
self.assertIsInstance(element.children[1], XMLDictElement)
self.assertEqual(element.children[1].tag, 'field')
self.assertEqual(element.children[1].attrs['name'], 'model')
self.assertEqual(element.children[1].children, ['sample.model'])
self.assertIsInstance(element.children[2], XMLDictElement)
self.assertEqual(element.children[2].tag, 'field')
self.assertEqual(element.children[2].attrs['name'], 'arch')
self.assertEqual(element.children[2].attrs['type'], 'xml')
self.assertFalse(element.children[2].children)
def test_view_new(self):
""" Test view_new function """
element = od.view_new('tree', 'sample.model', [])
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'record')
self.assertEqual(element.attrs['id'], 'sample_model_view_tree')
self.assertEqual(element.attrs['model'], 'ir.ui.view')
self.assertEqual((len(element.children)), 3)
self.assertIsInstance(element.children[0], XMLDictElement)
self.assertEqual(element.children[0].tag, 'field')
self.assertEqual(element.children[0].attrs['name'], 'name')
self.assertEqual(element.children[0].children, ['Sample Model Tree'])
self.assertIsInstance(element.children[1], XMLDictElement)
self.assertEqual(element.children[1].tag, 'field')
self.assertEqual(element.children[1].attrs['name'], 'model')
self.assertEqual(element.children[1].children, ['sample.model'])
self.assertIsInstance(element.children[2], XMLDictElement)
self.assertEqual(element.children[2].tag, 'field')
self.assertEqual(element.children[2].attrs['name'], 'arch')
self.assertEqual(element.children[2].attrs['type'], 'xml')
self.assertFalse(element.children[2].children)
def test_view_inherit(self):
""" Test view_inherit function """
element = od.view_inherit('odoo.addons.module', 'sample.model', 'parent.view', [])
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'record')
self.assertEqual(element.attrs['id'], 'view_inherit_module')
self.assertEqual(element.attrs['model'], 'ir.ui.view')
self.assertEqual((len(element.children)), 4)
self.assertIsInstance(element.children[0], XMLDictElement)
self.assertEqual(element.children[0].tag, 'field')
self.assertEqual(element.children[0].attrs['name'], 'name')
self.assertEqual(element.children[0].children, ['Sample Model Adaptations'])
self.assertIsInstance(element.children[1], XMLDictElement)
self.assertEqual(element.children[1].tag, 'field')
self.assertEqual(element.children[1].attrs['name'], 'model')
self.assertEqual(element.children[1].children, ['sample.model'])
self.assertIsInstance(element.children[2], XMLDictElement)
self.assertEqual(element.children[2].tag, 'field')
self.assertEqual(element.children[2].attrs['name'], 'inherit_id')
self.assertFalse(element.children[2].children)
self.assertIsInstance(element.children[3], XMLDictElement)
self.assertEqual(element.children[3].tag, 'field')
self.assertEqual(element.children[3].attrs['name'], 'arch')
self.assertEqual(element.children[3].attrs['type'], 'xml')
self.assertFalse(element.children[3].children)
def test_action_server_code(self):
""" Test action_server_code function """
element = od.action_server_code('sample.xmlid', 'Code', 'sample.model',
'''record.do_something()''')
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'record')
self.assertEqual(element.attrs['id'], 'sample.xmlid')
self.assertEqual(element.attrs['model'], 'ir.actions.server')
self.assertEqual((len(element.children)), 4)
self.assertIsInstance(element.children[0], XMLDictElement)
self.assertEqual(element.children[0].tag, 'field')
self.assertEqual(element.children[0].attrs['name'], 'name')
self.assertEqual(element.children[0].children, ['Code'])
self.assertIsInstance(element.children[1], XMLDictElement)
self.assertEqual(element.children[1].tag, 'field')
self.assertEqual(element.children[1].attrs['name'], 'model_id')
self.assertEqual(element.children[1].attrs['ref'], 'sample.model')
self.assertFalse(element.children[1].children)
self.assertEqual(element.children[2].tag, 'field')
self.assertEqual(element.children[2].attrs['name'], 'state')
self.assertEqual(element.children[2].children, ['code'])
self.assertEqual(element.children[3].tag, 'field')
self.assertEqual(element.children[3].attrs['name'], 'code')
self.assertEqual(element.children[3].children, ['record.do_something()'])
def test_client_action_multi(self):
""" Test client_action_multi function """
element = od.client_action_multi('sample.xmlid', 'Multi', 'sample.model', 'sample.action')
self.assertIsInstance(element, XMLDictElement)
self.assertEqual(element.tag, 'record')
self.assertEqual(element.attrs['id'], 'sample.xmlid')
self.assertEqual(element.attrs['model'], 'ir.values')
self.assertEqual((len(element.children)), 4)
self.assertIsInstance(element.children[0], XMLDictElement)
self.assertEqual(element.children[0].tag, 'field')
self.assertEqual(element.children[0].attrs['name'], 'name')
self.assertEqual(element.children[0].children, ['Multi'])
self.assertIsInstance(element.children[1], XMLDictElement)
self.assertEqual(element.children[1].tag, 'field')
self.assertEqual(element.children[1].attrs['name'], 'key2')
self.assertEqual(element.children[1].attrs['eval'], "'client_action_multi'")
self.assertFalse(element.children[1].children)
self.assertEqual(element.children[2].tag, 'field')
self.assertEqual(element.children[2].attrs['name'], 'model')
self.assertEqual(element.children[2].attrs['eval'], "'sample.model'")
self.assertEqual(element.children[3].tag, 'field')
self.assertEqual(element.children[3].attrs['name'], 'value')
self.assertEqual(element.children[3].attrs['eval'], "'ir.actions.server,%d'%sample.action")
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,127 @@
# Copyright 2020 Fabien Bourgeois <fabien@yaltik.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
""" XML Helpers tests """
from functools import partial
import unittest
import xml.etree.ElementTree as ET
from os import unlink
from src.xml_base import xmln, xmlroot, xmlchild, xml_write
class TestXMLBase(unittest.TestCase):
""" XML Helpers tests """
def test_xmln(self):
""" Text xmln """
# Tags
self.assertDictEqual(xmln()._asdict(), {'tag': '', 'attrs': {}, 'children': []})
self.assertEqual(xmln('a tag').tag, 'a tag')
# Attrs
self.assertDictEqual(xmln(attrs={'a good': 'one'}).attrs, {'a good': 'one'})
# Childrens
self.assertListEqual(xmln(children=[1, 2, 3]).children, [1, 2, 3])
self.assertListEqual(xmln(children='Some text').children, ['Some text'])
with self.assertRaisesRegex(TypeError, 'Invalid arguments'):
xmln(children=False)
# Ensure that only children after tags is managed
element = xmln('tag', {'something': 'inside'})
self.assertIsInstance(element.attrs, dict)
self.assertIsInstance(element.children, list)
element = xmln('tag', ['something', 'inside'])
self.assertIsInstance(element.attrs, dict)
self.assertIsInstance(element.children, list)
def test_xmlchild(self):
""" Test xmlchild """
parent = xmlroot({'tag': 'root', 'attrs': {}, 'children': []})
xmlc_par = partial(xmlchild, parent)
# Bad arguments
with self.assertRaisesRegex(TypeError, 'Invalid arguments for xmlchild'):
xmlc_par(False)
# Need XMLDictElement, not dict
with self.assertRaisesRegex(TypeError, 'Invalid arguments for xmlchild'):
xmlc_par([{'tag': 't', 'attrs': {'a': 'b'}, 'children': []},])
xmlc_par(['some text'])
self.assertEqual(parent.text, 'some text')
xmlc_par([xmln('t', {'a': 'b'}, []),])
child = parent.iter('t').__next__()
self.assertEqual(child.tag, 't')
self.assertDictEqual(child.attrib, {'a': 'b'})
self.assertListEqual(list(child), [])
xmlc_par([xmln('t2', {1: 2}, []),])
child = parent.iter('t2').__next__()
self.assertDictEqual(child.attrib, {'1': '2'})
xmlc_par([xmln('tchildren', {}, [xmln('subchild', {}, []),]),])
child = parent.iter('tchildren').__next__()
subchildren = list(child)
self.assertEqual(len(subchildren), 1)
self.assertEqual(subchildren[0].tag, 'subchild')
def test_xmlroot(self):
""" Test xmlroot """
root = xmlroot({'tag': 'root', 'attrs': {}, 'children': []})
self.assertIsInstance(root, ET.Element)
with self.assertRaisesRegex(TypeError, 'not subscriptable'):
xmlroot(False)
with self.assertRaisesRegex(KeyError, 'tag'):
xmlroot({})
with self.assertRaisesRegex(KeyError, 'attrs'):
xmlroot({'tag': 'root'})
def test_xml_write(self):
""" test xml_write """
children = [xmln('child1', {'attr': 'value'}, []),
xmln('child2', {}, "Some text")]
tree = xmlroot({'tag': 'root', 'attrs': {}, 'children': children})
xmlw = lambda p: xml_write(p, tree)
self.assertIsNone(xmlw('/badpath'))
self.assertIsNone(xmlw('/bad.ext'))
xmlw(__file__)
filepath = __file__.replace('.py', '_views.xml')
with open(filepath, 'r') as output_file:
output_xml = output_file.read()
self.assertIn('<?xml version', output_xml)
self.assertIn('<root>', output_xml)
self.assertIn('<child1 attr="value"/>', output_xml)
self.assertIn('<child2>Some text</child2>', output_xml)
unlink(filepath)
xml_write(__file__, tree, suffix='_data')
filepath = __file__.replace('.py', '_data.xml')
with open(filepath, 'r') as output_file:
output_xml = output_file.read()
self.assertIn('<?xml version', output_xml)
self.assertIn('<root>', output_xml)
self.assertIn('<child1 attr="value"/>', output_xml)
self.assertIn('<child2>Some text</child2>', output_xml)
unlink(filepath)
if __name__ == '__main__':
unittest.main()

View File

@ -25,7 +25,7 @@
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'application': False,
'installable': True,
'installable': False,
'depends': ['portal_project'],
'data': [
'security/ir.model.access.csv',

View File

@ -24,7 +24,7 @@
'author': 'Fabien Bourgeois',
'license': 'AGPL-3',
'application': False,
'installable': True,
'installable': False,
'depends': ['portal_project_issue'],
'data': [
'security/ir.model.access.csv',