19.0 vanilla

This commit is contained in:
Ernad Husremovic 2025-10-03 18:07:25 +02:00
parent 0a7ae8db93
commit 991d2234ca
416 changed files with 646602 additions and 300844 deletions

View file

@ -1,23 +1,4 @@
import logging
import sys
import os
import odoo
from .command import Command, main
from . import cloc
from . import upgrade_code
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
# Import just the command, the rest will get imported as needed
from .command import Command, main # noqa: F401
COMMAND = None

View file

@ -1,47 +1,43 @@
# 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 """
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 --addons-path=dirs cloc -d database
In the latter mode, only the custom code is accounted for.
"""
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)
self.parser.add_argument('--database', '-d', dest="database", help="Database name")
self.parser.add_argument('--path', '-p', action='append', help="File or directory path")
self.parser.add_argument('--verbose', '-v', action='count', default=0)
opt, unknown = self.parser.parse_known_args(args)
if not opt.database and not opt.path:
parser.print_help()
self.parser.print_help()
sys.exit()
c = cloc.Cloc()
if opt.database:
if ',' in opt.database:
sys.exit("-d/--database has multiple databases, please provide a single one")
config.parse_config(['-d', opt.database] + unknown)
c.count_database(opt.database)
c.count_database(config['db_name'][0])
if opt.path:
for i in opt.path:
c.count_path(i)

View file

@ -1,69 +1,139 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import os
import argparse
import contextlib
import re
import sys
from inspect import cleandoc
from pathlib import Path
import odoo
from odoo.modules import get_modules, get_module_path, initialize_sys_path
import odoo.init # import first for core setup
import odoo.cli
from odoo.modules import initialize_sys_path, load_script
from odoo.tools import config
COMMAND_NAME_RE = re.compile(r'^[a-z][a-z0-9_]*$', re.I)
PROG_NAME = Path(sys.argv[0]).name
commands = {}
"""All loaded commands"""
class Command:
name = None
description = None
epilog = None
_parser = None
def __init_subclass__(cls):
cls.name = cls.name or cls.__name__.lower()
commands[cls.name] = cls
module = cls.__module__.rpartition('.')[2]
if not cls.is_valid_name(cls.name):
raise ValueError(
f"Command name {cls.name!r} "
f"must match {COMMAND_NAME_RE.pattern!r}")
if cls.name != module:
raise ValueError(
f"Command name {cls.name!r} "
f"must match Module name {module!r}")
commands[cls.name] = cls
@property
def prog(self):
return f"{PROG_NAME} [--addons-path=PATH,...] {self.name}"
@property
def parser(self):
if not self._parser:
self._parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
prog=self.prog,
description=cleandoc(self.description or self.__doc__ or ""),
epilog=cleandoc(self.epilog or ""),
)
return self._parser
@classmethod
def is_valid_name(cls, name):
return re.match(COMMAND_NAME_RE, name)
ODOO_HELP = """\
Odoo CLI, use '{odoo_bin} --help' for regular server options.
def load_internal_commands():
""" Load ``commands`` from ``odoo.cli`` """
for path in odoo.cli.__path__:
for module in Path(path).iterdir():
if module.suffix != '.py':
continue
__import__(f'odoo.cli.{module.stem}')
Available commands:
{command_list}
Use '{odoo_bin} <command> --help' for individual command help."""
def load_addons_commands(command=None):
"""
Search the addons path for modules with a ``cli/{command}.py`` file.
In case no command is provided, discover and load all the commands.
"""
if command is None:
command = '*'
elif not Command.is_valid_name(command):
return
mapping = {}
initialize_sys_path()
for path in odoo.addons.__path__:
for fullpath in Path(path).glob(f'*/cli/{command}.py'):
if (found_command := fullpath.stem) and Command.is_valid_name(found_command):
# loading as odoo.cli and not odoo.addons.{module}.cli
# so it doesn't load odoo.addons.{module}.__init__
mapping[f'odoo.cli.{found_command}'] = fullpath
for fq_name, fullpath in mapping.items():
with contextlib.suppress(ImportError):
load_script(fullpath, fq_name)
def find_command(name: str) -> Command | None:
""" Get command by name. """
# built-in commands
if command := commands.get(name):
return command
# import from odoo.cli
with contextlib.suppress(ImportError):
__import__(f'odoo.cli.{name}')
return commands[name]
# import from odoo.addons.*.cli
load_addons_commands(command=name)
return commands.get(name)
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("-"):
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]])
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]
if len(args) and not args[0].startswith('-'):
# Command specified, search for it
command_name = args[0]
args = args[1:]
if command in commands:
o = commands[command]()
odoo.cli.COMMAND = command
o.run(args)
elif '-h' in args or '--help' in args:
# No command specified, but help is requested
command_name = 'help'
args = [x for x in args if x not in ('-h', '--help')]
else:
sys.exit('Unknown command %r' % (command,))
# No command specified, default command used
command_name = 'server'
if command := find_command(command_name):
odoo.cli.COMMAND = command_name
command().run(args)
else:
message = (
f"Unknown command {command_name!r}.\n"
f"Use '{PROG_NAME} --help' to see the list of available commands."
)
sys.exit(message)

View file

@ -1,35 +1,41 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import argparse
import io
import urllib.parse
import sys
import textwrap
import urllib.parse
import zipfile
from argparse import RawTextHelpFormatter
from functools import partial
from pathlib import Path
import requests
from ..service.db import (
dump_db,
exp_create_database,
exp_db_exist,
exp_drop,
exp_duplicate_database,
exp_rename,
restore_db,
)
from ..tools import config
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'
description = """
Command-line version of the database manager.
Commands are all filestore-aware.
"""
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 = self.parser
parser.add_argument('-c', '--config')
parser.add_argument('-D', '--data-dir')
parser.add_argument('--addons-path')
@ -42,6 +48,55 @@ class Db(Command):
parser.set_defaults(func=lambda _: exit(parser.format_help()))
subs = parser.add_subparsers()
# INIT ----------------------------------
init = subs.add_parser(
"init",
help="Create and initialize a database",
description="Create an empty database and install the minimum required modules",
formatter_class=RawTextHelpFormatter,
)
init.set_defaults(func=self.init)
init.add_argument(
'database',
help="database to create",
)
init.add_argument(
'--with-demo', action='store_true',
help="install demo data in the new database",
)
init.add_argument(
'--force', action='store_true',
help="delete database if exists",
)
init.add_argument(
'--language', default='en_US',
help="default language for the instance, default 'en_US'",
)
init.add_argument(
'--username', default='admin',
help="admin username, default 'admin'",
)
init.add_argument(
'--password', default='admin',
help="admin password, default 'admin'",
)
init.add_argument(
'--country',
help="country to be set on the main company",
)
init.epilog = textwrap.dedent("""\
Database initialization will install the minimum required modules.
To install more modules, use the `module install` command.
For more info:
$ odoo-bin module install --help
""")
# LOAD ----------------------------------
load = subs.add_parser(
"load", help="Load a dump file.",
description="Loads a dump file into odoo, dump file can be a URL. "
@ -63,11 +118,13 @@ class Db(Command):
)
load.add_argument('dump_file', help="zip or pg_dump file to load")
# DUMP ----------------------------------
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.")
"(with filestore), to get pg_dump format, use "
"dump_format argument.")
dump.set_defaults(func=self.dump)
dump.add_argument('database', help="database to dump")
dump.add_argument(
@ -75,6 +132,18 @@ class Db(Command):
help="if provided, database is dumped to specified path, otherwise "
"or if `-`, dumped to stdout",
)
dump.add_argument(
'--format', dest='dump_format', choices=('zip', 'dump'), default='zip',
help="if provided, database is dumped used the specified format, "
"otherwise defaults to `zip`.\n"
"Supported formats are `zip`, `dump` (pg_dump format) ",
)
dump.add_argument(
'--no-filestore', action='store_const', dest='filestore', default=True, const=False,
help="if passed, zip database is dumped without filestore (default: false)"
)
# DUPLICATE -----------------------------
duplicate = subs.add_parser("duplicate", help="Duplicate a database including filestore.")
duplicate.set_defaults(func=self.duplicate)
@ -89,6 +158,8 @@ class Db(Command):
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 --------------------------------
rename = subs.add_parser("rename", help="Rename a database including filestore.")
rename.set_defaults(func=self.rename)
rename.add_argument(
@ -98,6 +169,8 @@ class Db(Command):
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 ----------------------------------
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")
@ -123,6 +196,18 @@ class Db(Command):
args.func(args)
def init(self, args):
self._check_target(args.database, delete_if_exists=args.force)
exp_create_database(
db_name=args.database,
demo=args.with_demo,
lang=args.language,
login=args.username,
user_password=args.password,
country_code=args.country,
phone=None,
)
def load(self, args):
db_name = args.database or Path(args.dump_file).stem
self._check_target(db_name, delete_if_exists=args.force)
@ -151,7 +236,7 @@ class Db(Command):
dump_db(args.database, sys.stdout.buffer)
else:
with open(args.dump_path, 'wb') as f:
dump_db(args.database, f)
dump_db(args.database, f, args.dump_format, args.filestore)
def duplicate(self, args):
self._check_target(args.target, delete_if_exists=args.force)

View file

@ -1,18 +1,17 @@
# 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__()
super().__init__()
self.session = requests.session()
def deploy_module(self, module_path, url, login, password, db='', force=False):
@ -51,7 +50,7 @@ class Deploy(Command):
try:
print("Zipping module directory...")
with zipfile.ZipFile(temp, 'w') as zfile:
for root, dirs, files in os.walk(path):
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())
@ -61,10 +60,7 @@ class Deploy(Command):
raise
def run(self, cmdargs):
parser = argparse.ArgumentParser(
prog=f'{Path(sys.argv[0]).name} {self.name}',
description=self.__doc__
)
parser = self.parser
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.')

View file

@ -1,36 +0,0 @@
# 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,40 @@
import textwrap
import odoo.addons
import odoo.modules
import odoo.release
from .command import PROG_NAME, Command, commands, load_addons_commands, load_internal_commands
class Help(Command):
""" Display the list of available commands """
template = textwrap.dedent("""\
usage: {prog_name} [--addons-path=PATH,...] <command> [...]
Odoo {version}
Available commands:
{command_list}
Use '{prog_name} server --help' for regular server options.
Use '{prog_name} <command> --help' for other individual commands options.
""")
def run(self, args):
load_internal_commands()
load_addons_commands()
padding = max(len(cmd_name) for cmd_name in commands) + 2
name_desc = [
(cmd_name, (cmd.__doc__ or "").strip())
for cmd_name, cmd in sorted(commands.items())
]
command_list = "\n".join(f" {name:<{padding}}{desc}" for name, desc in name_desc)
print(Help.template.format( # noqa: T201
prog_name=PROG_NAME,
version=odoo.release.version,
command_list=command_list,
))

View file

@ -0,0 +1,240 @@
import argparse
import logging
import sys
import textwrap
from pathlib import Path
from odoo import SUPERUSER_ID
from odoo.api import Environment
from odoo.cli.command import Command
from odoo.modules import get_module_path
from odoo.modules.registry import Registry
from odoo.tools import OrderedSet, config
from odoo.tools.translate import TranslationImporter, load_language, trans_export
_logger = logging.getLogger(__name__)
EXPORT_EXTENSIONS = ['.po', '.pot', '.tgz', '.csv']
IMPORT_EXTENSIONS = ['.po', '.csv']
class SubcommandHelpFormatter(argparse.RawTextHelpFormatter):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs, max_help_position=80)
class I18n(Command):
""" Import, export, setup languages and internationalization files """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
subparsers = self.parser.add_subparsers(
dest='subcommand', required=True,
help='Subcommands help')
self.import_parser = subparsers.add_parser(
'import',
help="Import i18n files",
description="Imports provided translation files",
formatter_class=SubcommandHelpFormatter,
)
self.export_parser = subparsers.add_parser(
'export',
help="Export i18n files",
description="Exports language files into the i18n folder of each module",
formatter_class=SubcommandHelpFormatter,
)
self.loadlang_parser = subparsers.add_parser(
'loadlang',
help="Load languages",
description="Loads languages",
formatter_class=SubcommandHelpFormatter,
)
for parser in (self.import_parser, self.export_parser, self.loadlang_parser):
parser.add_argument(
'-c', '--config', dest='config',
help="use a specific configuration file")
parser.add_argument(
'-d', '--database', dest='db_name', default=None,
help="database name, connection details will be taken from the config file")
parser.epilog = textwrap.dedent("""\
Language codes must follow the XPG (POSIX) locale format.
see: https://www.gnu.org/software/libc/manual/html_node/Locale-Names.html
To list available codes, you can search them querying the database:
$ psql -d <dbname> -c "SELECT iso_code FROM res_lang ORDER BY iso_code"
Examples:
odoo-bin i18n loadlang -l en # English (U.S.)
odoo-bin i18n loadlang -l es es_AR # Spanish (Spain, Argentina)
odoo-bin i18n loadlang -l sr@latin # Serbian (Latin)
""")
self.import_parser.add_argument(
'files', nargs='+', metavar='FILE', type=Path,
help=f"files to be imported. Allowed extensions: {', '.join(IMPORT_EXTENSIONS)}\n")
self.import_parser.add_argument(
'-w', '--overwrite', action='store_true',
help="overwrite existing terms")
self.import_parser.add_argument(
'-l', '--language', dest='language', metavar='LANG', required=True,
help="language code")
self.export_parser.add_argument(
'-l', '--languages', dest='languages', nargs='+', default=['pot'], metavar='LANG',
help="list of language codes, 'pot' for template (default)")
self.export_parser.add_argument(
'modules', nargs='+', metavar='MODULE',
help="modules to be exported")
self.export_parser.add_argument(
'-o', '--output', metavar="FILE", dest='output',
help=(
"output only one file with translations from all provided modules\n"
f"allowed extensions: {', '.join(EXPORT_EXTENSIONS)},"
" '-' writes a '.po' file to stdout\n"
"only one language is allowed when this option is active"
),
)
self.loadlang_parser.add_argument(
'-l', '--languages', dest='languages', nargs='+', metavar='LANG',
help="List of language codes to install")
def run(self, cmdargs):
parsed_args = self.parser.parse_args(args=cmdargs)
config_args = []
if parsed_args.config:
config_args += ['-c', parsed_args.config]
if parsed_args.db_name:
config_args += ['-d', parsed_args.db_name]
config.parse_config(config_args, setup_logging=True)
db_names = config['db_name']
if not db_names or len(db_names) > 1:
self.parser.error("Please provide a single database in the config file")
parsed_args.db_name = db_names[0]
match parsed_args.subcommand:
case 'import':
self._import(parsed_args)
case 'export':
self._export(parsed_args)
case 'loadlang':
self._loadlang(parsed_args)
def _get_languages(self, env, language_codes, active_test=True):
# We want to log invalid parameters
Lang = env['res.lang'].with_context(active_test=False)
languages = Lang.search([('iso_code', 'in', language_codes)])
if not_found_language_codes := set(language_codes) - set(languages.mapped("iso_code")):
_logger.warning("Ignoring not found languages: %s", ', '.join(not_found_language_codes))
if active_test:
if not_installed_languages := languages.filtered(lambda x: not x.active):
languages -= not_installed_languages
iso_code_str = ", ".join(not_installed_languages.mapped("iso_code"))
_logger.warning("Ignoring not installed languages: %s", iso_code_str)
return languages
def _import(self, parsed_args):
paths = OrderedSet(parsed_args.files)
if invalid_paths := [path for path in paths if (
not path.exists()
or path.suffix not in IMPORT_EXTENSIONS
)]:
_logger.warning("Ignoring invalid paths: %s",
', '.join(str(path) for path in invalid_paths))
paths -= set(invalid_paths)
if not paths:
self.import_parser.error("No valid path was provided")
with Registry(parsed_args.db_name).cursor() as cr:
env = Environment(cr, SUPERUSER_ID, {})
translation_importer = TranslationImporter(cr)
language = self._get_languages(env, [parsed_args.language])
if not language:
self.import_parser.error("No valid language has been provided")
for path in paths:
with path.open("rb") as infile:
translation_importer.load(infile, path.suffix.removeprefix('.'), language.code)
translation_importer.save(overwrite=parsed_args.overwrite)
def _export(self, parsed_args):
export_pot = 'pot' in parsed_args.languages
if parsed_args.output:
if len(parsed_args.languages) != 1:
self.export_parser.error(
"When --output is specified, one single --language must be supplied")
if parsed_args.output != '-':
parsed_args.output = Path(parsed_args.output)
if parsed_args.output.suffix not in EXPORT_EXTENSIONS:
self.export_parser.error(
f"Extensions allowed for --output are {', '.join(EXPORT_EXTENSIONS)}")
if export_pot and parsed_args.output.suffix == '.csv':
self.export_parser.error(
"Cannot export template in .csv format, please specify a language.")
if export_pot:
parsed_args.languages.remove('pot')
with Registry(parsed_args.db_name).cursor(readonly=True) as cr:
env = Environment(cr, SUPERUSER_ID, {})
# We want to log invalid parameters
modules = env['ir.module.module'].search_fetch(
[('name', 'in', parsed_args.modules)], ['name', 'state'])
if not_found_module_names := set(parsed_args.modules) - set(modules.mapped("name")):
_logger.warning("Ignoring not found modules: %s",
", ".join(not_found_module_names))
if not_installed_modules := modules.filtered(lambda x: x.state != 'installed'):
_logger.warning("Ignoring not installed modules: %s",
", ".join(not_installed_modules.mapped("name")))
modules -= not_installed_modules
if len(modules) < 1:
self.export_parser.error("No valid module has been provided")
module_names = modules.mapped("name")
languages = self._get_languages(env, parsed_args.languages)
languages_count = len(languages) + export_pot
if languages_count == 0:
self.export_parser.error("No valid language has been provided")
if parsed_args.output:
self._export_file(env, module_names, languages.code, parsed_args.output)
else:
# Po(t) files in the modules' i18n folders
for module_name in module_names:
i18n_path = Path(get_module_path(module_name), 'i18n')
if export_pot:
path = i18n_path / f'{module_name}.pot'
self._export_file(env, [module_name], None, path)
for language in languages:
path = i18n_path / f'{language.iso_code}.po'
self._export_file(env, [module_name], language.code, path)
def _export_file(self, env, module_names, lang_code, path):
source = module_names[0] if len(module_names) == 1 else 'modules'
destination = 'stdout' if path == '-' else path
_logger.info("Exporting %s (%s) to %s", source, lang_code or 'pot', destination)
if destination == 'stdout':
if not trans_export(lang_code, module_names, sys.stdout.buffer, 'po', env):
_logger.warning("No translatable terms were found in %s.", module_names)
return
path.parent.mkdir(exist_ok=True)
export_format = path.suffix.removeprefix('.')
if export_format == 'pot':
export_format = 'po'
with path.open('wb') as outfile:
if not trans_export(lang_code, module_names, outfile, export_format, env):
_logger.warning("No translatable terms were found in %s.", module_names)
def _loadlang(self, parsed_args):
with Registry(parsed_args.db_name).cursor() as cr:
env = Environment(cr, SUPERUSER_ID, {})
for language in self._get_languages(env, parsed_args.languages, active_test=False):
load_language(env.cr, language.code)

View file

@ -0,0 +1,175 @@
import argparse
import logging
import textwrap
from contextlib import contextmanager
from pathlib import Path
from odoo import SUPERUSER_ID
from odoo.api import Environment
from odoo.cli.command import Command
from odoo.modules.loading import force_demo
from odoo.modules.module import get_module_path, initialize_sys_path
from odoo.modules.registry import Registry
from odoo.tools import OrderedSet, config, parse_version
_logger = logging.getLogger(__name__)
class Module(Command):
""" Manage modules, install demo data """
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
subparsers = self.parser.add_subparsers(
dest='subcommand', required=True,
help='Subcommands help')
install_parser = subparsers.add_parser(
'install',
help="Install modules",
description="Install selected modules",
)
install_parser.set_defaults(func=self._install)
upgrade_parser = subparsers.add_parser(
'upgrade',
help="Upgrade modules",
description="Upgrade selected modules",
)
upgrade_parser.set_defaults(func=self._upgrade)
uninstall_parser = subparsers.add_parser(
'uninstall',
help="Uninstall modules",
description="Uninstall selected modules",
)
uninstall_parser.set_defaults(func=self._uninstall)
force_demo_parser = subparsers.add_parser(
'force-demo',
help="Install demo data (force)",
description="Install demonstration data (force)",
)
force_demo_parser.set_defaults(func=self._force_demo)
for parser in (
install_parser,
uninstall_parser,
upgrade_parser,
force_demo_parser,
):
parser.formatter_class = argparse.RawDescriptionHelpFormatter
parser.add_argument(
'-c', '--config', dest='config',
help="use a specific configuration file")
parser.add_argument(
'-d', '--database', dest='db_name', default=None,
help="database name, connection details will be taken from the config file")
install_parser.add_argument(
'modules', nargs='+', metavar='MODULE',
help="names of the modules to be installed. For data modules (.zip), use the path instead")
install_parser.epilog = textwrap.dedent("""\
Before installing modules, an Odoo database needs to be created and initialized
on your PostgreSQL instance, using the `db init` command:
$ odoo-bin db init <db_name>
To get help on its parameters, see:
$ odoo-bin db init --help
""")
uninstall_parser.add_argument(
'modules', nargs='+', metavar='MODULE',
help="names of the modules to be uninstalled")
upgrade_parser.add_argument(
'modules', nargs='+', metavar='MODULE',
help="name of the modules to be upgraded, use 'base' if you want to upgrade everything")
upgrade_parser.add_argument(
'--outdated', action='store_true',
help="only update modules that have a newer version on disk",
)
def run(self, cmdargs):
parsed_args = self.parser.parse_args(args=cmdargs)
config_args = []
if parsed_args.config:
config_args += ['-c', parsed_args.config]
if parsed_args.db_name:
config_args += ['-d', parsed_args.db_name]
config.parse_config(config_args, setup_logging=True)
db_names = config['db_name']
if not db_names or len(db_names) > 1:
self.parser.error("Please provide a single database in the config file")
parsed_args.db_name = db_names[0]
parsed_args.func(parsed_args)
def _get_zip_path(self, path):
fullpath = Path(path).resolve()
if fullpath.is_file() and fullpath.suffix.lower() == '.zip':
return fullpath
return None
def _get_module_names(self, module_names):
""" Get valid module names from disk before starting the Db environment """
initialize_sys_path()
return {
module
for module in set(module_names)
if get_module_path(module)
or self._get_zip_path(module)
}
def _get_modules(self, env, module_names):
Module = env['ir.module.module']
Module.update_list()
return Module.search([('name', 'in', module_names)])
@contextmanager
def _create_env_context(self, db_name):
with Registry.new(db_name).cursor() as cr:
yield Environment(cr, SUPERUSER_ID, {})
def _install(self, parsed_args):
with self._create_env_context(parsed_args.db_name) as env:
valid_module_names = self._get_module_names(parsed_args.modules)
installable_modules = self._get_modules(env, valid_module_names)
if installable_modules:
installable_modules.button_immediate_install()
non_installable_modules = OrderedSet(
module
for module in parsed_args.modules
if module not in set(installable_modules.mapped("name"))
)
importable_zipfiles = [
fullpath
for module in non_installable_modules
if (fullpath := self._get_zip_path(module))
]
if importable_zipfiles:
if 'imported' not in env['ir.module.module']._fields:
_logger.warning("Cannot import data modules unless the `base_import_module` module is installed")
else:
for importable_zipfile in importable_zipfiles:
env['ir.module.module']._import_zipfile(importable_zipfile)
def _upgrade(self, parsed_args):
with self._create_env_context(parsed_args.db_name) as env:
valid_module_names = self._get_module_names(parsed_args.modules)
upgradable_modules = self._get_modules(env, valid_module_names)
if parsed_args.outdated:
upgradable_modules = upgradable_modules.filtered(
lambda x: parse_version(x.installed_version) > parse_version(x.latest_version),
)
if upgradable_modules:
upgradable_modules.button_immediate_upgrade()
def _uninstall(self, parsed_args):
with self._create_env_context(parsed_args.db_name) as env:
if modules := self._get_modules(env, parsed_args.modules):
modules.button_immediate_uninstall()
def _force_demo(self, parsed_args):
with self._create_env_context(parsed_args.db_name) as env:
force_demo(env)

View file

@ -1,10 +1,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import optparse
import sys
from pathlib import Path
import odoo
import odoo.modules.neutralize
import odoo.sql_db
import odoo.tools.config
from . import Command
@ -16,17 +16,20 @@ class Neutralize(Command):
def run(self, args):
parser = odoo.tools.config.parser
parser.prog = f'{Path(sys.argv[0]).name} {self.name}'
parser.prog = self.prog
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, setup_logging=True)
dbname = odoo.tools.config['db_name']
if not dbname:
dbnames = odoo.tools.config['db_name']
if not dbnames:
_logger.error('Neutralize command needs a database name. Use "-d" argument')
sys.exit(1)
if len(dbnames) > 1:
sys.exit("-d/--database/db_name has multiple database, please provide a single one")
dbname = dbnames[0]
if not opt.to_stdout:
_logger.info("Starting %s database neutralization", dbname)

View file

@ -1,5 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import odoo
import sys
import optparse
import logging
@ -8,7 +6,7 @@ from collections import defaultdict
from . import Command
from odoo.modules.registry import Registry
from odoo.tools import SQL
from odoo.tools import SQL, config
_logger = logging.getLogger(__name__)
@ -133,7 +131,8 @@ class Obfuscate(Command):
return True
def run(self, cmdargs):
parser = odoo.tools.config.parser
parser = config.parser
parser.prog = self.prog
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")
@ -152,14 +151,14 @@ class Obfuscate(Command):
sys.exit(parser.print_help())
try:
opt = odoo.tools.config.parse_config(cmdargs, setup_logging=True)
opt = config.parse_config(cmdargs, setup_logging=True)
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.dbname = config['db_name']
self.registry = Registry(self.dbname)
with self.registry.cursor() as cr:
self.cr = cr

View file

@ -1,15 +1,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import optparse
import sys
import time
from pathlib import Path
from odoo import api
from odoo.modules.registry import Registry
from odoo.tools import config
from odoo.tools.populate import populate_models
from . import Command
import odoo
from odoo.modules.registry import Registry
from odoo.tools.populate import populate_models
from odoo.api import Environment
DEFAULT_FACTOR = '10000'
DEFAULT_SEPARATOR = '_'
@ -22,8 +21,8 @@ class Populate(Command):
"""Populate database via duplication of existing data for testing/demo purposes"""
def run(self, cmdargs):
parser = odoo.tools.config.parser
parser.prog = f'{Path(sys.argv[0]).name} {self.name}'
parser = config.parser
parser.prog = self.prog
group = optparse.OptionGroup(parser, "Populate Configuration")
group.add_option("--factors", dest="factors",
help="Comma separated list of factors for each model, or just a single factor."
@ -39,7 +38,7 @@ class Populate(Command):
help="Single character separator for char/text fields.",
default=DEFAULT_SEPARATOR)
parser.add_option_group(group)
opt = odoo.tools.config.parse_config(cmdargs, setup_logging=True)
opt = config.parse_config(cmdargs, setup_logging=True)
# deduplicate models if necessary, and keep the last corresponding
# factor for each model
@ -53,14 +52,16 @@ class Populate(Command):
except TypeError:
raise ValueError("Separator must be a single Unicode character.")
dbname = odoo.tools.config['db_name']
registry = Registry(dbname)
dbnames = config['db_name']
if len(dbnames) > 1:
sys.exit("-d/--database/db_name has multiple database, please provide a single one")
registry = Registry(dbnames[0])
with registry.cursor() as cr:
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {'active_test': False})
env = api.Environment(cr, api.SUPERUSER_ID, {'active_test': False})
self.populate(env, model_factors, separator_code)
@classmethod
def populate(cls, env: Environment, modelname_factors: dict[str, int], separator_code: int):
def populate(cls, env: api.Environment, modelname_factors: dict[str, int], separator_code: int):
model_factors = {
model: factor
for model_name, factor in modelname_factors.items()

View file

@ -1,24 +1,25 @@
# 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 __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.epilog = "Built-in templates available are: %s" % ', '.join(
d for d in os.listdir(builtins())
if d != 'base'
)
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 = self.parser
parser.add_argument(
'-t', '--template', type=template, default=template('default'),
help="Use a custom module template, can be a template name or the"
@ -47,11 +48,6 @@ class Scaffold(Command):
params=params,
)
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__)),
@ -82,7 +78,7 @@ def directory(p, create=False):
if create and not os.path.exists(expanded):
os.makedirs(expanded)
if not os.path.isdir(expanded):
die("%s is not a directory" % p)
sys.exit("%s is not a directory" % p)
return expanded
env = jinja2.Environment()
@ -100,7 +96,7 @@ class template(object):
self.path = identifier
if os.path.isdir(self.path):
return
die("{} is not a valid module template".format(identifier))
sys.exit(f"{identifier} is not a valid module template")
def __str__(self):
return self.id
@ -141,10 +137,6 @@ class template(object):
.dump(f, encoding='utf-8')
f.write(b'\n')
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

@ -11,40 +11,34 @@ GNU Public Licence.
"""
import atexit
import csv # pylint: disable=deprecated-module
import logging
import os
import re
import sys
from pathlib import Path
from psycopg2.errors import InsufficientPrivilege
import odoo
from odoo.release import author as __author__ # noqa: F401
from odoo.release import version as __version__ # noqa: F401
from odoo.service import server
from odoo.tools import config
from . import Command
__author__ = odoo.release.author
__version__ = odoo.release.version
# Also use the `odoo` logger for the main script.
_logger = logging.getLogger('odoo')
re._MAXCACHE = 4096 # default is 512, a little too small for 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")
if os.name == 'posix' and os.getuid() == 0:
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)
@ -54,10 +48,11 @@ def report_configuration():
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)
import odoo.addons # noqa: PLC0415
import odoo.release # noqa: PLC0415
_logger.info("Odoo version %s", odoo.release.version)
if os.path.isfile(config['config']):
_logger.info("Using configuration file at %s", config['config'])
_logger.info('addons paths: %s', odoo.addons.__path__)
if config.get('upgrade_path'):
_logger.info('upgrade path: %s', config['upgrade_path'])
@ -69,16 +64,15 @@ def report_configuration():
_logger.info('database: %s@%s:%s', user, host, port)
replica_host = config['db_replica_host']
replica_port = config['db_replica_port']
if replica_host is not False or replica_port:
if replica_host or replica_port or 'replica' in config['dev_mode']:
_logger.info('replica database: %s@%s:%s', user, replica_host or 'default', replica_port or 'default')
if sys.version_info[:2] > odoo.MAX_PY_VERSION:
if sys.version_info[:2] > odoo.release.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))
'.'.join(map(str, odoo.release.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'])
@ -90,93 +84,44 @@ def setup_pid_file():
This function assumes the configuration has been initialized.
"""
config = odoo.tools.config
import odoo # for evented # noqa: PLC0415
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.translate.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, setup_logging=True)
config.parse_config(args, setup_logging=True)
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 InsufficientPrivilege as err:
# 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)
except odoo.service.db.DatabaseExists:
pass
if config["translate_out"]:
export_translation()
sys.exit(0)
if config["translate_in"]:
import_translation()
sys.exit(0)
for db_name in config['db_name']:
from odoo.service import db # noqa: PLC0415
try:
db._create_empty_database(db_name)
config['init']['base'] = True
except InsufficientPrivilege as err:
# 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)
except db.DatabaseExists:
pass
stop = config["stop_after_init"]
setup_pid_file()
rc = odoo.service.server.start(preload=preload, stop=stop)
rc = server.start(preload=config['db_name'], 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}'
config.parser.prog = self.prog
main(args)

View file

@ -1,16 +1,17 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import code
import logging
import optparse
import os
import signal
import sys
import threading
from pathlib import Path
import odoo
import odoo # to expose in the shell
from odoo import api
from odoo.modules.registry import Registry
from odoo.service import server
from odoo.tools import config
from . import Command
from . import Command, server as cli_server
_logger = logging.getLogger(__name__)
@ -39,15 +40,15 @@ def raise_keyboard_interrupt(*a):
class Console(code.InteractiveConsole):
def __init__(self, locals=None, filename="<console>"):
code.InteractiveConsole.__init__(self, locals, filename)
def __init__(self, local_vars=None, filename="<console>"):
code.InteractiveConsole.__init__(self, locals=local_vars, filename=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.set_completer(rlcompleter.Completer(local_vars).complete)
readline.parse_and_bind("tab: complete")
@ -56,10 +57,23 @@ class Shell(Command):
supported_shells = ['ipython', 'ptpython', 'bpython', 'python']
def init(self, args):
config.parser.prog = f'{Path(sys.argv[0]).name} {self.name}'
config.parser.prog = self.prog
group = optparse.OptionGroup(config.parser, "Shell options")
group.add_option(
'--shell-file', dest='shell_file', type='string', my_default='',
help="Specify a python script to be run after the start of the shell. "
"Overrides the env variable PYTHONSTARTUP."
)
group.add_option(
'--shell-interface', dest='shell_interface', type='string',
help="Specify a preferred REPL to use in shell mode. "
"Supported REPLs are: [ipython|ptpython|bpython|python]"
)
config.parser.add_option_group(group)
config.parse_config(args, setup_logging=True)
odoo.cli.server.report_configuration()
odoo.service.server.start(preload=[], stop=True)
cli_server.report_configuration()
server.start(preload=[], stop=True)
signal.signal(signal.SIGINT, raise_keyboard_interrupt)
def console(self, local_vars):
@ -72,6 +86,8 @@ class Shell(Command):
for i in sorted(local_vars):
print('%s: %s' % (i, local_vars[i]))
pythonstartup = config.options.get('shell_file') or os.environ.get('PYTHONSTARTUP')
preferred_interface = config.options.get('shell_interface')
if preferred_interface:
shells_to_try = [preferred_interface, 'python']
@ -80,27 +96,36 @@ class Shell(Command):
for shell in shells_to_try:
try:
return getattr(self, shell)(local_vars)
shell_func = getattr(self, shell)
return shell_func(local_vars, pythonstartup)
except ImportError:
pass
except Exception:
_logger.warning("Could not start '%s' shell." % shell)
_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 ipython(self, local_vars, pythonstartup=None):
from IPython import start_ipython # noqa: PLC0415
argv = (
['--TerminalIPythonApp.display_banner=False']
+ ([f'--TerminalIPythonApp.exec_files={pythonstartup}'] if pythonstartup else [])
)
start_ipython(argv=argv, user_ns=local_vars)
def ptpython(self, local_vars):
from ptpython.repl import embed
embed({}, local_vars)
def ptpython(self, local_vars, pythonstartup=None):
from ptpython.repl import embed # noqa: PLC0415
embed({}, local_vars, startup_paths=[pythonstartup] if pythonstartup else False)
def bpython(self, local_vars):
from bpython import embed
embed(local_vars)
def bpython(self, local_vars, pythonstartup=None):
from bpython import embed # noqa: PLC0415
embed(local_vars, args=['-q', '-i', pythonstartup] if pythonstartup else None)
def python(self, local_vars):
Console(locals=local_vars).interact()
def python(self, local_vars, pythonstartup=None):
console = Console(local_vars)
if pythonstartup:
with open(pythonstartup, encoding='utf-8') as f:
console.runsource(f.read(), filename=pythonstartup, symbol='exec')
console.interact(banner='')
def shell(self, dbname):
local_vars = {
@ -111,9 +136,9 @@ class Shell(Command):
threading.current_thread().dbname = dbname
registry = 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)
uid = api.SUPERUSER_ID
ctx = api.Environment(cr, uid, {})['res.users'].context_get()
env = api.Environment(cr, uid, ctx)
local_vars['env'] = env
local_vars['self'] = env.user
# context_get() has started the transaction already. Rollback to
@ -127,5 +152,12 @@ class Shell(Command):
def run(self, args):
self.init(args)
self.shell(config['db_name'])
dbnames = config['db_name']
if len(dbnames) > 1:
sys.exit("-d/--database/db_name has multiple database, please provide a single one")
if not dbnames:
self.shell(None)
else:
self.shell(dbnames[0])
return 0

View file

@ -1,16 +1,13 @@
# 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.modules.module import Manifest, MANIFEST_NAMES
from odoo.service.db import _create_empty_database, DatabaseExists
from odoo.tools import config
class Start(Command):
@ -24,26 +21,20 @@ class Start(Command):
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=".",
config.parser.prog = self.prog
self.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")
self.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)
args, _unknown = self.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:
if is_path_in_module(project_path):
# 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
@ -61,11 +52,11 @@ class Start(Command):
# TODO: forbid some database names ? eg template1, ...
try:
_create_empty_database(args.db_name)
odoo.tools.config['init']['base'] = True
config['init']['base'] = True
except DatabaseExists as e:
pass
except Exception as e:
die("Could not create database `%s`. (%s)" % (args.db_name, e))
sys.exit("Could not create database `%s`. (%s)" % (args.db_name, e))
if '--db-filter' not in cmdargs:
cmdargs.append('--db-filter=^%s$' % args.db_name)
@ -79,6 +70,12 @@ class Start(Command):
main(cmdargs)
def die(message, code=1):
print(message, file=sys.stderr)
sys.exit(code)
def is_path_in_module(path):
old_path = None
while path != old_path:
if Manifest._from_path(path):
return True
old_path = path
path, _ = os.path.split(path)
return False

View file

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

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
{
'name': "{{ name }}",

View file

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

View file

@ -1,7 +1,6 @@
{%- 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

View file

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

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
# from odoo import models, fields, api

View file

@ -15,7 +15,6 @@
result_rate = payslip.rule_parameter('l10n_{{code}}_social_employee_rate')
result = categories.BASIC
</field>
<field name="appears_on_payroll_report" eval="True"/>
<field name="struct_id" ref="l10n_{{code}}_hr_payroll.l10n_{{code}}_hr_payroll_structure_{{code}}_employee_salary"/>
</record>
@ -30,7 +29,6 @@ result = categories.BASIC
result_rate = payslip.rule_parameter('l10n_{{code}}_social_employer_rate')
result = categories.BASIC
</field>
<field name="appears_on_payroll_report" eval="True"/>
<field name="appears_on_payslip" eval="False"/>
<field name="struct_id" ref="l10n_{{code}}_hr_payroll.l10n_{{code}}_hr_payroll_structure_{{code}}_employee_salary"/>
</record>
@ -45,7 +43,6 @@ result = categories.BASIC
<field name="amount_python_compute">
result = categories['SOCIAL.EMPLOYEE']
</field>
<field name="appears_on_payroll_report" eval="True"/>
<field name="appears_on_payslip" eval="True"/>
<field name="struct_id" ref="l10n_{{code}}_hr_payroll.l10n_{{code}}_hr_payroll_structure_{{code}}_employee_salary"/>
</record>
@ -60,7 +57,6 @@ result = categories['SOCIAL.EMPLOYEE']
<field name="amount_python_compute">
result = categories['SOCIAL.EMPLOYER']
</field>
<field name="appears_on_payroll_report" eval="True"/>
<field name="appears_on_payslip" eval="False"/>
<field name="struct_id" ref="l10n_{{code}}_hr_payroll.l10n_{{code}}_hr_payroll_structure_{{code}}_employee_salary"/>
</record>
@ -75,7 +71,6 @@ result = categories['SOCIAL.EMPLOYER']
<field name="amount_python_compute">
result = categories.GROSS
</field>
<field name="appears_on_payroll_report" eval="True"/>
<field name="appears_on_payslip" eval="True"/>
<field name="struct_id" ref="l10n_{{code}}_hr_payroll.l10n_{{code}}_hr_payroll_structure_{{code}}_employee_salary"/>
</record>
@ -97,7 +92,6 @@ for low, high, prev, rate in brackets:
if low &lt;= taxable &lt; high:
result = -(prev + (taxable - low) * rate / 100)
</field>
<field name="appears_on_payroll_report" eval="True"/>
<field name="appears_on_payslip" eval="True"/>
<field name="struct_id" ref="l10n_{{code}}_hr_payroll.l10n_{{code}}_hr_payroll_structure_{{code}}_employee_salary"/>
</record>
@ -123,7 +117,6 @@ elif children == 3:
elif children &gt;= 4:
result = amounts[0] + amounts[1] + amounts[2] + (children - 3) * amounts[3]
</field>
<field name="appears_on_payroll_report" eval="True"/>
<field name="appears_on_payslip" eval="True"/>
<field name="struct_id" ref="l10n_{{code}}_hr_payroll.l10n_{{code}}_hr_payroll_structure_{{code}}_employee_salary"/>
</record>

View file

@ -35,7 +35,7 @@
<field name="job_id" ref="job_developer_{{name}}"/>
<field name="country_id" ref="base.{{code}}"/>
<field name="company_id" ref="l10n_{{code}}_hr_payroll.res_company_{{code}}"/>
<field name="gender">male</field>
<field name="sex">male</field>
</record>
<record id="l10n_{{code}}_res_partner_antonina" model="res.partner">
@ -53,10 +53,10 @@
<field name="partner_id" ref="l10n_{{code}}_hr_payroll.res_partner_antonina"/>
<field name="login">antoninakaczmarczyk@example.com</field>
<field name="password">antoninakaczmarczyk</field>
<field name="signature" type="html"><span>--<br/>+A. Kaczmarczyk</span></field>
<field name="signature">A. Kaczmarczyk</field>
<field name="company_ids" eval="[(4, ref('l10n_{{code}}_hr_payroll.res_company_{{code}}'))]"/>
<field name="company_id" ref="l10n_{{code}}_hr_payroll.res_company_{{code}}"/>
<field name="groups_id" eval="[(6,0,[ref('base.group_user')])]"/>
<field name="group_ids" eval="[(6,0,[ref('base.group_user')])]"/>
</record>
<record id="l10n_{{code}}_res_partner_antonina_work_address" model="res.partner">
@ -77,7 +77,7 @@
<record id="l10n_{{code}}_hr_employee_antonina" model="hr.employee">
<field name="name">Antonina Kaczmarczyk (fpo)</field>
<field name="gender">female</field>
<field name="sex">female</field>
<field name="marital">single</field>
<field name="job_title">Software Developer</field>
<field name="address_id" ref="l10n_{{code}}_hr_payroll.res_partner_antonina_work_address"/>
@ -100,7 +100,7 @@
<field name="country_id" ref="base.{{code}}"/>
<field name="resource_calendar_id" ref="resource.resource_calendar_std"/>
<field name="identification_id">8752027365496</field>
<field name="bank_account_id" ref="l10n_{{code}}_hr_payroll.res_partner_bank_account_norberta"/>
<field name="bank_account_ids" eval="[(4, ref('l10n_{{code}}_hr_payroll.res_partner_bank_account_norberta'))]"/>
<field name="company_id" ref="l10n_{{code}}_hr_payroll.res_company_{{code}}"/>
<field name="user_id" ref="l10n_{{code}}_hr_payroll.user_antonina"/>
</record>

View file

@ -1,5 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import hr_payslip
from . import hr_contract
from . import hr_version
from . import hr_payslip_worked_days

View file

@ -3,7 +3,7 @@
from odoo import fields, models
class HrContract(models.Model):
_inherit = 'hr.payslip'
class HrVersion(models.Model):
_inherit = ['hr.version']
l10n_{{code}}_field = fields.Char()

View file

@ -1,133 +0,0 @@
# 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",
]

View file

@ -30,6 +30,7 @@ bullets.
"""
import argparse
import functools
import sys
from importlib.machinery import SourceFileLoader
@ -38,6 +39,9 @@ from types import ModuleType
from typing import Iterator
ROOT = Path(__file__).parent.parent
UPGRADE = ROOT / 'upgrade_code'
AVAILABLE_EXT = ('.py', '.js', '.css', '.scss', '.xml', '.csv', '.po', '.pot')
try:
import odoo.addons
@ -54,15 +58,18 @@ except ImportError:
import release
from parse_version import parse_version
class Command:
pass
config = {'addons_path': ''}
""" Simplified version of the one in command.py, for standalone execution """
@property
def parser(self):
return argparse.ArgumentParser(
prog=Path(sys.argv[0]).name,
description=__doc__.replace('/odoo/upgrade_code', str(UPGRADE)),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
config = None
initialize_sys_path = None
UPGRADE = ROOT / 'upgrade_code'
AVAILABLE_EXT = ('.py', '.js', '.css', '.scss', '.xml', '.csv')
class FileAccessor:
addon: Path
path: Path
@ -113,11 +120,11 @@ class FileManager:
return self._files.get(str(path))
if sys.stdout.isatty():
def print_progress(self, current, total=None):
def print_progress(self, current: int, total: int | None =None, file_name : str | Path = ""):
total = total or len(self) or 1
print(f'{current / total:>4.0%}', end='\r', file=sys.stderr) # noqa: T201
print(f'\033[K{current / total:>4.0%} \033[37m{file_name}\033[0m', end='\r', file=sys.stderr) # noqa: T201
else:
def print_progress(self, current, total=None):
def print_progress(self, current: int, total: int | None =None, file_name : str | Path = ""):
pass
@ -168,18 +175,8 @@ def migrate(
class UpgradeCode(Command):
""" Rewrite the entire source code using the scripts found at /odoo/upgrade_code """
name = 'upgrade_code'
prog_name = Path(sys.argv[0]).name
def __init__(self):
self.parser = argparse.ArgumentParser(
prog=(
f"{self.prog_name} [--addons-path=PATH,...] {self.name}"
if initialize_sys_path else
self.prog_name
),
description=__doc__.replace('/odoo/upgrade_code', str(UPGRADE)),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
group = self.parser.add_mutually_exclusive_group(required=True)
group.add_argument(
'--script',
@ -208,7 +205,13 @@ class UpgradeCode(Command):
help="list the files that would be re-written, but rewrite none")
self.parser.add_argument(
'--addons-path',
default=config['addons_path'],
type=(
functools.partial(config.parse, 'addons_path')
if config else
# the paths must be resolved already
functools.partial(str.split, sep=',')
),
default=config['addons_path'] if config else [],
metavar='PATH,...',
help="specify additional addons paths (separated by commas)",
)