Files
odoo_source/odoo/cli/scaffold.py
T
dasz d7df1856fa [IMP] cli: add scaffold for payroll localization
Added few basic rules and folders that are usually common to most localizations.
The template will also serve as documentation on naming conventions (not strict atm).

closes odoo/odoo#121446

Usage: ./odoo_bin scaffold -t l10n_payroll name-code (e.g. estonia-ee) [destination]
Signed-off-by: Kevin Baptiste <kba@odoo.com>
2023-06-26 16:41:35 +02:00

151 lines
4.7 KiB
Python

# 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)
if args.template.id == 'l10n_payroll':
name_split = args.name.split('-')
params = {
'name': name_split[0],
'code': name_split[1]
}
else:
params = {'name': args.name}
args.template.render_to(
snake(args.name),
directory(args.dest, create=True),
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__)),
'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('[_\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():
path = env.from_string(path).render(params)
local = os.path.relpath(path, self.path)
# strip .template extension
root, ext = os.path.splitext(local)
if ext == '.template':
local = root
if self.id == "l10n_payroll":
modname = f"l10n_{params['code']}_hr_payroll"
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')
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)