diff --git a/doc/adding-command.rst b/doc/adding-command.rst new file mode 100644 index 00000000000..5a177b06817 --- /dev/null +++ b/doc/adding-command.rst @@ -0,0 +1,34 @@ +.. _adding-command: + +Adding a new command +==================== + +``oe`` uses the argparse_ library to implement commands. Each +command lives in its own ``openerpcommand/.py`` file. + +.. _argparse: http://docs.python.org/2.7/library/argparse.html + +To create a new command, probably the most simple way to get started is to +copy/paste an existing command, say ``openerpcommand/initialize.py`` to +``openerpcommand/foo.py``. In the newly created file, the important bits +are the ``run(args)`` and ``add_parser(subparsers)`` functions. + +``add_parser``'s responsability is to create a (sub-)parser for the command, +i.e. describe the different options and flags. The last thing it does is to set +``run`` as the function to call when the command is invoked. + +.. code-block:: python + + > def add_parser(subparsers): + > parser = subparsers.add_parser('', + > description='...') + > parser.add_argument(...) + > ... + > parser.set_defaults(run=run) + +``run(args)`` actually implements the command. It should be kept as simple as +possible and delegate most of its work to small functions (probably placed at +the top of the new file). In other words, its responsability is mainly to +deal with the presence/absence/pre-processing of ``argparse``'s arguments. + +Finally, the module must be added to ``openerpcommand/__init__.py``. diff --git a/doc/index.rst b/doc/index.rst index 50a37695206..64fc04b552e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -18,6 +18,15 @@ OpenERP Server 06_misc 09_deployment +OpenERP Command +''''''''''''''' + +.. toctree:: + :maxdepth: 1 + + openerp-command.rst + adding-command.rst + OpenERP Server API '''''''''''''''''' diff --git a/doc/openerp-command.rst b/doc/openerp-command.rst new file mode 100644 index 00000000000..2bfd845f8e3 --- /dev/null +++ b/doc/openerp-command.rst @@ -0,0 +1,60 @@ +.. _openerp-command: + +OpenERP Command +=============== + +The ``oe`` script provides a set of command-line tools around the OpenERP +framework. + +Using OpenERP Command +--------------------- + +In contrast to the previous ``openerp-server`` script, ``oe`` defines a few +sub-commands, each with its own set of flags and options. You can get some +information for any of them with + +:: + + > oe --help + +For instance:: + + > oe run-tests --help + +Some ``oe`` options can be provided via environment variables. For instance:: + + > export OPENERP_DATABASE=trunk + > export OPENERP_HOST=127.0.0.1 + > export OPENERP_PORT=8069 + +Depending on your needs, you can group all of the above in one single script; +for instance here is a, say, ``test-trunk-view-validation.sh`` file:: + + COMMAND_REPO=/home/thu/repos/command/trunk/ + SERVER_REPO=/home/thu/repos/server/trunk + + export PYTHONPATH=$SERVER_REPO:$COMMAND_REPO + export PATH=$SERVER_REPO:$COMMAND_REPO:$PATH + export OPENERP_DATABASE=trunk + export OPENERP_HOST=127.0.0.1 + export OPENERP_PORT=8069 + + # The -d ignored is actually needed by `oe` even though `test_view_validation` + # itself does not need it. + oe run-tests -d ignored -m openerp.test_view_validation + +Adding new commands +------------------- + +See the :doc:`adding-command` page. + +Bash completion +--------------- + +A preliminary ``oe-bash-completion`` file is provided. After sourcing it, + +:: + + > . oe-bash-completion + +completion (using the TAB character) in Bash should be working. diff --git a/oe b/oe new file mode 100755 index 00000000000..59827731409 --- /dev/null +++ b/oe @@ -0,0 +1,5 @@ +#! /usr/bin/env python2 + +if __name__ == '__main__': + import openerpcommand.main + openerpcommand.main.run() diff --git a/oe-bash-completion b/oe-bash-completion new file mode 100644 index 00000000000..57a4391d803 --- /dev/null +++ b/oe-bash-completion @@ -0,0 +1,89 @@ +_oe() +{ + local cur prev opts + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + cmd="${COMP_WORDS[0]}" + subcmd="" + if [[ ${COMP_CWORD} > 0 ]] ; then + subcmd="${COMP_WORDS[1]}" + fi + + # oe + + opts="initialize model read run-tests scaffold update \ + call open show consume-nothing consume-memory leak-memory \ + consume-cpu bench-read bench-fields-view-get bench-dummy bench-login \ + bench-sale-mrp --help" + + if [[ ${prev} == oe && ${cur} != -* ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + + # oe call + + opts="--database --user --password --host --port --help" + + if [[ ${subcmd} == call ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + + # oe initialize + + opts="--database --addons --all-modules --exclude --no-create --help" + + if [[ ${subcmd} == initialize ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + + # oe model + + opts="--database --model --field --verbose --help" + + if [[ ${subcmd} == model ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + + # oe read + + opts="--database --model --id --field --verbose --short --help" + + if [[ ${subcmd} == read ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + + # oe run-tests + + opts="--database --addons --module --dry-run --help" + + if [[ ${subcmd} == run-tests ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + + # oe scaffold + + opts="--help" + + if [[ ${subcmd} == scaffold ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi + + # fallback for unimplemented completion + + opts="--help" + + if [[ true ]] ; then + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + fi +} +complete -F _oe oe diff --git a/openerpcommand/__init__.py b/openerpcommand/__init__.py new file mode 100644 index 00000000000..53d456c7639 --- /dev/null +++ b/openerpcommand/__init__.py @@ -0,0 +1,61 @@ +import argparse +import textwrap + +from .call import Call +from .client import Open, Show, ConsumeNothing, ConsumeMemory, LeakMemory, ConsumeCPU +from .benchmarks import Bench, BenchRead, BenchFieldsViewGet, BenchDummy, BenchLogin +from .bench_sale_mrp import BenchSaleMrp +from . import common + +from . import conf # Not really server-side (in the `for` below). +from . import drop +from . import initialize +from . import model +from . import module +from . import read +from . import run_tests +from . import scaffold +from . import uninstall +from . import update + +command_list_server = (conf, drop, initialize, model, module, read, run_tests, + scaffold, uninstall, update, ) + +command_list_client = (Call, Open, Show, ConsumeNothing, ConsumeMemory, + LeakMemory, ConsumeCPU, Bench, BenchRead, + BenchFieldsViewGet, BenchDummy, BenchLogin, + BenchSaleMrp, ) + +def main_parser(): + parser = argparse.ArgumentParser( + usage=argparse.SUPPRESS, + description=textwrap.fill(textwrap.dedent("""\ + OpenERP Command provides a set of command-line tools around + the OpenERP framework: openobject-server. All the tools are + sub-commands of a single oe executable.""")), + epilog="""Use --help to get information about the command.""", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + description = [] + for x in command_list_server: + description.append(x.__name__[len(__package__)+1:]) + if x.__doc__: + description.extend([ + ":\n", + textwrap.fill(str(x.__doc__).strip(), + subsequent_indent=' ', + initial_indent=' '), + ]) + description.append("\n\n") + subparsers = parser.add_subparsers( + title="Available commands", + help=argparse.SUPPRESS, + description="".join(description[:-1]), + ) + # Server-side commands. + for x in command_list_server: + x.add_parser(subparsers) + # Client-side commands. TODO one per .py file. + for x in command_list_client: + x(subparsers) + return parser diff --git a/openerpcommand/addons/bench_sale_mrp/__init__.py b/openerpcommand/addons/bench_sale_mrp/__init__.py new file mode 100644 index 00000000000..99ae05ee3a1 --- /dev/null +++ b/openerpcommand/addons/bench_sale_mrp/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# Nothing here, the module provides only data. +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerpcommand/addons/bench_sale_mrp/__openerp__.py b/openerpcommand/addons/bench_sale_mrp/__openerp__.py new file mode 100644 index 00000000000..3d8781905cc --- /dev/null +++ b/openerpcommand/addons/bench_sale_mrp/__openerp__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'bench_sale_mrp', + 'version': '0.1', + 'category': 'Benchmarks', + 'description': """Prepare some data to run a benchmark.""", + 'author': 'OpenERP SA', + 'maintainer': 'OpenERP SA', + 'website': 'http://www.openerp.com', + 'depends': ['base', 'sale_mrp'], + 'data': ['data.yml'], + 'installable': True, + 'auto_install': False, +} +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerpcommand/addons/bench_sale_mrp/data.yml b/openerpcommand/addons/bench_sale_mrp/data.yml new file mode 100644 index 00000000000..419492e60e1 --- /dev/null +++ b/openerpcommand/addons/bench_sale_mrp/data.yml @@ -0,0 +1,41 @@ +- + This is a subset of `sale_mrp/test/sale_mrp.yml`. +- + I define a product category `Mobile Products Sellable`. +- + !record {model: product.category, id: my_product_category_0}: + name: Mobile Products Sellable +- + I define a product `Slider Mobile` +- + !record {model: product.product, id: my_slider_mobile_0}: + categ_id: my_product_category_0 + cost_method: standard + list_price: 200.0 + mes_type: fixed + name: Slider Mobile + procure_method: make_to_order + seller_delay: '1' + seller_ids: + - delay: 1 + name: base.res_partner_agrolait + min_qty: 2.0 + qty: 5.0 + standard_price: 189.0 + supply_method: produce + type: product + uom_id: product.product_uom_unit + uom_po_id: product.product_uom_unit +- + I create a Bill of Material for the `Slider Mobile` product. +- + !record {model: mrp.bom, id: mrp_bom_slidermobile0}: + company_id: base.main_company + name: Slider Mobile + product_efficiency: 1.0 + product_id: my_slider_mobile_0 + product_qty: 1.0 + product_uom: product.product_uom_unit + product_uos_qty: 0.0 + sequence: 0.0 + type: normal diff --git a/openerpcommand/bench_sale_mrp.py b/openerpcommand/bench_sale_mrp.py new file mode 100644 index 00000000000..60e53037855 --- /dev/null +++ b/openerpcommand/bench_sale_mrp.py @@ -0,0 +1,68 @@ +""" +Benchmark based on the `sale_mrp` addons (in `sale_mrp/test/sale_mrp.yml`). +""" + +import time + +from .benchmarks import Bench + +class BenchSaleMrp(Bench): + """\ + Similar to `sale_mrp/test/sale_mrp.yml`. + + This benchmarks the OpenERP server `sale_mrp` module by creating and + confirming a sale order. As it creates data in the server, it is necessary + to ensure unique names for the newly created data. You can use the --seed + argument to give a lower bound to those names. (The number of generated + names is --jobs * --samples.) + """ + + command_name = 'bench-sale-mrp' + bench_name = '`sale_mrp/test/sale_mrp.yml`' + + def measure_once(self, i): + if self.worker >= 0: + i = int(self.args.seed) + i + (self.worker * int(self.args.samples)) + else: + i = int(self.args.seed) + i + + # Resolve a few external-ids (this has little impact on the running + # time of the whole method). + product_uom_unit = self.execute('ir.model.data', 'get_object_reference', 'product', 'product_uom_unit')[1] + my_slider_mobile_0 = self.execute('ir.model.data', 'get_object_reference', 'bench_sale_mrp', 'my_slider_mobile_0')[1] + res_partner_4 = self.execute('ir.model.data', 'get_object_reference', 'base', 'res_partner_4')[1] + res_partner_address_7 = self.execute('ir.model.data', 'get_object_reference', 'base', 'res_partner_address_7')[1] + list0 = self.execute('ir.model.data', 'get_object_reference', 'product', 'list0')[1] + shop = self.execute('ir.model.data', 'get_object_reference', 'sale', 'shop')[1] + + # Create a sale order for the product `Slider Mobile`. + data = { + 'client_order_ref': 'ref_xxx_' + str(i).rjust(6, '0'), + 'date_order': time.strftime('%Y-%m-%d'), + 'invoice_quantity': 'order', + 'name': 'sale_order_ref_xxx_' + str(i).rjust(6, '0'), + 'order_line': [(0, 0, { + 'name': 'Slider Mobile', + 'price_unit': 2, + 'product_uom': product_uom_unit, + 'product_uom_qty': 5.0, + 'state': 'draft', + 'delay': 7.0, + 'product_id': my_slider_mobile_0, + 'product_uos_qty': 5, + 'type': 'make_to_order', + })], + 'order_policy': 'manual', + 'partner_id': res_partner_4, + 'partner_invoice_id': res_partner_address_7, + 'partner_order_id': res_partner_address_7, + 'partner_shipping_id': res_partner_address_7, + 'picking_policy': 'direct', + 'pricelist_id': list0, + 'shop_id': shop, + } + sale_order_id = self.execute('sale.order', 'create', data, {}) + + # Confirm the sale order. + self.object_proxy.exec_workflow(self.database, self.uid, self.password, 'sale.order', 'order_confirm', sale_order_id, {}) + diff --git a/openerpcommand/benchmarks.py b/openerpcommand/benchmarks.py new file mode 100644 index 00000000000..401ce077b77 --- /dev/null +++ b/openerpcommand/benchmarks.py @@ -0,0 +1,166 @@ +""" +Define a base class for client-side benchmarking. +""" +import hashlib +import multiprocessing +import sys +import time + +from .client import Client + +class Bench(Client): + """ + Base class for concurrent benchmarks. The measure_once() method must be + overriden. + + Each sub-benchmark will be run in its own process then a report is done + with all the results (shared with the main process using a + `multiprocessing.Array`). + """ + + def __init__(self, subparsers=None): + super(Bench, self).__init__(subparsers) + self.parser.add_argument('-n', '--samples', metavar='INT', + default=100, help='number of measurements to take') + # TODO if -n s is given (instead of -n ), run the + # benchmark for seconds and return the number of iterations. + self.parser.add_argument('-o', '--output', metavar='PATH', + required=True, help='path to save the generated report') + self.parser.add_argument('--append', action='store_true', + default=False, help='append the report to an existing file') + self.parser.add_argument('-j', '--jobs', metavar='JOBS', + default=1, help='number of concurrent workers') + self.parser.add_argument('--seed', metavar='SEED', + default=0, help='a value to ensure different runs can create unique data') + self.worker = -1 + + def work(self, iarr=None): + if iarr: + # If an array is given, it means we are a worker process... + self.work_slave(iarr) + else: + # ... else we are the main process and we will spawn workers, + # passing them an array. + self.work_master() + + def work_master(self): + N = int(self.args.samples) + self.arrs = [(i, multiprocessing.Array('f', range(N))) + for i in xrange(int(self.args.jobs))] + ps = [multiprocessing.Process(target=self.run, args=(arr,)) + for arr in self.arrs] + [p.start() for p in ps] + [p.join() for p in ps] + + self.report_html() + + def work_slave(self, iarr): + j, arr = iarr + self.worker = j + N = int(self.args.samples) + total_t0 = time.time() + for i in xrange(N): + t0 = time.time() + self.measure_once(i) + t1 = time.time() + arr[i] = t1 - t0 + print >> sys.stdout, '\r%s' % ('|' * (i * 60 / N)), + print >> sys.stdout, '%s %s%%' % \ + (' ' * (60 - (i * 60 / N)), int(float(i+1)/N*100)), + sys.stdout.flush() + total_t1 = time.time() + print '\nDone in %ss.' % (total_t1 - total_t0) + + def report_html(self): + series = [] + for arr in self.arrs: + serie = """{ + data: %s, + points: { show: true } + }""" % ([[x, i] for i, x in enumerate(arr)],) + series.append(serie) + chart_id = hashlib.md5(" ".join(sys.argv)).hexdigest() + HEADER = """ +Benchmarks + + + +""" + + CONTENT = """

