Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

@ -0,0 +1,20 @@
import logging
import sys
import os
import odoo
from .command import Command, main
from . import cloc
from . import deploy
from . import scaffold
from . import server
from . import shell
from . import start
from . import populate
from . import tsconfig
from . import neutralize
from . import obfuscate
from . import genproxytoken
from . import db

View file

@ -0,0 +1,48 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import argparse
import os
import sys
import textwrap
from pathlib import Path
from odoo.tools import cloc, config
from . import Command
class Cloc(Command):
""" Count lines of code per modules """
def run(self, args):
parser = argparse.ArgumentParser(
prog=f'{Path(sys.argv[0]).name} {self.name}',
description="""\
Odoo cloc is a tool to count the number of relevant lines written in
Python, Javascript or XML. This can be used as rough metric for pricing
maintenance of customizations.
It has two modes of operation, either by providing a path:
odoo-bin cloc -p module_path
Or by providing the name of a database:
odoo-bin cloc --addons-path=dirs -d database
In the latter mode, only the custom code is accounted for.
""",
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument('--database', '-d', dest="database", help="Database name")
parser.add_argument('--path', '-p', action='append', help="File or directory path")
parser.add_argument('--verbose', '-v', action='count', default=0)
opt, unknown = parser.parse_known_args(args)
if not opt.database and not opt.path:
parser.print_help()
sys.exit()
c = cloc.Cloc()
if opt.database:
config.parse_config(['-d', opt.database] + unknown)
c.count_database(opt.database)
if opt.path:
for i in opt.path:
c.count_path(i)
c.report(opt.verbose)

View file

@ -0,0 +1,68 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import os
import sys
from pathlib import Path
import odoo
from odoo.modules import get_modules, get_module_path, initialize_sys_path
commands = {}
class Command:
name = None
def __init_subclass__(cls):
cls.name = cls.name or cls.__name__.lower()
commands[cls.name] = cls
ODOO_HELP = """\
Odoo CLI, use '{odoo_bin} --help' for regular server options.
Available commands:
{command_list}
Use '{odoo_bin} <command> --help' for individual command help."""
class Help(Command):
""" Display the list of available commands """
def run(self, args):
padding = max([len(cmd) for cmd in commands]) + 2
command_list = "\n ".join([
" {}{}".format(name.ljust(padding), (command.__doc__ or "").strip())
for name, command in sorted(commands.items())
])
print(ODOO_HELP.format( # pylint: disable=bad-builtin
odoo_bin=Path(sys.argv[0]).name,
command_list=command_list
))
def main():
args = sys.argv[1:]
# The only shared option is '--addons-path=' needed to discover additional
# commands from modules
if len(args) > 1 and args[0].startswith('--addons-path=') and not args[1].startswith("-"):
# parse only the addons-path, do not setup the logger...
odoo.tools.config._parse_config([args[0]])
args = args[1:]
# Default legacy command
command = "server"
# TODO: find a way to properly discover addons subcommands without importing the world
# Subcommand discovery
if len(args) and not args[0].startswith("-"):
logging.disable(logging.CRITICAL)
initialize_sys_path()
for module in get_modules():
if (Path(get_module_path(module)) / 'cli').is_dir():
__import__('odoo.addons.' + module)
logging.disable(logging.NOTSET)
command = args[0]
args = args[1:]
if command in commands:
o = commands[command]()
o.run(args)
else:
sys.exit('Unknown command %r' % (command,))

View file

@ -0,0 +1,174 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import argparse
import io
import urllib.parse
import sys
import zipfile
from functools import partial
from pathlib import Path
import requests
from . import Command
from .server import report_configuration
from ..service.db import dump_db, exp_drop, exp_db_exist, exp_duplicate_database, exp_rename, restore_db
from ..tools import config
eprint = partial(print, file=sys.stderr, flush=True)
class Db(Command):
""" Create, drop, dump, load databases """
name = 'db'
def run(self, cmdargs):
"""Command-line version of the database manager.
Doesn't provide a `create` command as that's not useful. Commands are
all filestore-aware.
"""
parser = argparse.ArgumentParser(
prog=f'{Path(sys.argv[0]).name} {self.name}',
description=self.__doc__.strip()
)
parser.add_argument('-c', '--config')
parser.add_argument('-D', '--data-dir')
parser.add_argument('--addons-path')
parser.add_argument('-r', '--db_user')
parser.add_argument('-w', '--db_password')
parser.add_argument('--pg_path')
parser.add_argument('--db_host')
parser.add_argument('--db_port')
parser.add_argument('--db_sslmode')
parser.set_defaults(func=lambda _: exit(parser.format_help()))
subs = parser.add_subparsers()
load = subs.add_parser(
"load", help="Load a dump file.",
description="Loads a dump file into odoo, dump file can be a URL. "
"If `database` is provided, uses that as the database name. "
"Otherwise uses the dump file name without extension.")
load.set_defaults(func=self.load)
load.add_argument(
'-f', '--force', action='store_const', default=False, const=True,
help="delete `database` database before loading if it exists"
)
load.add_argument(
'-n', '--neutralize', action='store_const', default=False, const=True,
help="neutralize the database after restore"
)
load.add_argument(
'database', nargs='?',
help="database to create, defaults to dump file's name "
"(without extension)"
)
load.add_argument('dump_file', help="zip or pg_dump file to load")
dump = subs.add_parser(
"dump", help="Create a dump with filestore.",
description="Creates a dump file. The dump is always in zip format "
"(with filestore), to get a no-filestore format use "
"pg_dump directly.")
dump.set_defaults(func=self.dump)
dump.add_argument('database', help="database to dump")
dump.add_argument(
'dump_path', nargs='?', default='-',
help="if provided, database is dumped to specified path, otherwise "
"or if `-`, dumped to stdout",
)
duplicate = subs.add_parser("duplicate", help="Duplicate a database including filestore.")
duplicate.set_defaults(func=self.duplicate)
duplicate.add_argument(
'-f', '--force', action='store_const', default=False, const=True,
help="delete `target` database before copying if it exists"
)
duplicate.add_argument(
'-n', '--neutralize', action='store_const', default=False, const=True,
help="neutralize the target database after duplicate"
)
duplicate.add_argument("source")
duplicate.add_argument("target", help="database to copy `source` to, must not exist unless `-f` is specified in which case it will be dropped first")
rename = subs.add_parser("rename", help="Rename a database including filestore.")
rename.set_defaults(func=self.rename)
rename.add_argument(
'-f', '--force', action='store_const', default=False, const=True,
help="delete `target` database before renaming if it exists"
)
rename.add_argument('source')
rename.add_argument("target", help="database to rename `source` to, must not exist unless `-f` is specified, in which case it will be dropped first")
drop = subs.add_parser("drop", help="Delete a database including filestore")
drop.set_defaults(func=self.drop)
drop.add_argument("database", help="database to delete")
args = parser.parse_args(cmdargs)
config.parse_config([
val
for k, v in vars(args).items()
if v is not None
if k in ['config', 'data_dir', 'addons_path'] or k.startswith(('db_', 'pg_'))
for val in [
'--data-dir' if k == 'data_dir'\
else '--addons-path' if k == 'addons_path'\
else f'--{k}',
v,
]
])
# force db management active to bypass check when only a
# `check_db_management_enabled` version is available.
config['list_db'] = True
report_configuration()
args.func(args)
def load(self, args):
db_name = args.database or Path(args.dump_file).stem
self._check_target(db_name, delete_if_exists=args.force)
url = urllib.parse.urlparse(args.dump_file)
if url.scheme:
eprint(f"Fetching {args.dump_file}...", end='')
r = requests.get(args.dump_file, timeout=10)
if not r.ok:
exit(f" unable to fetch {args.dump_file}: {r.reason}")
eprint(" done")
dump_file = io.BytesIO(r.content)
else:
eprint(f"Restoring {args.dump_file}...")
dump_file = args.dump_file
if not zipfile.is_zipfile(dump_file):
exit("Not a zipped dump file, use `pg_restore` to restore raw dumps,"
" and `psql` to execute sql dumps or scripts.")
restore_db(db=db_name, dump_file=dump_file, copy=True, neutralize_database=args.neutralize)
def dump(self, args):
if args.dump_path == '-':
dump_db(args.database, sys.stdout.buffer)
else:
with open(args.dump_path, 'wb') as f:
dump_db(args.database, f)
def duplicate(self, args):
self._check_target(args.target, delete_if_exists=args.force)
exp_duplicate_database(args.source, args.target, neutralize_database=args.neutralize)
def rename(self, args):
self._check_target(args.target, delete_if_exists=args.force)
exp_rename(args.source, args.target)
def drop(self, args):
if not exp_drop(args.database):
exit(f"Database {args.database} does not exist.")
def _check_target(self, target, *, delete_if_exists):
if exp_db_exist(target):
if delete_if_exists:
exp_drop(target)
else:
exit(f"Target database {target} exists, aborting.\n\n"
f"\tuse `--force` to delete the existing database anyway.")

View file

@ -0,0 +1,89 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import argparse
import os
import requests
import sys
import tempfile
import zipfile
from pathlib import Path
from . import Command
class Deploy(Command):
"""Deploy a module on an Odoo instance"""
def __init__(self):
super(Deploy, self).__init__()
self.session = requests.session()
def deploy_module(self, module_path, url, login, password, db='', force=False):
url = url.rstrip('/')
module_file = self.zip_module(module_path)
try:
return self.login_upload_module(module_file, url, login, password, db, force=force)
finally:
os.remove(module_file)
def login_upload_module(self, module_file, url, login, password, db, force=False):
print("Uploading module file...")
self.session.get(f'{url}/web/login?db={db}', allow_redirects=False) # this set the db in the session
endpoint = url + '/base_import_module/login_upload'
post_data = {
'login': login,
'password': password,
'db': db,
'force': '1' if force else '',
}
with open(module_file, 'rb') as f:
res = self.session.post(endpoint, files={'mod_file': f}, data=post_data)
if res.status_code == 404:
raise Exception(
"The server '%s' does not have the 'base_import_module' installed or is not up-to-date." % url)
res.raise_for_status()
return res.text
def zip_module(self, path):
path = os.path.abspath(path)
if not os.path.isdir(path):
raise Exception("Could not find module directory '%s'" % path)
container, module_name = os.path.split(path)
temp = tempfile.mktemp(suffix='.zip')
try:
print("Zipping module directory...")
with zipfile.ZipFile(temp, 'w') as zfile:
for root, dirs, files in os.walk(path):
for file in files:
file_path = os.path.join(root, file)
zfile.write(file_path, file_path.split(container).pop())
return temp
except Exception:
os.remove(temp)
raise
def run(self, cmdargs):
parser = argparse.ArgumentParser(
prog=f'{Path(sys.argv[0]).name} {self.name}',
description=self.__doc__
)
parser.add_argument('path', help="Path of the module to deploy")
parser.add_argument('url', nargs='?', help='Url of the server (default=http://localhost:8069)', default="http://localhost:8069")
parser.add_argument('--db', dest='db', help='Database to use if server does not use db-filter.')
parser.add_argument('--login', dest='login', default="admin", help='Login (default=admin)')
parser.add_argument('--password', dest='password', default="admin", help='Password (default=admin)')
parser.add_argument('--verify-ssl', action='store_true', help='Verify SSL certificate')
parser.add_argument('--force', action='store_true', help='Force init even if module is already installed. (will update `noupdate="1"` records)')
if not cmdargs:
sys.exit(parser.print_help())
args = parser.parse_args(args=cmdargs)
if not args.verify_ssl:
self.session.verify = False
try:
if not args.url.startswith(('http://', 'https://')):
args.url = 'https://%s' % args.url
result = self.deploy_module(args.path, args.url, args.login, args.password, args.db, force=args.force)
print(result)
except Exception as e:
sys.exit("ERROR: %s" % e)

View file

@ -0,0 +1,36 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import argparse
import os
import secrets
import sys
import textwrap
from pathlib import Path
from passlib.hash import pbkdf2_sha512
from . import Command
from odoo.tools import config
class GenProxyToken(Command):
""" Generate and (re)set proxy access token in config file """
def generate_token(self, length=16):
token = secrets.token_hex(int(length / 2))
split_size = int(length / 4)
return '-'.join(textwrap.wrap(token, split_size))
def run(self, cmdargs):
parser = argparse.ArgumentParser(
prog=f'{Path(sys.argv[0]).name} {self.name}',
description=self.__doc__.strip()
)
parser.add_argument('-c', '--config', type=str, help="Specify an alternate config file")
parser.add_argument('--token-length', type=int, help="Token Length", default=16)
args, _ = parser.parse_known_args()
if args.config:
config.rcfile = args.config
token = self.generate_token(length=args.token_length)
config['proxy_access_token'] = pbkdf2_sha512.hash(token)
config.save()
sys.stdout.write(f'{token}\n')

View file

@ -0,0 +1,51 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import optparse
import sys
from pathlib import Path
import odoo
from . import Command
_logger = logging.getLogger(__name__)
class Neutralize(Command):
"""Neutralize a production database for testing: no emails sent, etc."""
def run(self, args):
parser = odoo.tools.config.parser
parser.prog = f'{Path(sys.argv[0]).name} {self.name}'
group = optparse.OptionGroup(parser, "Neutralize", "Neutralize the database specified by the `-d` argument.")
group.add_option("--stdout", action="store_true", dest="to_stdout",
help="Output the neutralization SQL instead of applying it")
parser.add_option_group(group)
opt = odoo.tools.config.parse_config(args)
dbname = odoo.tools.config['db_name']
if not dbname:
_logger.error('Neutralize command needs a database name. Use "-d" argument')
sys.exit(1)
if not opt.to_stdout:
_logger.info("Starting %s database neutralization", dbname)
try:
with odoo.sql_db.db_connect(dbname).cursor() as cursor:
if opt.to_stdout:
installed_modules = odoo.modules.neutralize.get_installed_modules(cursor)
queries = odoo.modules.neutralize.get_neutralization_queries(installed_modules)
# pylint: disable=bad-builtin
print('BEGIN;')
for query in queries:
# pylint: disable=bad-builtin
print(query.rstrip(";") + ";")
# pylint: disable=bad-builtin
print("COMMIT;")
else:
odoo.modules.neutralize.neutralize_database(cursor)
except Exception:
_logger.critical("An error occurred during the neutralization. THE DATABASE IS NOT NEUTRALIZED!")
sys.exit(1)

View file

@ -0,0 +1,250 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo
import sys
import optparse
import logging
from collections import defaultdict
from psycopg2 import sql
from . import Command
_logger = logging.getLogger(__name__)
class Obfuscate(Command):
"""Obfuscate data in a given odoo database"""
def __init__(self):
super().__init__()
self.cr = None
def _ensure_cr(func):
def check_cr(self, *args, **kwargs):
if not self.cr:
raise Exception("No database connection")
return func(self, *args, **kwargs)
return check_cr
@_ensure_cr
def begin(self):
self.cr.execute("begin work")
self.cr.execute("CREATE EXTENSION IF NOT EXISTS pgcrypto")
@_ensure_cr
def commit(self):
self.cr.commit()
@_ensure_cr
def rollback(self):
self.cr.rollback()
@_ensure_cr
def set_pwd(self, pwd):
"""Set password to cypher/uncypher datas"""
self.cr.execute("INSERT INTO ir_config_parameter (key, value) VALUES ('odoo_cyph_pwd', 'odoo_cyph_'||encode(pgp_sym_encrypt(%s, %s), 'base64')) ON CONFLICT(key) DO NOTHING", [pwd, pwd])
@_ensure_cr
def check_pwd(self, pwd):
"""If password is set, check if it's valid"""
uncypher_pwd = self.uncypher_string(sql.Identifier('value'))
try:
qry = sql.SQL("SELECT {uncypher_pwd} FROM ir_config_parameter WHERE key='odoo_cyph_pwd'").format(uncypher_pwd=uncypher_pwd)
self.cr.execute(qry, {'pwd': pwd})
if self.cr.rowcount == 0 or (self.cr.rowcount == 1 and self.cr.fetchone()[0] == pwd):
return True
except Exception as e: # noqa: BLE001
_logger.error("Error checking password: %s", e)
return False
@_ensure_cr
def clear_pwd(self):
"""Unset password to cypher/uncypher datas"""
self.cr.execute("DELETE FROM ir_config_parameter WHERE key='odoo_cyph_pwd' ")
def cypher_string(self, sql_field):
# don't double cypher fields
return sql.SQL("""CASE WHEN starts_with({field_name}, 'odoo_cyph_') THEN {field_name} ELSE 'odoo_cyph_'||encode(pgp_sym_encrypt({field_name}, %(pwd)s), 'base64') END""").format(field_name=sql_field)
def uncypher_string(self, sql_field):
return sql.SQL("""CASE WHEN starts_with({field_name}, 'odoo_cyph_') THEN pgp_sym_decrypt(decode(substring({field_name}, 11)::text, 'base64'), %(pwd)s) ELSE {field_name} END""").format(field_name=sql_field)
def check_field(self, table, field):
qry = "SELECT udt_name FROM information_schema.columns WHERE table_name=%s AND column_name=%s"
self.cr.execute(qry, [table, field])
if self.cr.rowcount == 1:
res = self.cr.fetchone()
if res[0] in ['text', 'varchar']:
# Doesn t work for selection fields ...
return 'string'
if res[0] == 'jsonb':
return 'json'
return False
def get_all_fields(self):
qry = "SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public' AND udt_name IN ['text', 'varchar', 'jsonb'] AND NOT table_name LIKE 'ir_%' ORDER BY 1,2"
self.cr.execute(qry)
return self.cr.fetchall()
def convert_table(self, table, fields, pwd, with_commit=False, unobfuscate=False):
cypherings = []
cyph_fct = self.uncypher_string if unobfuscate else self.cypher_string
for field in fields:
field_type = self.check_field(table, field)
if field_type == 'string':
cypher_query = cyph_fct(sql.Identifier(field))
cypherings.append(sql.SQL('{field}={cypher}').format(field=sql.Identifier(field), cypher=cypher_query))
elif field_type == 'json':
# List every key
# Loop on keys
# Nest the jsonb_set calls to update all values at once
# Do not create the key in json if doesn't esist
new_field_value = sql.Identifier(field)
self.cr.execute(sql.SQL('select distinct jsonb_object_keys({field}) as key from {table}').format(field=sql.Identifier(field), table=sql.Identifier(table)))
keys = [k[0] for k in self.cr.fetchall()]
for key in keys:
cypher_query = cyph_fct(sql.SQL("{field}->>{key}").format(field=sql.Identifier(field), key=sql.Literal(key)))
new_field_value = sql.SQL("""jsonb_set({new_field_value}, array[{key}], to_jsonb({cypher_query})::jsonb, FALSE) """).format(new_field_value=new_field_value, key=sql.Literal(key), cypher_query=cypher_query)
cypherings.append(sql.SQL('{field}={cypher}').format(field=sql.Identifier(field), cypher=new_field_value))
if cypherings:
query = sql.SQL("UPDATE {table} SET {fields}").format(table=sql.Identifier(table), fields=sql.SQL(',').join(cypherings))
self.cr.execute(query, {"pwd": pwd})
if with_commit:
self.commit()
self.begin()
def confirm_not_secure(self):
_logger.info("The obfuscate method is not considered as safe to transfer anonymous datas to a third party.")
conf_y = input(f"This will alter data in the database {self.dbname} and can lead to a data loss. Would you like to proceed [y/N]? ")
if conf_y.upper() != 'Y':
self.rollback()
sys.exit(0)
conf_db = input(f"Please type your database name ({self.dbname}) in UPPERCASE to confirm you understand this operation is not considered secure : ")
if self.dbname.upper() != conf_db:
self.rollback()
sys.exit(0)
return True
def run(self, cmdargs):
parser = odoo.tools.config.parser
group = optparse.OptionGroup(parser, "Obfuscate Configuration")
group.add_option('--pwd', dest="pwd", default=False, help="Cypher password")
group.add_option('--fields', dest="fields", default=False, help="List of table.columns to obfuscate/unobfuscate: table1.column1,table2.column1,table2.column2")
group.add_option('--exclude', dest="exclude", default=False, help="List of table.columns to exclude from obfuscate/unobfuscate: table1.column1,table2.column1,table2.column2")
group.add_option('--file', dest="file", default=False, help="File containing the list of table.columns to obfuscate/unobfuscate")
group.add_option('--unobfuscate', action='store_true', default=False)
group.add_option('--allfields', action='store_true', default=False, help="Used in unobfuscate mode, try to unobfuscate all fields. Cannot be used in obfuscate mode. Slower than specifying fields.")
group.add_option('--vacuum', action='store_true', default=False, help="Vacuum database after unobfuscating")
group.add_option('--pertablecommit', action='store_true', default=False, help="Commit after each table instead of a big transaction")
group.add_option(
'-y', '--yes', dest="yes", action='store_true', default=False,
help="Don't ask for manual confirmation. Use it carefully as the obfuscate method is not considered as safe to transfer anonymous datas to a third party.")
parser.add_option_group(group)
if not cmdargs:
sys.exit(parser.print_help())
try:
opt = odoo.tools.config.parse_config(cmdargs)
if not opt.pwd:
_logger.error("--pwd is required")
sys.exit("ERROR: --pwd is required")
if opt.allfields and not opt.unobfuscate:
_logger.error("--allfields can only be used in unobfuscate mode")
sys.exit("ERROR: --allfields can only be used in unobfuscate mode")
self.dbname = odoo.tools.config['db_name']
self.registry = odoo.registry(self.dbname)
with self.registry.cursor() as cr:
self.cr = cr
self.begin()
if self.check_pwd(opt.pwd):
fields = [
('mail_tracking_value', 'old_value_char'),
('mail_tracking_value', 'old_value_text'),
('mail_tracking_value', 'new_value_char'),
('mail_tracking_value', 'new_value_text'),
('res_partner', 'name'),
('res_partner', 'display_name'),
('res_partner', 'email'),
('res_partner', 'phone'),
('res_partner', 'mobile'),
('res_partner', 'street'),
('res_partner', 'street2'),
('res_partner', 'city'),
('res_partner', 'zip'),
('res_partner', 'vat'),
('res_partner', 'website'),
('res_country', 'name'),
('mail_message', 'subject'),
('mail_message', 'email_from'),
('mail_message', 'reply_to'),
('mail_message', 'body'),
('crm_lead', 'name'),
('crm_lead', 'contact_name'),
('crm_lead', 'partner_name'),
('crm_lead', 'email_from'),
('crm_lead', 'phone'),
('crm_lead', 'mobile'),
('crm_lead', 'website'),
('crm_lead', 'description'),
]
if opt.fields:
if not opt.allfields:
fields += [tuple(f.split('.')) for f in opt.fields.split(',')]
else:
_logger.error("--allfields option is set, ignoring --fields option")
if opt.file:
with open(opt.file, encoding='utf-8') as f:
fields += [tuple(l.strip().split('.')) for l in f]
if opt.exclude:
if not opt.allfields:
fields = [f for f in fields if f not in [tuple(f.split('.')) for f in opt.exclude.split(',')]]
else:
_logger.error("--allfields option is set, ignoring --exclude option")
if opt.allfields:
fields = self.get_all_fields()
else:
invalid_fields = [f for f in fields if not self.check_field(f[0], f[1])]
if invalid_fields:
_logger.error("Invalid fields: %s", ', '.join([f"{f[0]}.{f[1]}" for f in invalid_fields]))
fields = [f for f in fields if f not in invalid_fields]
if not opt.unobfuscate and not opt.yes:
self.confirm_not_secure()
_logger.info("Processing fields: %s", ', '.join([f"{f[0]}.{f[1]}" for f in fields]))
tables = defaultdict(set)
for t, f in fields:
if t[0:3] != 'ir_' and '.' not in t:
tables[t].add(f)
if opt.unobfuscate:
_logger.info("Unobfuscating datas")
for table in tables:
_logger.info("Unobfuscating table %s", table)
self.convert_table(table, tables[table], opt.pwd, opt.pertablecommit, True)
if opt.vacuum:
_logger.info("Vacuuming obfuscated tables")
for table in tables:
_logger.debug("Vacuuming table %s", table)
self.cr.execute(sql.SQL("""VACUUM FULL {table}""").format(table=sql.Identifier(table)))
self.clear_pwd()
else:
_logger.info("Obfuscating datas")
self.set_pwd(opt.pwd)
for table in tables:
_logger.info("Obfuscating table %s", table)
self.convert_table(table, tables[table], opt.pwd, opt.pertablecommit)
self.commit()
else:
self.rollback()
except Exception as e: # noqa: BLE001
sys.exit("ERROR: %s" % e)

View file

@ -0,0 +1,129 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import fnmatch
import logging
import optparse
import sys
import time
from contextlib import nullcontext
from pathlib import Path
from unittest.mock import patch
import odoo
from . import Command
_logger = logging.getLogger(__name__)
class Populate(Command):
""" Inject fake data inside a database for testing """
def run(self, cmdargs):
parser = odoo.tools.config.parser
parser.prog = f'{Path(sys.argv[0]).name} {self.name}'
group = optparse.OptionGroup(parser, "Populate Configuration")
group.add_option("--size", dest="population_size",
help="Populate database with auto-generated data. Value should be the population size: small, medium or large",
default='small')
group.add_option("--models",
dest='populate_models',
help="Comma separated list of model or pattern (fnmatch)")
group.add_option("--profile",
dest='profiling_enabled', action="store_true",
help="Specify if you want to profile records population.",
default=False)
group.add_option("--rollback",
dest='populate_rollback', action="store_true",
help="Specify if you want to rollback database population.",
default=False)
parser.add_option_group(group)
opt = odoo.tools.config.parse_config(cmdargs)
populate_models = opt.populate_models and set(opt.populate_models.split(','))
dbname = odoo.tools.config['db_name']
registry = odoo.registry(dbname)
with registry.cursor() as cr:
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
self.populate(
env, opt.population_size, populate_models,
profiling_enabled=opt.profiling_enabled,
commit=not opt.populate_rollback)
@classmethod
def populate(cls, env, size, model_patterns=False, profiling_enabled=False, commit=True):
registry = env.registry
populated_models = None
try:
registry.populated_models = {} # todo master, initialize with already populated models
ordered_models = cls._get_ordered_models(env, model_patterns)
_logger.log(25, 'Populating database')
for model in ordered_models:
if profiling_enabled:
profiling_context = odoo.tools.profiler.Profiler(
description=f'{model} {size}',
db=env.cr.dbname
)
else:
profiling_context = nullcontext()
if commit:
commit_context = nullcontext()
else:
commit_context = patch('odoo.sql_db.Cursor.commit')
_logger.info('Populating database for model %s', model._name)
t0 = time.time()
with profiling_context, commit_context:
registry.populated_models[model._name] = model._populate(size).ids
if not registry.populated_models[model._name]:
# Do not create ir.profile records
# for models without any population factories
profiling_context.db = False
# force the flush to make sure population time still
# considers flushing all values to database
env.flush_all()
if commit:
env.cr.commit()
model_time = time.time() - t0
if model_time > 1:
_logger.info('Populated database for model %s (total: %fs) (average: %fms per record)',
model._name, model_time, model_time / len(registry.populated_models[model._name]) * 1000)
except:
_logger.exception('Something went wrong populating database')
finally:
if not commit:
env.cr.rollback()
populated_models = registry.populated_models
del registry.populated_models
return populated_models
@classmethod
def _get_ordered_models(cls, env, model_patterns=False):
_logger.info('Computing model order')
processed = set()
ordered_models = []
visited = set()
def add_model(model):
if model not in processed:
if model in visited:
raise ValueError('Cyclic dependency detected for %s' % model)
visited.add(model)
for dep in model._populate_dependencies:
add_model(env[dep])
ordered_models.append(model)
processed.add(model)
for model in env.values():
if model_patterns and not any(fnmatch.fnmatch(model._name, match) for match in model_patterns):
continue
if model._transient or model._abstract:
continue
ir_model = env['ir.model'].search([('model', '=', model._name)])
if not model_patterns and all(module.startswith('test_') for module in ir_model.modules.split(',')):
continue
add_model(model)
return ordered_models

View file

@ -0,0 +1,136 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import argparse
import os
import re
import sys
from pathlib import Path
import jinja2
from . import Command
class Scaffold(Command):
""" Generates an Odoo module skeleton. """
def run(self, cmdargs):
# TODO: bash completion file
parser = argparse.ArgumentParser(
prog=f'{Path(sys.argv[0]).name} {self.name}',
description=self.__doc__,
epilog=self.epilog(),
)
parser.add_argument(
'-t', '--template', type=template, default=template('default'),
help="Use a custom module template, can be a template name or the"
" path to a module template (default: %(default)s)")
parser.add_argument('name', help="Name of the module to create")
parser.add_argument(
'dest', default='.', nargs='?',
help="Directory to create the module in (default: %(default)s)")
if not cmdargs:
sys.exit(parser.print_help())
args = parser.parse_args(args=cmdargs)
args.template.render_to(
snake(args.name),
directory(args.dest, create=True),
{'name': args.name})
def epilog(self):
return "Built-in templates available are: %s" % ', '.join(
d for d in os.listdir(builtins())
if d != 'base'
)
builtins = lambda *args: os.path.join(
os.path.abspath(os.path.dirname(__file__)),
'templates',
*args)
def snake(s):
""" snake cases ``s``
:param str s:
:return: str
"""
# insert a space before each uppercase character preceded by a
# non-uppercase letter
s = re.sub(r'(?<=[^A-Z])\B([A-Z])', r' \1', s)
# lowercase everything, split on whitespace and join
return '_'.join(s.lower().split())
def pascal(s):
return ''.join(
ss.capitalize()
for ss in re.sub(r'[_\s]+', ' ', s).split()
)
def directory(p, create=False):
expanded = os.path.abspath(
os.path.expanduser(
os.path.expandvars(p)))
if create and not os.path.exists(expanded):
os.makedirs(expanded)
if not os.path.isdir(expanded):
die("%s is not a directory" % p)
return expanded
env = jinja2.Environment()
env.filters['snake'] = snake
env.filters['pascal'] = pascal
class template(object):
def __init__(self, identifier):
# TODO: archives (zipfile, tarfile)
self.id = identifier
# is identifier a builtin?
self.path = builtins(identifier)
if os.path.isdir(self.path):
return
# is identifier a directory?
self.path = identifier
if os.path.isdir(self.path):
return
die("{} is not a valid module template".format(identifier))
def __str__(self):
return self.id
def files(self):
""" Lists the (local) path and content of all files in the template
"""
for root, _, files in os.walk(self.path):
for f in files:
path = os.path.join(root, f)
yield path, open(path, 'rb').read()
def render_to(self, modname, directory, params=None):
""" Render this module template to ``dest`` with the provided
rendering parameters
"""
# overwrite with local
for path, content in self.files():
local = os.path.relpath(path, self.path)
# strip .template extension
root, ext = os.path.splitext(local)
if ext == '.template':
local = root
dest = os.path.join(directory, modname, local)
destdir = os.path.dirname(dest)
if not os.path.exists(destdir):
os.makedirs(destdir)
with open(dest, 'wb') as f:
if ext not in ('.py', '.xml', '.csv', '.js', '.rst', '.html', '.template'):
f.write(content)
else:
env.from_string(content.decode('utf-8'))\
.stream(params or {})\
.dump(f, encoding='utf-8')
def die(message, code=1):
print(message, file=sys.stderr)
sys.exit(code)
def warn(message):
# ASK: shall we use logger ?
print("WARNING:", message)

View file

@ -0,0 +1,187 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
"""
OpenERP - Server
OpenERP is an ERP+CRM program for small and medium businesses.
The whole source code is distributed under the terms of the
GNU Public Licence.
(c) 2003-TODAY, Fabien Pinckaers - OpenERP SA
"""
import atexit
import csv # pylint: disable=deprecated-module
import logging
import os
import signal
import sys
import threading
import traceback
import time
from pathlib import Path
from psycopg2 import ProgrammingError, errorcodes
import odoo
from . import Command
__author__ = odoo.release.author
__version__ = odoo.release.version
# Also use the `odoo` logger for the main script.
_logger = logging.getLogger('odoo')
def check_root_user():
"""Warn if the process's user is 'root' (on POSIX system)."""
if os.name == 'posix':
import getpass
if getpass.getuser() == 'root':
sys.stderr.write("Running as user 'root' is a security risk.\n")
def check_postgres_user():
""" Exit if the configured database user is 'postgres'.
This function assumes the configuration has been initialized.
"""
config = odoo.tools.config
if (config['db_user'] or os.environ.get('PGUSER')) == 'postgres':
sys.stderr.write("Using the database user 'postgres' is a security risk, aborting.")
sys.exit(1)
def report_configuration():
""" Log the server version and some configuration values.
This function assumes the configuration has been initialized.
"""
config = odoo.tools.config
_logger.info("Odoo version %s", __version__)
if os.path.isfile(config.rcfile):
_logger.info("Using configuration file at " + config.rcfile)
_logger.info('addons paths: %s', odoo.addons.__path__)
if config.get('upgrade_path'):
_logger.info('upgrade path: %s', config['upgrade_path'])
if config.get('pre_upgrade_scripts'):
_logger.info('extra upgrade scripts: %s', config['pre_upgrade_scripts'])
host = config['db_host'] or os.environ.get('PGHOST', 'default')
port = config['db_port'] or os.environ.get('PGPORT', 'default')
user = config['db_user'] or os.environ.get('PGUSER', 'default')
_logger.info('database: %s@%s:%s', user, host, port)
if sys.version_info[:2] > odoo.MAX_PY_VERSION:
_logger.warning("Python %s is not officially supported, please use Python %s instead",
'.'.join(map(str, sys.version_info[:2])),
'.'.join(map(str, odoo.MAX_PY_VERSION))
)
def rm_pid_file(main_pid):
config = odoo.tools.config
if config['pidfile'] and main_pid == os.getpid():
try:
os.unlink(config['pidfile'])
except OSError:
pass
def setup_pid_file():
""" Create a file with the process id written in it.
This function assumes the configuration has been initialized.
"""
config = odoo.tools.config
if not odoo.evented and config['pidfile']:
pid = os.getpid()
with open(config['pidfile'], 'w') as fd:
fd.write(str(pid))
atexit.register(rm_pid_file, pid)
def export_translation():
config = odoo.tools.config
dbname = config['db_name']
if config["language"]:
msg = "language %s" % (config["language"],)
else:
msg = "new language"
_logger.info('writing translation file for %s to %s', msg,
config["translate_out"])
fileformat = os.path.splitext(config["translate_out"])[-1][1:].lower()
# .pot is the same fileformat as .po
if fileformat == "pot":
fileformat = "po"
with open(config["translate_out"], "wb") as buf:
registry = odoo.modules.registry.Registry.new(dbname)
with registry.cursor() as cr:
odoo.tools.trans_export(config["language"],
config["translate_modules"] or ["all"], buf, fileformat, cr)
_logger.info('translation file written successfully')
def import_translation():
config = odoo.tools.config
overwrite = config["overwrite_existing_translations"]
dbname = config['db_name']
registry = odoo.modules.registry.Registry.new(dbname)
with registry.cursor() as cr:
translation_importer = odoo.tools.translate.TranslationImporter(cr)
translation_importer.load_file(config["translate_in"], config["language"])
translation_importer.save(overwrite=overwrite)
def main(args):
check_root_user()
odoo.tools.config.parse_config(args)
check_postgres_user()
report_configuration()
config = odoo.tools.config
# the default limit for CSV fields in the module is 128KiB, which is not
# quite sufficient to import images to store in attachment. 500MiB is a
# bit overkill, but better safe than sorry I guess
csv.field_size_limit(500 * 1024 * 1024)
preload = []
if config['db_name']:
preload = config['db_name'].split(',')
for db_name in preload:
try:
odoo.service.db._create_empty_database(db_name)
config['init']['base'] = True
except ProgrammingError as err:
if err.pgcode == errorcodes.INSUFFICIENT_PRIVILEGE:
# We use an INFO loglevel on purpose in order to avoid
# reporting unnecessary warnings on build environment
# using restricted database access.
_logger.info("Could not determine if database %s exists, "
"skipping auto-creation: %s", db_name, err)
else:
raise err
except odoo.service.db.DatabaseExists:
pass
if config["translate_out"]:
export_translation()
sys.exit(0)
if config["translate_in"]:
import_translation()
sys.exit(0)
# This needs to be done now to ensure the use of the multiprocessing
# signaling mechanism for registries loaded with -d
if config['workers']:
odoo.multi_process = True
stop = config["stop_after_init"]
setup_pid_file()
rc = odoo.service.server.start(preload=preload, stop=stop)
sys.exit(rc)
class Server(Command):
"""Start the odoo server (default command)"""
def run(self, args):
odoo.tools.config.parser.prog = f'{Path(sys.argv[0]).name} {self.name}'
main(args)

View file

@ -0,0 +1,124 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import code
import logging
import os
import signal
import sys
from pathlib import Path
import odoo
from odoo.tools import config
from . import Command
_logger = logging.getLogger(__name__)
"""
Shell exit behaviors
====================
Legend:
stop = The REPL main loop stop.
raise = Exception raised.
loop = Stay in REPL.
Shell | ^D | exit() | quit() | sys.exit() | raise SystemExit()
----------------------------------------------------------------------
python | stop | raise | raise | raise | raise
ipython | stop | stop | stop | loop | loop
ptpython | stop | raise | raise | raise | raise
bpython | stop | stop | stop | stop | stop
"""
def raise_keyboard_interrupt(*a):
raise KeyboardInterrupt()
class Console(code.InteractiveConsole):
def __init__(self, locals=None, filename="<console>"):
code.InteractiveConsole.__init__(self, locals, filename)
try:
import readline
import rlcompleter
except ImportError:
print('readline or rlcompleter not available, autocomplete disabled.')
else:
readline.set_completer(rlcompleter.Completer(locals).complete)
readline.parse_and_bind("tab: complete")
class Shell(Command):
"""Start odoo in an interactive shell"""
supported_shells = ['ipython', 'ptpython', 'bpython', 'python']
def init(self, args):
config.parser.prog = f'{Path(sys.argv[0]).name} {self.name}'
config.parse_config(args)
odoo.cli.server.report_configuration()
odoo.service.server.start(preload=[], stop=True)
signal.signal(signal.SIGINT, raise_keyboard_interrupt)
def console(self, local_vars):
if not os.isatty(sys.stdin.fileno()):
local_vars['__name__'] = '__main__'
exec(sys.stdin.read(), local_vars)
else:
if 'env' not in local_vars:
print('No environment set, use `%s shell -d dbname` to get one.' % sys.argv[0])
for i in sorted(local_vars):
print('%s: %s' % (i, local_vars[i]))
preferred_interface = config.options.get('shell_interface')
if preferred_interface:
shells_to_try = [preferred_interface, 'python']
else:
shells_to_try = self.supported_shells
for shell in shells_to_try:
try:
return getattr(self, shell)(local_vars)
except ImportError:
pass
except Exception:
_logger.warning("Could not start '%s' shell." % shell)
_logger.debug("Shell error:", exc_info=True)
def ipython(self, local_vars):
from IPython import start_ipython
start_ipython(argv=[], user_ns=local_vars)
def ptpython(self, local_vars):
from ptpython.repl import embed
embed({}, local_vars)
def bpython(self, local_vars):
from bpython import embed
embed(local_vars)
def python(self, local_vars):
Console(locals=local_vars).interact()
def shell(self, dbname):
local_vars = {
'openerp': odoo,
'odoo': odoo,
}
if dbname:
registry = odoo.registry(dbname)
with registry.cursor() as cr:
uid = odoo.SUPERUSER_ID
ctx = odoo.api.Environment(cr, uid, {})['res.users'].context_get()
env = odoo.api.Environment(cr, uid, ctx)
local_vars['env'] = env
local_vars['self'] = env.user
self.console(local_vars)
cr.rollback()
else:
self.console(local_vars)
def run(self, args):
self.init(args)
self.shell(config['db_name'])
return 0

View file

@ -0,0 +1,84 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import argparse
import glob
import itertools
import os
import sys
from pathlib import Path
import odoo
from . import Command
from .server import main
from odoo.modules.module import get_module_root, MANIFEST_NAMES
from odoo.service.db import _create_empty_database, DatabaseExists
class Start(Command):
""" Quickly start the odoo server with default options """
def get_module_list(self, path):
mods = itertools.chain.from_iterable(
glob.glob(os.path.join(path, '*/%s' % mname))
for mname in MANIFEST_NAMES
)
return [mod.split(os.path.sep)[-2] for mod in mods]
def run(self, cmdargs):
odoo.tools.config.parser.prog = f'{Path(sys.argv[0]).name} {self.name}'
parser = argparse.ArgumentParser(
prog=f'{Path(sys.argv[0]).name} {self.name}',
description=self.__doc__.strip(),
)
parser.add_argument('--path', default=".",
help="Directory where your project's modules are stored (will autodetect from current dir)")
parser.add_argument("-d", "--database", dest="db_name", default=None,
help="Specify the database name (default to project's directory name")
args, unknown = parser.parse_known_args(args=cmdargs)
# When in a virtualenv, by default use it's path rather than the cwd
if args.path == '.' and os.environ.get('VIRTUAL_ENV'):
args.path = os.environ.get('VIRTUAL_ENV')
project_path = os.path.abspath(os.path.expanduser(os.path.expandvars(args.path)))
module_root = get_module_root(project_path)
db_name = None
if module_root:
# started in a module so we choose this module name for database
db_name = project_path.split(os.path.sep)[-1]
# go to the parent's directory of the module root
project_path = os.path.abspath(os.path.join(project_path, os.pardir))
# check if one of the subfolders has at least one module
mods = self.get_module_list(project_path)
if mods and '--addons-path' not in cmdargs:
cmdargs.append('--addons-path=%s' % project_path)
if not args.db_name:
args.db_name = db_name or project_path.split(os.path.sep)[-1]
cmdargs.extend(('-d', args.db_name))
# TODO: forbid some database names ? eg template1, ...
try:
_create_empty_database(args.db_name)
odoo.tools.config['init']['base'] = True
except DatabaseExists as e:
pass
except Exception as e:
die("Could not create database `%s`. (%s)" % (args.db_name, e))
if '--db-filter' not in cmdargs:
cmdargs.append('--db-filter=^%s$' % args.db_name)
# Remove --path /-p options from the command arguments
def to_remove(i, l):
return l[i] == '-p' or l[i].startswith('--path') or \
(i > 0 and l[i-1] in ['-p', '--path'])
cmdargs = [v for i, v in enumerate(cmdargs)
if not to_remove(i, cmdargs)]
main(cmdargs)
def die(message, code=1):
print(message, file=sys.stderr)
sys.exit(code)

View file

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
{
'name': "{{ name }}",
'summary': """
Short (1 phrase/line) summary of the module's purpose, used as
subtitle on modules listing or apps.openerp.com""",
'description': """
Long description of module's purpose
""",
'author': "My Company",
'website': "https://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/16.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base'],
# always loaded
'data': [
# 'security/ir.model.access.csv',
'views/views.xml',
'views/templates.xml',
],
# only loaded in demonstration mode
'demo': [
'demo/demo.xml',
],
}

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import controllers

View file

@ -0,0 +1,25 @@
{%- set mod = name|snake -%}
{%- set model = "%s.%s"|format(mod, mod) -%}
{%- set root = "/%s/%s"|format(mod, mod) -%}
# -*- coding: utf-8 -*-
# from odoo import http
# class {{ mod|pascal }}(http.Controller):
# @http.route('{{ root }}', auth='public')
# def index(self, **kw):
# return "Hello, world"
# @http.route('{{ root }}/objects', auth='public')
# def list(self, **kw):
# return http.request.render('{{ mod }}.listing', {
# 'root': '{{ root }}',
# 'objects': http.request.env['{{ model }}'].search([]),
# })
# @http.route('{{ root }}/objects/<model("{{ model }}"):obj>', auth='public')
# def object(self, obj, **kw):
# return http.request.render('{{ mod }}.object', {
# 'object': obj
# })

View file

@ -0,0 +1,12 @@
{%- set mod= name|snake -%}
{%- set model = "%s.%s"|format(mod, mod) -%}
<odoo>
<data>
<!--{% for item in range(5) %}
<record id="object{{ item }}" model="{{ model }}">
<field name="name">Object {{ item }}</field>
<field name="value">{{ item * 10 }}</field>
</record>
{% endfor %}-->
</data>
</odoo>

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# from odoo import models, fields, api
# class {{ name|snake }}(models.Model):
# _name = '{{ name|snake }}.{{ name|snake }}'
# _description = '{{ name|snake }}.{{ name|snake }}'
# name = fields.Char()
# value = fields.Integer()
# value2 = fields.Float(compute="_value_pc", store=True)
# description = fields.Text()
#
# @api.depends('value')
# def _value_pc(self):
# for record in self:
# record.value2 = float(record.value) / 100

View file

@ -0,0 +1,6 @@
{%- set snake_name = name|snake -%}
{%- set id = "access_%s_%s"|format(snake_name, snake_name) -%}
{%- set name = "%s.%s"|format(snake_name, snake_name) -%}
{%- set model_id = "model_%s_%s"|format(snake_name, snake_name) -%}
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
{{ id }},{{ name }},{{ model_id }},base.group_user,1,1,1,1

View file

@ -0,0 +1,24 @@
<odoo>
<data>
<!--
<template id="listing">
<ul>
<li t-foreach="objects" t-as="object">
<a t-attf-href="#{ root }/objects/#{ object.id }">
<t t-esc="object.display_name"/>
</a>
</li>
</ul>
</template>
<template id="object">
<h1><t t-esc="object.display_name"/></h1>
<dl>
<t t-foreach="object._fields" t-as="field">
<dt><t t-esc="field"/></dt>
<dd><t t-esc="object[field]"/></dd>
</t>
</dl>
</template>
-->
</data>
</odoo>

View file

@ -0,0 +1,62 @@
{%- set mod = name|snake -%}
{%- set model = "%s.%s"|format(mod, mod) -%}
<odoo>
<data>
<!-- explicit list view definition -->
<!--
<record model="ir.ui.view" id="{{mod}}.list">
<field name="name">{{name}} list</field>
<field name="model">{{model}}</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="value"/>
<field name="value2"/>
</tree>
</field>
</record>
-->
<!-- actions opening views on models -->
<!--
<record model="ir.actions.act_window" id="{{mod}}.action_window">
<field name="name">{{name}} window</field>
<field name="res_model">{{model}}</field>
<field name="view_mode">tree,form</field>
</record>
-->
<!-- server action to the one above -->
<!--
<record model="ir.actions.server" id="{{mod}}.action_server">
<field name="name">{{name}} server</field>
<field name="model_id" ref="model_{{mod}}_{{mod}}"/>
<field name="state">code</field>
<field name="code">
action = {
"type": "ir.actions.act_window",
"view_mode": "tree,form",
"res_model": model._name,
}
</field>
</record>
-->
<!-- Top menu item -->
<!--
<menuitem name="{{name}}" id="{{mod}}.menu_root"/>
-->
<!-- menu categories -->
<!--
<menuitem name="Menu 1" id="{{mod}}.menu_1" parent="{{mod}}.menu_root"/>
<menuitem name="Menu 2" id="{{mod}}.menu_2" parent="{{mod}}.menu_root"/>
-->
<!-- actions -->
<!--
<menuitem name="List" id="{{mod}}.menu_1_list" parent="{{mod}}.menu_1"
action="{{mod}}.action_window"/>
<menuitem name="Server to list" id="{{mod}}" parent="{{mod}}.menu_2"
action="{{mod}}.action_server"/>
-->
</data>
</odoo>

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View file

@ -0,0 +1,24 @@
{
# Theme information
'name': "{{name}}",
'description': """
""",
'category': 'Theme',
'version': '0.1',
'depends': ['website'],
# templates
'data': [
'views/options.xml',
'views/snippets.xml',
],
# demo pages
'demo': [
'demo/pages.xml',
],
# Your information
'author': "My Company",
'website': "https://www.yourcompany.com",
}

View file

@ -0,0 +1,133 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import argparse
import glob
import json
import os
import re
import sys
from pathlib import Path
from . import Command
from odoo.modules.module import MANIFEST_NAMES
class TSConfig(Command):
""" Generates tsconfig files for javascript code """
def get_module_list(self, path):
return [
mod.split(os.path.sep)[-2]
for mname in MANIFEST_NAMES
for mod in glob.glob(os.path.join(path, f'*/{mname}'))
]
def clean_path(self, path):
return re.sub(r"/{2,}", "/", path)
def prefix_suffix_path(self, path, prefix, suffix):
return self.clean_path(f"{prefix}/{path}/{suffix}")
def remove_(self, modules, module):
for name, path in modules:
if module == name:
modules.remove((name, path))
def run(self, cmdargs):
parser = argparse.ArgumentParser(
prog=f'{Path(sys.argv[0]).name} {self.name}',
description=self.__doc__.strip()
)
parser.add_argument('--addons-path', type=str, nargs=1, dest="paths")
args = parser.parse_args(args=cmdargs)
paths = list(map(self.clean_path, args.paths[0].split(',')))
modules = {}
owl_path = ""
for path in paths:
for module in self.get_module_list(path):
modules[module] = self.prefix_suffix_path(module, path, "/static/src/*")
if module == "web":
owl_path = self.prefix_suffix_path(module, path, "/static/lib/owl/owl.js")
content = self.generate_file_content(modules, paths)
content["compilerOptions"]["paths"]["@odoo/owl"] = [owl_path]
# pylint: disable=bad-builtin
print(json.dumps(content, indent=2))
def generate_imports(self, modules):
return {
f'@{module}/*': [path]
for module, path in modules.items()
}
def generate_file_content(self, modules, paths):
return {
'compilerOptions': {
"baseUrl": ".",
"target": "es2019",
"checkJs": True,
"allowJs": True,
"noEmit": True,
"typeRoots": list(map(lambda p: p + "/web/tooling/types", paths)),
"paths": self.generate_imports(modules)
}, "exclude": self.generate_excludes()
}
def generate_excludes(self):
return [
"/**/*.po",
"/**/*.py",
"/**/*.pyc",
"/**/*.xml",
"/**/*.png",
"/**/*.md",
"/**/*.dat",
"/**/*.scss",
"/**/*.jpg",
"/**/*.svg",
"/**/*.pot",
"/**/*.csv",
"/**/*.mo",
"/**/*.txt",
"/**/*.less",
"/**/*.bcmap",
"/**/*.properties",
"/**/*.html",
"/**/*.ttf",
"/**/*.rst",
"/**/*.css",
"/**/*.pack",
"/**/*.idx",
"/**/*.h",
"/**/*.map",
"/**/*.gif",
"/**/*.sample",
"/**/*.doctree",
"/**/*.so",
"/**/*.pdf",
"/**/*.xslt",
"/**/*.conf",
"/**/*.woff",
"/**/*.xsd",
"/**/*.eot",
"/**/*.jst",
"/**/*.flow",
"/**/*.sh",
"/**/*.yml",
"/**/*.pfb",
"/**/*.jpeg",
"/**/*.crt",
"/**/*.template",
"/**/*.pxd",
"/**/*.dylib",
"/**/*.pem",
"/**/*.rng",
"/**/*.xsl",
"/**/*.xls",
"/**/*.cfg",
"/**/*.pyi",
"/**/*.pth",
"/**/*.markdown",
"/**/*.key",
"/**/*.ico",
]