%s

+%s +
...
+""" % (self.bench_name, ' '.join(sys.argv), chart_id, chart_id, + ','.join(series)) + if self.args.append: + with open(self.args.output, 'a') as f: + f.write(CONTENT,) + else: + with open(self.args.output, 'w') as f: + f.write(HEADER + CONTENT,) + + def measure_once(self, i): + """ + The `measure_once` method is called --jobs times. A `i` argument is + supplied to allow to create unique values for each execution (e.g. to + supply fresh identifiers to a `create` method. + """ + pass + +class BenchRead(Bench): + """Read a record repeatedly.""" + + command_name = 'bench-read' + bench_name = 'res.users.read(1)' + + def __init__(self, subparsers=None): + super(BenchRead, self).__init__(subparsers) + self.parser.add_argument('-m', '--model', metavar='MODEL', + required=True, help='the model') + self.parser.add_argument('-i', '--id', metavar='RECORDID', + required=True, help='the record id') + + def measure_once(self, i): + self.execute(self.args.model, 'read', [self.args.id], []) + +class BenchFieldsViewGet(Bench): + """Read a record's fields and view architecture repeatedly.""" + + command_name = 'bench-view' + bench_name = 'res.users.fields_view_get(1)' + + def __init__(self, subparsers=None): + super(BenchFieldsViewGet, self).__init__(subparsers) + self.parser.add_argument('-m', '--model', metavar='MODEL', + required=True, help='the model') + self.parser.add_argument('-i', '--id', metavar='RECORDID', + required=True, help='the record id') + + def measure_once(self, i): + self.execute(self.args.model, 'fields_view_get', self.args.id) + +class BenchDummy(Bench): + """Dummy (call test.limits.model.consume_nothing()).""" + + command_name = 'bench-dummy' + bench_name = 'test.limits.model.consume_nothing()' + + def __init__(self, subparsers=None): + super(BenchDummy, self).__init__(subparsers) + self.parser.add_argument('-a', '--args', metavar='ARGS', + default='', help='some arguments to serialize') + + def measure_once(self, i): + self.execute('test.limits.model', 'consume_nothing') + +class BenchLogin(Bench): + """Login (update res_users.date).""" + + command_name = 'bench-login' + bench_name = 'res.users.login(1)' + + def measure_once(self, i): + self.common_proxy.login(self.database, self.user, self.password) diff --git a/openerpcommand/call.py b/openerpcommand/call.py new file mode 100644 index 00000000000..b4f9e95075a --- /dev/null +++ b/openerpcommand/call.py @@ -0,0 +1,44 @@ +""" +Call an arbitrary model's method. +""" +import ast +import os +import pprint +import sys +import time +import xmlrpclib + +import client + +class Call(client.Client): + """\ + Call an arbitrary model's method. + + Example: + > oe call res.users.read '[1, 3]' '[]' -u 1 -p admin + """ + # TODO The above docstring is completely borked in the + # --help message. + + command_name = 'call' + + def __init__(self, subparsers=None): + super(Call, self).__init__(subparsers) + self.parser.add_argument('call', metavar='MODEL.METHOD', + help='the model and the method to call, using the ' + '. format.') + self.parser.add_argument('args', metavar='ARGUMENT', + nargs='+', + help='the argument for the method call, must be ' + '`ast.literal_eval` compatible. Can be repeated.') + + def work(self): + try: + model, method = self.args.call.rsplit('.', 1) + except: + print "Invalid syntax `%s` must have the form .." + sys.exit(1) + args = tuple(map(ast.literal_eval, self.args.args)) if self.args.args else () + x = self.execute(model, method, *args) + pprint.pprint(x, indent=4) + diff --git a/openerpcommand/client.py b/openerpcommand/client.py new file mode 100644 index 00000000000..87586b68ddf --- /dev/null +++ b/openerpcommand/client.py @@ -0,0 +1,137 @@ +""" +Define a few common arguments for client-side command-line tools. +""" +import os +import sys +import time +import xmlrpclib + +import common + +class Client(common.Command): + """ + Base class for XML-RPC command-line clients. It must be inherited and the + work() method overriden. + """ + + def __init__(self, subparsers=None): + super(Client, self).__init__(subparsers) + required_or_default = common.required_or_default + self.parser.add_argument('-H', '--host', metavar='HOST', + **required_or_default('HOST', 'the server host')) + self.parser.add_argument('-P', '--port', metavar='PORT', + **required_or_default('PORT', 'the server port')) + + def execute(self, *args): + return self.object_proxy.execute(self.database, self.uid, self.password, *args) + + def initialize(self): + self.host = self.args.host + self.port = int(self.args.port) + self.database = self.args.database + self.user = self.args.user + self.password = self.args.password + + self.url = 'http://%s:%d/xmlrpc/' % (self.host, self.port) + self.common_proxy = xmlrpclib.ServerProxy(self.url + 'common') + self.object_proxy = xmlrpclib.ServerProxy(self.url + 'object') + + try: + self.uid = int(self.user) + except ValueError, e: + self.uid = self.common_proxy.login(self.database, self.user, self.password) + + def run(self, *args): + self.initialize() + self.work(*args) + + def work(self, *args): + pass + +class Open(Client): + """Get the web client's URL to view a specific model.""" + + command_name = 'open' + + def __init__(self, subparsers=None): + super(Open, self).__init__(subparsers) + self.parser.add_argument('-m', '--model', metavar='MODEL', + required=True, help='the view type') + self.parser.add_argument('-v', '--view-mode', metavar='VIEWMODE', + default='tree', help='the view mode') + + def work(self): + ids = self.execute('ir.actions.act_window', 'search', [ + ('res_model', '=', self.args.model), + ('view_mode', 'like', self.args.view_mode), + ]) + xs = self.execute('ir.actions.act_window', 'read', ids, []) + for x in xs: + print x['id'], x['name'] + d = {} + d['host'] = self.host + d['port'] = self.port + d['action_id'] = x['id'] + print " http://%(host)s:%(port)s/web/webclient/home#action_id=%(action_id)s" % d + +class Show(Client): + """Display a record.""" + + command_name = 'show' + + def __init__(self, subparsers=None): + super(Show, self).__init__(subparsers) + self.parser.add_argument('-m', '--model', metavar='MODEL', + required=True, help='the model') + self.parser.add_argument('-i', '--id', metavar='RECORDID', + required=True, help='the record id') + + def work(self): + xs = self.execute(self.args.model, 'read', [self.args.id], []) + if xs: + x = xs[0] + print x['name'] + else: + print "Record not found." + +class ConsumeNothing(Client): + """Call test.limits.model.consume_nothing().""" + + command_name = 'consume-nothing' + + def work(self): + xs = self.execute('test.limits.model', 'consume_nothing') + +class ConsumeMemory(Client): + """Call test.limits.model.consume_memory().""" + + command_name = 'consume-memory' + + def __init__(self, subparsers=None): + super(ConsumeMemory, self).__init__(subparsers) + self.parser.add_argument('--size', metavar='SIZE', + required=True, help='size of the list to allocate') + + def work(self): + xs = self.execute('test.limits.model', 'consume_memory', int(self.args.size)) + +class LeakMemory(ConsumeMemory): + """Call test.limits.model.leak_memory().""" + + command_name = 'leak-memory' + + def work(self): + xs = self.execute('test.limits.model', 'leak_memory', int(self.args.size)) + +class ConsumeCPU(Client): + """Call test.limits.model.consume_cpu_time().""" + + command_name = 'consume-cpu' + + def __init__(self, subparsers=None): + super(ConsumeCPU, self).__init__(subparsers) + self.parser.add_argument('--seconds', metavar='INT', + required=True, help='how much CPU time to consume') + + def work(self): + xs = self.execute('test.limits.model', 'consume_cpu_time', int(self.args.seconds)) diff --git a/openerpcommand/common.py b/openerpcommand/common.py new file mode 100644 index 00000000000..89898ff4c3c --- /dev/null +++ b/openerpcommand/common.py @@ -0,0 +1,88 @@ +""" +Define a few common arguments for server-side command-line tools. +""" +import argparse +import os +import sys + +def add_addons_argument(parser): + """ + Add a common --addons argument to a parser. + """ + parser.add_argument('--addons', metavar='ADDONS', + **required_or_default('ADDONS', + 'colon-separated list of paths to addons')) + +def get_addons_from_paths(paths, exclude): + """ + Build a list of available modules from a list of addons paths. + """ + exclude = exclude or [] + module_names = [] + for p in paths: + if os.path.exists(p): + names = list(set(os.listdir(p))) + names = filter(lambda a: not (a.startswith('.') or a in exclude), names) + module_names.extend(names) + else: + print "The addons path `%s` doesn't exist." % p + sys.exit(1) + return module_names + +def required_or_default(name, h): + """ + Helper to define `argparse` arguments. If the name is the environment, + the argument is optional and draw its value from the environment if not + supplied on the command-line. If it is not in the environment, make it + a mandatory argument. + """ + if os.environ.get('OPENERP_' + name.upper()): + d = {'default': os.environ['OPENERP_' + name.upper()]} + else: + d = {'required': True} + d['help'] = h + '. The environment variable OPENERP_' + \ + name.upper() + ' can be used instead.' + return d + +class Command(object): + """ + Base class to create command-line tools. It must be inherited and the + run() method overriden. + """ + + command_name = 'stand-alone' + + def __init__(self, subparsers=None): + if subparsers: + self.parser = parser = subparsers.add_parser(self.command_name, + description=self.__class__.__doc__) + else: + self.parser = parser = argparse.ArgumentParser( + description=self.__class__.__doc__) + + parser.add_argument('-d', '--database', metavar='DATABASE', + **required_or_default('DATABASE', 'the database to connect to')) + parser.add_argument('-u', '--user', metavar='USER', + **required_or_default('USER', 'the user login or ID. When using ' + 'RPC, providing an ID avoid the login() step')) + parser.add_argument('-p', '--password', metavar='PASSWORD', + **required_or_default('PASSWORD', 'the user password')) # TODO read it from the command line or from file. + + parser.set_defaults(run=self.run_with_args) + + def run_with_args(self, args): + self.args = args + self.run() + + def run(self): + print 'Stub Command.run().' + + @classmethod + def stand_alone(cls): + """ + A single Command object is a complete command-line program. See + `openerp-command/stand-alone` for an example. + """ + command = cls() + args = command.parser.parse_args() + args.run(args) diff --git a/openerpcommand/conf.py b/openerpcommand/conf.py new file mode 100644 index 00000000000..98237ea5ad8 --- /dev/null +++ b/openerpcommand/conf.py @@ -0,0 +1,25 @@ +""" +Display the currently used configuration. The configuration for any +sub-command is normally given by options. But some options can be specified +using environment variables. This sub-command shows those variables. +A `set` sub-command should be provided when the configuration is in a real +configuration file instead of environment variables. +""" +import os +import sys +import textwrap + +def run(args): + for x in ('database', 'addons', 'host', 'port'): + x_ = ('openerp_' + x).upper() + if x_ in os.environ: + print '%s: %s' % (x, os.environ[x_]) + else: + print '%s: ' % (x, ) + os.environ['OPENERP_DATABASE'] = 'yeah' + +def add_parser(subparsers): + parser = subparsers.add_parser('conf', + description='Display the currently used configuration.') + + parser.set_defaults(run=run) diff --git a/openerpcommand/drop.py b/openerpcommand/drop.py new file mode 100644 index 00000000000..aee8c2859f9 --- /dev/null +++ b/openerpcommand/drop.py @@ -0,0 +1,45 @@ +""" +Drop a database. +""" + +import common + +# TODO turn template1 in a parameter +# This should be exposed from openerp (currently in +# openerp/service/web_services.py). +def drop_database(database_name): + import openerp + db = openerp.sql_db.db_connect('template1') + cr = db.cursor() + cr.autocommit(True) # avoid transaction block + try: + # TODO option for doing this. + # Try to terminate all other connections that might prevent + # dropping the database + try: + cr.execute("""SELECT pg_terminate_backend(procpid) + FROM pg_stat_activity + WHERE datname = %s AND + procpid != pg_backend_pid()""", + (database_name,)) + except Exception: + pass + + try: + cr.execute('DROP DATABASE "%s"' % database_name) + except Exception, e: + print "Can't drop %s" % (database_name,) + finally: + cr.close() + +def run(args): + assert args.database + drop_database(args.database) + +def add_parser(subparsers): + parser = subparsers.add_parser('drop', + description='Drop a database.') + parser.add_argument('-d', '--database', metavar='DATABASE', + **common.required_or_default('DATABASE', 'the database to create')) + + parser.set_defaults(run=run) diff --git a/openerpcommand/initialize.py b/openerpcommand/initialize.py new file mode 100644 index 00000000000..3393c9a02d4 --- /dev/null +++ b/openerpcommand/initialize.py @@ -0,0 +1,103 @@ +""" +Install OpenERP on a new (by default) database. +""" +import os +import sys + +import common + +def install_openerp(database_name, create_database_flag, module_names, install_demo_data): + import openerp + config = openerp.tools.config + + if create_database_flag: + create_database(database_name) + + config['init'] = dict.fromkeys(module_names, 1) + + # Install the import hook, to import openerp.addons.. + openerp.modules.module.initialize_sys_path() + if hasattr(openerp.modules.loading, 'open_openerp_namespace'): + openerp.modules.loading.open_openerp_namespace() + + registry = openerp.modules.registry.RegistryManager.get( + database_name, update_module=True, force_demo=install_demo_data) + + return registry + +# TODO turn template1 in a parameter +# This should be exposed from openerp (currently in +# openerp/service/web_services.py). +def create_database(database_name): + import openerp + db = openerp.sql_db.db_connect('template1') + cr = db.cursor() # TODO `with db as cr:` + try: + cr.autocommit(True) + cr.execute("""CREATE DATABASE "%s" + ENCODING 'unicode' TEMPLATE "template1" """ \ + % (database_name,)) + finally: + cr.close() + +def run(args): + assert args.database + assert not (args.module and args.all_modules) + + import openerp + + config = openerp.tools.config + + if args.tests: + config['log_handler'] = [':TEST'] + config['test_enable'] = True + config['without_demo'] = False + else: + config['log_handler'] = [':CRITICAL'] + config['test_enable'] = False + config['without_demo'] = True + + if args.addons: + args.addons = args.addons.split(':') + else: + args.addons = [] + config['addons_path'] = ','.join(args.addons) + + if args.all_modules: + module_names = common.get_addons_from_paths(args.addons, args.exclude) + elif args.module: + module_names = args.module + else: + module_names = ['base'] + + openerp.netsvc.init_logger() + registry = install_openerp(args.database, not args.no_create, module_names, not config['without_demo']) + + # The `_assertion_report` attribute was added on the registry during the + # OpenERP 7.0 development. + if hasattr(registry, '_assertion_report'): + sys.exit(1 if registry._assertion_report.failures else 0) + +def add_parser(subparsers): + parser = subparsers.add_parser('initialize', + description='Create and initialize a new OpenERP database.') + parser.add_argument('-d', '--database', metavar='DATABASE', + **common.required_or_default('DATABASE', 'the database to create')) + common.add_addons_argument(parser) + parser.add_argument('--module', metavar='MODULE', action='append', + help='specify a module to install' + ' (this option can be repeated)') + parser.add_argument('--all-modules', action='store_true', + help='install all visible modules (not compatible with --module)') + parser.add_argument('--no-create', action='store_true', + help='do not create the database, only initialize it') + parser.add_argument('--exclude', metavar='MODULE', action='append', + help='exclude a module from installation' + ' (this option can be repeated)') + parser.add_argument('--tests', action='store_true', + help='run the tests as modules are installed' + ' (use the `run-tests` command to choose specific' + ' tests to run against an existing database).' + ' Demo data are installed.') + + parser.set_defaults(run=run) diff --git a/openerpcommand/main.py b/openerpcommand/main.py new file mode 100644 index 00000000000..f5a1b03f774 --- /dev/null +++ b/openerpcommand/main.py @@ -0,0 +1,7 @@ +import openerpcommand + +def run(): + """ Main entry point for the openerp-command tool.""" + parser = openerpcommand.main_parser() + args = parser.parse_args() + args.run(args) diff --git a/openerpcommand/model.py b/openerpcommand/model.py new file mode 100644 index 00000000000..26a499c647f --- /dev/null +++ b/openerpcommand/model.py @@ -0,0 +1,61 @@ +""" +Display information about a given model. +""" +import os +import sys +import textwrap + +def run(args): + assert args.database + assert args.model + import openerp + openerp.tools.config['log_level'] = 100 + openerp.netsvc.init_logger() + registry = openerp.modules.registry.RegistryManager.get( + args.database, update_module=False) + model = registry.get(args.model) + longest_k = 1 + longest_string = 1 + columns = model._columns + + if args.field and args.field not in columns: + print "No such field." + sys.exit(1) + + if args.field: + columns = { args.field: columns[args.field] } + else: + print "Fields (model `%s`, database `%s`):" % (args.model, args.database) + + for k, v in columns.items(): + longest_k = len(k) if longest_k < len(k) else longest_k + longest_string = len(v.string) \ + if longest_string < len(v.string) else longest_string + for k, v in sorted(columns.items()): + attr = [] + if v.required: + attr.append("Required") + if v.readonly: + attr.append("Read-only") + attr = '/'.join(attr) + attr = '(' + attr + ')' if attr else attr + if args.verbose: + print v.string, '-- ' + k + ', ' + v._type, attr + else: + print k.ljust(longest_k + 2), v._type, attr + if args.verbose and v.help: + print textwrap.fill(v.help, initial_indent=' ', subsequent_indent=' ') + +def add_parser(subparsers): + parser = subparsers.add_parser('model', + description='Display information about a given model for an existing database.') + parser.add_argument('-d', '--database', metavar='DATABASE', required=True, + help='the database to connect to') + parser.add_argument('-m', '--model', metavar='MODEL', required=True, + help='the model for which information should be displayed') + parser.add_argument('-v', '--verbose', action='store_true', + help='display more information') + parser.add_argument('-f', '--field', metavar='FIELD', + help='display information only for this particular field') + + parser.set_defaults(run=run) diff --git a/openerpcommand/module.py b/openerpcommand/module.py new file mode 100644 index 00000000000..550e0a2be28 --- /dev/null +++ b/openerpcommand/module.py @@ -0,0 +1,68 @@ +""" +Show module information for a given database or from the file-system. +""" +import os +import sys +import textwrap + +from . import common + +# TODO provide a --rpc flag to use XML-RPC (with a specific username) instead +# of server-side library. +def run(args): + assert args.database + import openerp + + config = openerp.tools.config + config['log_handler'] = [':CRITICAL'] + if args.addons: + args.addons = args.addons.split(':') + else: + args.addons = [] + config['addons_path'] = ','.join(args.addons) + openerp.netsvc.init_logger() + + if args.filesystem: + module_names = common.get_addons_from_paths(args.addons, []) + print "Modules (addons path %s):" % (', '.join(args.addons),) + for x in sorted(module_names): + print x + else: + registry = openerp.modules.registry.RegistryManager.get( + args.database, update_module=False) + + xs = [] + ir_module_module = registry.get('ir.module.module') + cr = registry.db.cursor() # TODO context manager + try: + ids = ir_module_module.search(cr, openerp.SUPERUSER_ID, [], {}) + xs = ir_module_module.read(cr, openerp.SUPERUSER_ID, ids, [], {}) + finally: + cr.close() + + if xs: + print "Modules (database `%s`):" % (args.database,) + for x in xs: + if args.short: + print '%3d %s' % (x['id'], x['name']) + else: + print '%3d %s %s' % (x['id'], x['name'], {'installed': '(installed)'}.get(x['state'], '')) + else: + print "No module found (database `%s`)." % (args.database,) + +def add_parser(subparsers): + parser = subparsers.add_parser('module', + description='Display modules known from a given database or on file-system.') + parser.add_argument('-d', '--database', metavar='DATABASE', + **common.required_or_default('DATABASE', 'the database to modify')) + common.add_addons_argument(parser) + parser.add_argument('-m', '--module', metavar='MODULE', required=False, + help='the module for which information should be shown') + parser.add_argument('-v', '--verbose', action='store_true', + help='display more information') + parser.add_argument('--short', action='store_true', + help='display less information') + parser.add_argument('-f', '--filesystem', action='store_true', + help='display module in the addons path, not in db') + + parser.set_defaults(run=run) diff --git a/openerpcommand/read.py b/openerpcommand/read.py new file mode 100644 index 00000000000..83d6a5340f8 --- /dev/null +++ b/openerpcommand/read.py @@ -0,0 +1,60 @@ +""" +Read a record. +""" +import os +import sys +import textwrap + +# TODO provide a --rpc flag to use XML-RPC (with a specific username) instead +# of server-side library. +def run(args): + assert args.database + assert args.model + import openerp + config = openerp.tools.config + config['log_handler'] = [':CRITICAL'] + openerp.netsvc.init_logger() + registry = openerp.modules.registry.RegistryManager.get( + args.database, update_module=False) + model = registry.get(args.model) + cr = registry.db.cursor() # TODO context manager + field_names = [args.field] if args.field else [] + if args.short: + # ignore --field + field_names = ['name'] + try: + xs = model.read(cr, 1, args.id, field_names, {}) + finally: + cr.close() + + if xs: + print "Records (model `%s`, database `%s`):" % (args.model, args.database) + x = xs[0] + if args.short: + print str(x['id']) + '.', x['name'] + else: + longest_k = 1 + for k, v in x.items(): + longest_k = len(k) if longest_k < len(k) else longest_k + for k, v in sorted(x.items()): + print (k + ':').ljust(longest_k + 2), v + else: + print "Record not found." + +def add_parser(subparsers): + parser = subparsers.add_parser('read', + description='Display a record.') + parser.add_argument('-d', '--database', metavar='DATABASE', required=True, + help='the database to connect to') + parser.add_argument('-m', '--model', metavar='MODEL', required=True, + help='the model for which a record should be read') + parser.add_argument('-i', '--id', metavar='RECORDID', required=True, + help='the record id') + parser.add_argument('-v', '--verbose', action='store_true', + help='display more information') + parser.add_argument('--short', action='store_true', + help='display less information') + parser.add_argument('-f', '--field', metavar='FIELD', + help='display information only for this particular field') + + parser.set_defaults(run=run) diff --git a/openerpcommand/run_tests.py b/openerpcommand/run_tests.py new file mode 100644 index 00000000000..80e968b6fae --- /dev/null +++ b/openerpcommand/run_tests.py @@ -0,0 +1,202 @@ +""" +Execute the unittest2 tests available in OpenERP addons. +""" + +import os +import sys +import types + +import common + +def get_test_modules(module, submodule, explode): + """ + Return a list of submodules containing tests. + `submodule` can be: + - None + - the name of a submodule + - '__fast_suite__' + - '__sanity_checks__' + """ + # Turn command-line module, submodule into importable names. + if module is None: + pass + elif module == 'openerp': + module = 'openerp.tests' + else: + module = 'openerp.addons.' + module + '.tests' + + # Try to import the module + try: + __import__(module) + except Exception, e: + if explode: + print 'Can not `import %s`.' % module + import logging + logging.exception('') + sys.exit(1) + else: + if str(e) == 'No module named tests': + # It seems the module has no `tests` sub-module, no problem. + pass + else: + print 'Can not `import %s`.' % module + return [] + + # Discover available test sub-modules. + m = sys.modules[module] + submodule_names = sorted([x for x in dir(m) \ + if x.startswith('test_') and \ + isinstance(getattr(m, x), types.ModuleType)]) + submodules = [getattr(m, x) for x in submodule_names] + + def show_submodules_and_exit(): + if submodule_names: + print 'Available submodules are:' + for x in submodule_names: + print ' ', x + sys.exit(1) + + if submodule is None: + # Use auto-discovered sub-modules. + ms = submodules + elif submodule == '__fast_suite__': + # Obtain the explicit test sub-modules list. + ms = getattr(sys.modules[module], 'fast_suite', None) + # `suite` was used before the 6.1 release instead of `fast_suite`. + ms = ms if ms else getattr(sys.modules[module], 'suite', None) + if ms is None: + if explode: + print 'The module `%s` has no defined test suite.' % (module,) + show_submodules_and_exit() + else: + ms = [] + elif submodule == '__sanity_checks__': + ms = getattr(sys.modules[module], 'checks', None) + if ms is None: + if explode: + print 'The module `%s` has no defined sanity checks.' % (module,) + show_submodules_and_exit() + else: + ms = [] + else: + # Pick the command-line-specified test sub-module. + m = getattr(sys.modules[module], submodule, None) + ms = [m] + + if m is None: + if explode: + print 'The module `%s` has no submodule named `%s`.' % \ + (module, submodule) + show_submodules_and_exit() + else: + ms = [] + + return ms + +def run(args): + import unittest2 + + import openerp + + config = openerp.tools.config + config['db_name'] = args.database + if args.port: + config['xmlrpc_port'] = int(args.port) + config['admin_passwd'] = 'admin' + config['db_password'] = 'a2aevl8w' # TODO from .openerpserverrc + config['addons_path'] = args.addons.replace(':',',') + if args.addons: + args.addons = args.addons.split(':') + else: + args.addons = [] + if args.sanity_checks and args.fast_suite: + print 'Only at most one of `--sanity-checks` and `--fast-suite` ' \ + 'can be specified.' + sys.exit(1) + + import logging + openerp.netsvc.init_alternative_logger() + logging.getLogger('openerp').setLevel(logging.CRITICAL) + + # Install the import hook, to import openerp.addons.. + openerp.modules.module.initialize_sys_path() + openerp.modules.loading.open_openerp_namespace() + + # Extract module, submodule from the command-line args. + if args.module is None: + module, submodule = None, None + else: + splitted = args.module.split('.') + if len(splitted) == 1: + module, submodule = splitted[0], None + elif len(splitted) == 2: + module, submodule = splitted + else: + print 'The `module` argument must have the form ' \ + '`module[.submodule]`.' + sys.exit(1) + + # Import the necessary modules and get the corresponding suite. + if module is None: + # TODO + modules = common.get_addons_from_paths(args.addons, []) # TODO openerp.addons.base is not included ? + test_modules = [] + for module in ['openerp'] + modules: + if args.fast_suite: + submodule = '__fast_suite__' + if args.sanity_checks: + submodule = '__sanity_checks__' + test_modules.extend(get_test_modules(module, + submodule, explode=False)) + else: + if submodule and args.fast_suite: + print "Submodule name `%s` given, ignoring `--fast-suite`." % (submodule,) + if submodule and args.sanity_checks: + print "Submodule name `%s` given, ignoring `--sanity-checks`." % (submodule,) + if not submodule and args.fast_suite: + submodule = '__fast_suite__' + if not submodule and args.sanity_checks: + submodule = '__sanity_checks__' + test_modules = get_test_modules(module, + submodule, explode=True) + + # Run the test suite. + if not args.dry_run: + suite = unittest2.TestSuite() + for test_module in test_modules: + suite.addTests(unittest2.TestLoader().loadTestsFromModule(test_module)) + r = unittest2.TextTestRunner(verbosity=2).run(suite) + if r.errors or r.failures: + sys.exit(1) + else: + print 'Test modules:' + for test_module in test_modules: + print ' ', test_module.__name__ + +def add_parser(subparsers): + parser = subparsers.add_parser('run-tests', + description='Run the OpenERP server and/or addons tests.') + parser.add_argument('-d', '--database', metavar='DATABASE', required=True, + help='the database to test. Depending on the test suites, the ' + 'database must already exist or not.') + parser.add_argument('-p', '--port', metavar='PORT', + help='the port used for WML-RPC tests') + common.add_addons_argument(parser) + parser.add_argument('-m', '--module', metavar='MODULE', + default=None, + help='the module to test in `module[.submodule]` notation. ' + 'Use `openerp` for the core OpenERP tests. ' + 'Leave empty to run every declared tests. ' + 'Give a module but no submodule to run all the module\'s declared ' + 'tests. If both the module and the submodule are given, ' + 'the sub-module can be run even if it is not declared in the module.') + parser.add_argument('--fast-suite', action='store_true', + help='run only the tests explicitely declared in the fast suite (this ' + 'makes sense only with the bare `module` notation or no module at ' + 'all).') + parser.add_argument('--sanity-checks', action='store_true', + help='run only the sanity check tests') + parser.add_argument('--dry-run', action='store_true', + help='do not run the tests') + + parser.set_defaults(run=run) diff --git a/openerpcommand/scaffold.py b/openerpcommand/scaffold.py new file mode 100644 index 00000000000..e590653c91b --- /dev/null +++ b/openerpcommand/scaffold.py @@ -0,0 +1,77 @@ +""" +Generate an OpenERP module skeleton. +""" + +import os +import sys + +def run(args): + assert args.module + module = args.module + + if os.path.exists(module): + print "The path `%s` already exists." + sys.exit(1) + + os.mkdir(module) + os.mkdir(os.path.join(module, 'models')) + with open(os.path.join(module, '__openerp__.py'), 'w') as h: + h.write(MANIFEST) + with open(os.path.join(module, '__init__.py'), 'w') as h: + h.write(INIT_PY) + with open(os.path.join(module, 'models', '__init__.py'), 'w') as h: + h.write(MODELS_PY % (module,)) + +def add_parser(subparsers): + parser = subparsers.add_parser('scaffold', + description='Generate an OpenERP module skeleton.') + parser.add_argument('module', metavar='MODULE', + help='the name of the generated module') + + parser.set_defaults(run=run) + +MANIFEST = """\ +# -*- coding: utf-8 -*- +{ + 'name': '', + 'version': '0.0', + 'category': '', + 'description': ''' + +''', + 'author': '', + 'maintainer': '', + 'website': 'http://', + # Add any module that are necessary for this module to correctly work in + # the `depends` list. + 'depends': ['base'], + 'data': [ + ], + 'test': [ + ], + # Set to False if you want to prevent the module to be known by OpenERP + # (and thus appearing in the list of modules). + 'installable': True, + # Set to True if you want the module to be automatically whenever all its + # dependencies are installed. + 'auto_install': False, +} +""" + +INIT_PY = """\ +# -*- coding: utf-8 -*- +import models +""" + +MODELS_PY = """\ +# -*- coding: utf-8 -*- +import openerp + +# Define a new model. +class my_model(openerp.osv.osv.Model): + + _name = '%s.my_model' + + _columns = { + } +""" diff --git a/openerpcommand/uninstall.py b/openerpcommand/uninstall.py new file mode 100644 index 00000000000..9d5d93bda05 --- /dev/null +++ b/openerpcommand/uninstall.py @@ -0,0 +1,67 @@ +""" +Install OpenERP on a new (by default) database. +""" +import os +import sys + +import common + +# TODO turn template1 in a parameter +# This should be exposed from openerp (currently in +# openerp/service/web_services.py). +def create_database(database_name): + import openerp + db = openerp.sql_db.db_connect('template1') + cr = db.cursor() # TODO `with db as cr:` + try: + cr.autocommit(True) + cr.execute("""CREATE DATABASE "%s" + ENCODING 'unicode' TEMPLATE "template1" """ \ + % (database_name,)) + finally: + cr.close() + +def run(args): + assert args.database + assert args.module + + import openerp + + config = openerp.tools.config + config['log_handler'] = [':CRITICAL'] + if args.addons: + args.addons = args.addons.split(':') + else: + args.addons = [] + config['addons_path'] = ','.join(args.addons) + openerp.netsvc.init_logger() + + # Install the import hook, to import openerp.addons.. + openerp.modules.module.initialize_sys_path() + openerp.modules.loading.open_openerp_namespace() + + registry = openerp.modules.registry.RegistryManager.get( + args.database, update_module=False) + + ir_module_module = registry.get('ir.module.module') + cr = registry.db.cursor() # TODO context manager + try: + ids = ir_module_module.search(cr, openerp.SUPERUSER_ID, [('name', 'in', args.module), ('state', '=', 'installed')], {}) + if len(ids) == len(args.module): + ir_module_module.button_immediate_uninstall(cr, openerp.SUPERUSER_ID, ids, {}) + else: + print "At least one module not found (database `%s`)." % (args.database,) + finally: + cr.close() + +def add_parser(subparsers): + parser = subparsers.add_parser('uninstall', + description='Uninstall some modules from an OpenERP database.') + parser.add_argument('-d', '--database', metavar='DATABASE', + **common.required_or_default('DATABASE', 'the database to modify')) + common.add_addons_argument(parser) + parser.add_argument('--module', metavar='MODULE', action='append', + help='specify a module to uninstall' + ' (this option can be repeated)') + + parser.set_defaults(run=run) diff --git a/openerpcommand/update.py b/openerpcommand/update.py new file mode 100644 index 00000000000..66a47dc5692 --- /dev/null +++ b/openerpcommand/update.py @@ -0,0 +1,19 @@ +""" +Update an existing OpenERP database. +""" + +def run(args): + assert args.database + import openerp + config = openerp.tools.config + config['update']['all'] = 1 + openerp.modules.registry.RegistryManager.get( + args.database, update_module=True) + +def add_parser(subparsers): + parser = subparsers.add_parser('update', + description='Update an existing OpenERP database.') + parser.add_argument('-d', '--database', metavar='DATABASE', required=True, + help='the database to update') + + parser.set_defaults(run=run)