From f9e24e1918b895fc49a6954b774d2dec30126b4a Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 29 Jun 2014 14:42:27 +0200 Subject: [PATCH 01/13] [IMP] module loading and tests profiling Add a loglevel 25 to log some INFO to runbot Add timers for loading and testing Silence empty test suite ir_logging use relative pathname, warning this doesnt work for --addons-path modules yet. --- openerp/modules/loading.py | 15 +++++++++++++-- openerp/modules/module.py | 15 +++++++++------ openerp/netsvc.py | 11 ++++++++--- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index b8d52416514..1a76f943cc8 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -29,6 +29,7 @@ import logging import os import sys import threading +import time import openerp import openerp.modules.db @@ -110,7 +111,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= if kind in ('demo', 'test'): threading.currentThread().testing = True for filename in _get_files_of_kind(kind): - _logger.info("module %s: loading %s", module_name, filename) + _logger.info("loading %s/%s", module_name, filename) noupdate = False if kind in ('demo', 'demo_xml') or (filename.endswith('.csv') and kind in ('init', 'init_xml')): noupdate = True @@ -137,6 +138,8 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= registry.fields_by_model.setdefault(field['model'], []).append(field) # register, instantiate and initialize models for each modules + ta0 = time.time() + for index, package in enumerate(graph): module_name = package.name module_id = package.id @@ -144,7 +147,8 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= if skip_modules and module_name in skip_modules: continue - _logger.debug('module %s: loading objects', package.name) + tm0 = time.time() + migrations.migrate_module(package, 'pre') load_openerp_module(package.name) @@ -222,9 +226,13 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= if hasattr(package, kind): delattr(package, kind) + #_logger.log(25, "%s loaded in %.2fs", package.name, time.time() - tm0) + registry._init_modules.add(package.name) cr.commit() + _logger.log(25, "%s modules loaded in %.2fs", len(graph), time.time() - ta0) + # The query won't be valid for models created later (i.e. custom model # created after the registry has been loaded), so empty its result. registry.fields_by_model = None @@ -451,10 +459,13 @@ def load_modules(db, force_demo=False, status=None, update_module=False): # STEP 9: Run the post-install tests cr.commit() + + ta0 = time.time() if openerp.tools.config['test_enable']: cr.execute("SELECT name FROM ir_module_module WHERE state='installed'") for module_name in cr.fetchall(): report.record_result(openerp.modules.module.run_unit_tests(module_name[0], cr.dbname, position=runs_post_install)) + _logger.log(25, "All post-tested in %.2fs", time.time() - ta0) finally: cr.close() diff --git a/openerp/modules/module.py b/openerp/modules/module.py index cd21e681a8b..18a0cdbf727 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -27,6 +27,7 @@ import logging import os import re import sys +import time import unittest from os.path import join as opj @@ -422,14 +423,16 @@ def run_unit_tests(module_name, dbname, position=runs_at_install): for m in mods: tests = unwrap_suite(unittest2.TestLoader().loadTestsFromModule(m)) suite = unittest2.TestSuite(itertools.ifilter(position, tests)) - _logger.info('running %s tests.', m.__name__) - result = unittest2.TextTestRunner(verbosity=2, stream=TestStream(m.__name__)).run(suite) + if suite.countTestCases(): + tm0 = time.time() + _logger.info('%s running tests.', m.__name__) + result = unittest2.TextTestRunner(verbosity=2, stream=TestStream(m.__name__)).run(suite) + _logger.log(25, "%s tested in %.2fs", m.__name__, time.time() - tm0) + if not result.wasSuccessful(): + r = False + _logger.error("Module %s: %d failures, %d errors", module_name, len(result.failures), len(result.errors)) - if not result.wasSuccessful(): - r = False - _logger.error("Module %s: %d failures, %d errors", - module_name, len(result.failures), len(result.errors)) current_test = None return r diff --git a/openerp/netsvc.py b/openerp/netsvc.py index e6c05bf7298..856faa8f61d 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -68,6 +68,8 @@ def LocalService(name): with registry.cursor() as cr: return registry['ir.actions.report.xml']._lookup_report(cr, name[len('report.'):]) +path_prefix = os.path.realpath(os.path.dirname(os.path.dirname(__file__))) + class PostgreSQLHandler(logging.Handler): """ PostgreSQL Loggin Handler will store logs in the database, by default the current database, can be set using --log-db=DBNAME @@ -87,7 +89,8 @@ class PostgreSQLHandler(logging.Handler): msg = "%s\n%s" % (msg, traceback) # we do not use record.levelname because it may have been changed by ColoredFormatter. levelname = logging.getLevelName(record.levelno) - val = ('server', ct_db, record.name, levelname, msg, record.pathname, record.lineno, record.funcName) + + val = ('server', ct_db, record.name, levelname, msg, record.pathname[len(path_prefix)+1:], record.lineno, record.funcName) cr.execute(""" INSERT INTO ir_logging(create_date, type, dbname, name, level, message, path, line, func) VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s) @@ -116,7 +119,7 @@ class DBFormatter(logging.Formatter): class ColoredFormatter(DBFormatter): def format(self, record): - fg_color, bg_color = LEVEL_COLOR_MAPPING[record.levelno] + fg_color, bg_color = LEVEL_COLOR_MAPPING.get(record.levelno, (GREEN, DEFAULT)) record.levelname = COLOR_PATTERN % (30 + fg_color, 40 + bg_color, record.levelname) return DBFormatter.format(self, record) @@ -127,6 +130,8 @@ def init_logger(): return _logger_init = True + logging.addLevelName(25, "INFO") + from tools.translate import resetlocale resetlocale() @@ -180,7 +185,7 @@ def init_logger(): if tools.config['log_db']: postgresqlHandler = PostgreSQLHandler() - postgresqlHandler.setLevel(logging.WARNING) + postgresqlHandler.setLevel(25) logging.getLogger().addHandler(postgresqlHandler) # Configure loggers levels From 78e044b5c71e4d593679162720c43724d5454607 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 29 Jun 2014 16:32:26 +0200 Subject: [PATCH 02/13] [IMP] test impprove HTTPCase opener Simplify website crawler using the generic HTTPCase opener --- addons/website/tests/__init__.py | 2 +- addons/website/tests/cases.py | 35 ------ addons/website/tests/test_crawl.py | 84 ++++++++++++++ addons/website/tests/test_requests.py | 155 -------------------------- openerp/modules/loading.py | 4 +- openerp/modules/module.py | 3 +- openerp/tests/common.py | 37 +++++- 7 files changed, 123 insertions(+), 197 deletions(-) delete mode 100644 addons/website/tests/cases.py create mode 100644 addons/website/tests/test_crawl.py delete mode 100644 addons/website/tests/test_requests.py diff --git a/addons/website/tests/__init__.py b/addons/website/tests/__init__.py index b7eb1831806..468a16017e8 100644 --- a/addons/website/tests/__init__.py +++ b/addons/website/tests/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- import test_converter -import test_requests +import test_crawl import test_ui import test_views diff --git a/addons/website/tests/cases.py b/addons/website/tests/cases.py deleted file mode 100644 index 3e777293ff9..00000000000 --- a/addons/website/tests/cases.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -import unittest2 - -class URLCase(unittest2.TestCase): - """ - URLCase moved out of test_requests, otherwise discovery attempts to - instantiate and run it - """ - def __init__(self, user, url, source, result): - super(URLCase, self).__init__() - self.user = user - self.url = url - self.source = source - self.result = result - - @property - def username(self): - return self.user or "Anonymous Coward" - - def __str__(self): - if self.source: - return "%s (from %s, as %s)" % (self.url, self.source, self.username) - return "%s (as %s)" % (self.url, self.username) - - __repr__ = __str__ - - def shortDescription(self): - return "" - - def runTest(self): - code = self.result.getcode() - self.assertIn( - code, xrange(200, 300), - "Fetching %s as %s returned an error response (%d)" % ( - self.url, self.username, code)) diff --git a/addons/website/tests/test_crawl.py b/addons/website/tests/test_crawl.py new file mode 100644 index 00000000000..2fdb8c668b8 --- /dev/null +++ b/addons/website/tests/test_crawl.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +import logging +import urlparse +import unittest2 +import urllib2 +import time +import werkzeug.urls + +import lxml.html + +import openerp +from openerp import tools + +_logger = logging.getLogger(__name__) + +class Crawler(openerp.tests.HttpCase): + """ Test suite crawling an openerp CMS instance and checking that all + internal links lead to a 200 response. + + If a username and a password are provided, authenticates the user before + starting the crawl + """ + + at_install = False + post_install = True + + def crawl(self, url, seen=None, msg=''): + if seen == None: + seen = set() + if url in seen: + return seen + else: + seen.add(url) + + _logger.info("%s %s", msg, url) + r = self.url_open(url) + code = r.getcode() + self.assertIn( code, xrange(200, 300), "%s Fetching %s returned error response (%d)" % (msg, url, code)) + + if r.info().gettype() == 'text/html': + doc = lxml.html.fromstring(r.read()) + for link in doc.xpath('//a[@href]'): + href = link.get('href') + + parts = urlparse.urlsplit(href) + # href with any fragment removed + href = urlparse.urlunsplit(( + parts.scheme, + parts.netloc, + parts.path, + parts.query, + '' + )) + + # FIXME: handle relative link (not parts.path.startswith /) + if parts.netloc or \ + not parts.path.startswith('/') or \ + parts.path == '/web' or\ + parts.path.startswith('/web/') or \ + parts.path.startswith('/en_US/') or \ + (parts.scheme and parts.scheme not in ('http', 'https')): + continue + + self.crawl(href, seen, msg) + return seen + + + def test_10_crawl_public(self): + t0 = time.time() + seen = self.crawl('/', msg='Anonymous Coward') + _logger.log(25, "public crawled %s urls in %.2fs", len(seen) ,time.time() - t0) + + def test_20_crawl_demo(self): + t0 = time.time() + self.authenticate('demo', 'demo') + seen = self.crawl('/', msg='demo') + _logger.log(25, "demo crawled %s urls in %.2fs", len(seen), time.time() - t0) + + def test_30_crawl_admin(self): + t0 = time.time() + self.authenticate('admin', 'admin') + seen = self.crawl('/', msg='admin') + _logger.log(25, "admin crawled %s urls in %.2fs", len(seen), time.time() - t0) + diff --git a/addons/website/tests/test_requests.py b/addons/website/tests/test_requests.py deleted file mode 100644 index 2a1eea4c877..00000000000 --- a/addons/website/tests/test_requests.py +++ /dev/null @@ -1,155 +0,0 @@ -# -*- coding: utf-8 -*- -import urlparse -import unittest2 -import urllib2 -import werkzeug.urls - -import lxml.html - -import openerp -from openerp import tools - -import cases - -__all__ = ['load_tests', 'CrawlSuite'] - -class RedirectHandler(urllib2.HTTPRedirectHandler): - """ - HTTPRedirectHandler is predicated upon HTTPErrorProcessor being used and - works by intercepting 3xy "errors". - - Inherit from it to handle 3xy non-error responses instead, as we're not - using the error processor - """ - - def http_response(self, request, response): - code, msg, hdrs = response.code, response.msg, response.info() - - if 300 <= code < 400: - return self.parent.error( - 'http', request, response, code, msg, hdrs) - - return response - - https_response = http_response - -class CrawlSuite(unittest2.TestSuite): - """ Test suite crawling an openerp CMS instance and checking that all - internal links lead to a 200 response. - - If a username and a password are provided, authenticates the user before - starting the crawl - """ - - at_install = False - post_install = True - - def __init__(self, user=None, password=None): - super(CrawlSuite, self).__init__() - - registry = openerp.registry(tools.config['db_name']) - try: - # switch registry to test mode, so that requests can be made - registry.enter_test_mode() - - self.opener = urllib2.OpenerDirector() - self.opener.add_handler(urllib2.UnknownHandler()) - self.opener.add_handler(urllib2.HTTPHandler()) - self.opener.add_handler(urllib2.HTTPSHandler()) - self.opener.add_handler(urllib2.HTTPCookieProcessor()) - self.opener.add_handler(RedirectHandler()) - - self._authenticate(user, password) - self.user = user - - finally: - registry.leave_test_mode() - - def _request(self, path): - return self.opener.open(urlparse.urlunsplit([ - 'http', 'localhost:%s' % tools.config['xmlrpc_port'], - path, '', '' - ])) - - def _authenticate(self, user, password): - # force tools.config['db_name'] in user session so opening `/` doesn't - # blow up in multidb situations - self.opener.open('http://localhost:{port}/web/?db={db}'.format( - port=tools.config['xmlrpc_port'], - db=werkzeug.urls.url_quote_plus(tools.config['db_name']), - )) - if user is not None: - url = 'http://localhost:{port}/login?{query}'.format( - port=tools.config['xmlrpc_port'], - query=werkzeug.urls.url_encode({ - 'db': tools.config['db_name'], - 'login': user, - 'key': password, - }) - ) - auth = self.opener.open(url) - assert auth.getcode() < 400, "Auth failure %d" % auth.getcode() - - def _wrapped_run(self, result, debug=False): - registry = openerp.registry(tools.config['db_name']) - try: - # switch registry to test mode, so that requests can be made - registry.enter_test_mode() - - paths = [URL('/')] - seen = set(paths) - - while paths: - url = paths.pop(0) - r = self._request(url.url) - url.to_case(self.user, r).run(result) - - if r.info().gettype() != 'text/html': - continue - - doc = lxml.html.fromstring(r.read()) - for link in doc.xpath('//a[@href]'): - href = link.get('href') - - parts = urlparse.urlsplit(href) - # href with any fragment removed - href = urlparse.urlunsplit(( - parts.scheme, - parts.netloc, - parts.path, - parts.query, - '' - )) - - # avoid repeats, even for links we won't crawl no need to - # bother splitting them if we've already ignored them - # previously - if href in seen: continue - seen.add(href) - - # FIXME: handle relative link (not parts.path.startswith /) - if parts.netloc or \ - not parts.path.startswith('/') or \ - parts.path == '/web' or\ - parts.path.startswith('/web/') or \ - (parts.scheme and parts.scheme not in ('http', 'https')): - continue - - paths.append(URL(href, url.url)) - - finally: - registry.leave_test_mode() - -class URL(object): - def __init__(self, url, source=None): - self.url = url - self.source = source - - def to_case(self, user, result): - return cases.URLCase(user, self.url, self.source, result) - -def load_tests(loader, base, _): - base.addTest(CrawlSuite()) - base.addTest(CrawlSuite('admin', 'admin')) - base.addTest(CrawlSuite('demo', 'demo')) - return base diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index 1a76f943cc8..b9b421a20ca 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -231,12 +231,12 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= registry._init_modules.add(package.name) cr.commit() - _logger.log(25, "%s modules loaded in %.2fs", len(graph), time.time() - ta0) + _logger.log(25, "%s modules loaded in %.2fs", len(graph), time.time() - ta0) # The query won't be valid for models created later (i.e. custom model # created after the registry has been loaded), so empty its result. registry.fields_by_model = None - + cr.commit() return loaded_modules, processed_modules diff --git a/openerp/modules/module.py b/openerp/modules/module.py index 18a0cdbf727..66119ce31f1 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -428,7 +428,8 @@ def run_unit_tests(module_name, dbname, position=runs_at_install): tm0 = time.time() _logger.info('%s running tests.', m.__name__) result = unittest2.TextTestRunner(verbosity=2, stream=TestStream(m.__name__)).run(suite) - _logger.log(25, "%s tested in %.2fs", m.__name__, time.time() - tm0) + if time.time() - tm0 > 5: + _logger.log(25, "%s tested in %.2fs", m.__name__, time.time() - tm0) if not result.wasSuccessful(): r = False _logger.error("Module %s: %d failures, %d errors", module_name, len(result.failures), len(result.errors)) diff --git a/openerp/tests/common.py b/openerp/tests/common.py index 658ef89423d..4d4417ee781 100644 --- a/openerp/tests/common.py +++ b/openerp/tests/common.py @@ -130,6 +130,25 @@ class SingleTransactionCase(BaseCase): cls.cr.rollback() cls.cr.close() +class RedirectHandler(urllib2.HTTPRedirectHandler): + """ + HTTPRedirectHandler is predicated upon HTTPErrorProcessor being used and + works by intercepting 3xy "errors". + + Inherit from it to handle 3xy non-error responses instead, as we're not + using the error processor + """ + + def http_response(self, request, response): + code, msg, hdrs = response.code, response.msg, response.info() + + if 300 <= code < 400: + return self.parent.error( + 'http', request, response, code, msg, hdrs) + + return response + + https_response = http_response class HttpCase(TransactionCase): """ Transactionnal HTTP TestCase with url_open and phantomjs helpers. @@ -152,6 +171,14 @@ class HttpCase(TransactionCase): self.session.db = DB openerp.http.root.session_store.save(self.session) self.localstorage_path = mkdtemp() + # setup an url opener helper + self.opener = urllib2.OpenerDirector() + self.opener.add_handler(urllib2.UnknownHandler()) + self.opener.add_handler(urllib2.HTTPHandler()) + self.opener.add_handler(urllib2.HTTPSHandler()) + self.opener.add_handler(urllib2.HTTPCookieProcessor()) + self.opener.add_handler(RedirectHandler()) + self.opener.addheaders.append(('Cookie', 'session_id=%s' % self.session_id)) def tearDown(self): rmtree(self.localstorage_path) @@ -159,11 +186,15 @@ class HttpCase(TransactionCase): super(HttpCase, self).tearDown() def url_open(self, url, data=None, timeout=10): - opener = urllib2.build_opener() - opener.addheaders.append(('Cookie', 'session_id=%s' % self.session_id)) if url.startswith('/'): url = "http://localhost:%s%s" % (PORT, url) - return opener.open(url, data, timeout) + return self.opener.open(url, data, timeout) + + def authenticate(self, user, password): + if user is not None: + url = '/login?%s' % werkzeug.urls.url_encode({'db': DB,'login': user, 'key': password}) + auth = self.url_open(url) + assert auth.getcode() < 400, "Auth failure %d" % auth.getcode() def phantom_poll(self, phantom, timeout): """ Phantomjs Test protocol. From e634ec806f0bab6e0e0be332ed10a9f34f466190 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 29 Jun 2014 21:37:00 +0200 Subject: [PATCH 03/13] [IMP] website crawler log show query/s --- addons/website/tests/test_crawl.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/addons/website/tests/test_crawl.py b/addons/website/tests/test_crawl.py index 2fdb8c668b8..2d68410f6f9 100644 --- a/addons/website/tests/test_crawl.py +++ b/addons/website/tests/test_crawl.py @@ -68,17 +68,23 @@ class Crawler(openerp.tests.HttpCase): def test_10_crawl_public(self): t0 = time.time() seen = self.crawl('/', msg='Anonymous Coward') - _logger.log(25, "public crawled %s urls in %.2fs", len(seen) ,time.time() - t0) + count = len(seen) + duration = time.time() - t0 + _logger.log(25, "public crawled %s urls in %.2fs, %.3fs per query", count, duration, duration/count) def test_20_crawl_demo(self): t0 = time.time() self.authenticate('demo', 'demo') seen = self.crawl('/', msg='demo') - _logger.log(25, "demo crawled %s urls in %.2fs", len(seen), time.time() - t0) + count = len(seen) + duration = time.time() - t0 + _logger.log(25, "demo crawled %s urls in %.2fs, %.3fs per query", count, duration, duration/count) def test_30_crawl_admin(self): t0 = time.time() self.authenticate('admin', 'admin') seen = self.crawl('/', msg='admin') - _logger.log(25, "admin crawled %s urls in %.2fs", len(seen), time.time() - t0) + count = len(seen) + duration = time.time() - t0 + _logger.log(25, "admin crawled %s urls in %.2fs, %.3fs per query", count, duration, duration/count) From 5209fbc7ed9fcad966ab064654a8a8697142be42 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 30 Jun 2014 01:51:40 +0200 Subject: [PATCH 04/13] [REM] useless icon send a message The action is available from the wall. --- addons/mail/static/src/js/mail.js | 45 ----------------------------- addons/mail/static/src/xml/mail.xml | 12 -------- 2 files changed, 57 deletions(-) diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index dbe7d54aade..b3fb0dab18f 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -1984,51 +1984,6 @@ openerp.mail = function (session) { } }); - - /** - * ------------------------------------------------------------ - * UserMenu - * ------------------------------------------------------------ - * - * Add a link on the top user bar for write a full mail - */ - session.web.ComposeMessageTopButton = session.web.Widget.extend({ - template:'mail.ComposeMessageTopButton', - - start: function () { - this.$el.on('click', this.on_compose_message); - this._super(); - }, - - on_compose_message: function (event) { - event.preventDefault(); - event.stopPropagation(); - var action = { - type: 'ir.actions.act_window', - res_model: 'mail.compose.message', - view_mode: 'form', - view_type: 'form', - views: [[false, 'form']], - target: 'new', - context: {}, - }; - session.client.action_manager.do_action(action); - }, - }); - - session.web.UserMenu.include({ - do_update: function(){ - var self = this; - this._super.apply(this, arguments); - this.update_promise.then(function() { - var mail_button = new session.web.ComposeMessageTopButton(); - mail_button.appendTo(session.webclient.$el.parents().find('.oe_systray')); - openerp.web.bus.trigger('resize'); // Re-trigger the reflow logic - }); - }, - }); - - /** * ------------------------------------------------------------ * Sub-widgets loading diff --git a/addons/mail/static/src/xml/mail.xml b/addons/mail/static/src/xml/mail.xml index c79b95224b1..86041cfcdae 100644 --- a/addons/mail/static/src/xml/mail.xml +++ b/addons/mail/static/src/xml/mail.xml @@ -335,18 +335,6 @@ - - -
  • - - - -
  • -
    - From 22f4c315a3ee4916d476fceea2a6d5a5248c8a2d Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 29 Jun 2014 23:31:16 +0200 Subject: [PATCH 05/13] [IMP] automatic fiscal positions for simple cases Add group of countries res.country.group Add get_fiscal_position method a method to compute a fiscal position based on company_id, partner_id, delivery_id The meaning of res.partner.fiscal_position is now a forced a fiscal position. The default implementation should handle simple cases, like VAT in UE and sales tax in the US, but the method can be overriden to handle more complex ficals rules. --- addons/account/partner.py | 34 +++++++++++++++++++ addons/account/partner_view.xml | 8 +++++ addons/base_vat/base_vat.py | 4 --- addons/sale/sale.py | 23 ++++++++++--- addons/sale/sale_view.xml | 2 +- addons/website_sale/controllers/main.py | 7 +++- addons/website_sale/models/sale_order.py | 24 +++++++------ openerp/addons/base/res/res_country.py | 8 +++++ openerp/addons/base/res/res_country_data.xml | 13 +++++++ openerp/addons/base/res/res_country_view.xml | 32 +++++++++++++++++ .../addons/base/security/ir.model.access.csv | 2 ++ 11 files changed, 135 insertions(+), 22 deletions(-) diff --git a/addons/account/partner.py b/addons/account/partner.py index d9bb6083f43..70f5280b861 100644 --- a/addons/account/partner.py +++ b/addons/account/partner.py @@ -27,13 +27,19 @@ from openerp.osv import fields, osv class account_fiscal_position(osv.osv): _name = 'account.fiscal.position' _description = 'Fiscal Position' + _order = 'sequence' _columns = { + 'sequence': fields.integer('Sequence'), 'name': fields.char('Fiscal Position', required=True), 'active': fields.boolean('Active', help="By unchecking the active field, you may hide a fiscal position without deleting it."), 'company_id': fields.many2one('res.company', 'Company'), 'account_ids': fields.one2many('account.fiscal.position.account', 'position_id', 'Account Mapping'), 'tax_ids': fields.one2many('account.fiscal.position.tax', 'position_id', 'Tax Mapping'), 'note': fields.text('Notes'), + 'auto_apply': fields.boolean('Automatic', help="Apply automatically this fiscal position."), + 'vat_required': fields.boolean('VAT required', help="Apply only if partner has a VAT number."), + 'country_id': fields.many2one('res.country', 'Countries', help="Apply only if delivery or invoicing country match."), + 'country_group_id': fields.many2one('res.country.group', 'Country Group', help="Apply only if delivery or invocing country match the group."), } _defaults = { @@ -66,6 +72,33 @@ class account_fiscal_position(osv.osv): break return account_id + def get_fiscal_position(self, cr, uid, company_id, partner_id, delivery_id=None, context=None): + if not partner_id: + return False + # This can be easily overriden to apply more complex fiscal rules + part_obj = self.pool['res.partner'] + partner = part_obj.browse(cr, uid, partner_id, context=context) + + # partner manually set fiscal position always win + if partner.property_account_position: + return part.property_account_position.id + + # if no delivery use invocing + if delivery_id: + delivery = part_obj.browse(cr, uid, delivery_id, context=context) + else: + delivery = partner + + domain = [ + ('auto_apply', '=', True), + '|', ('vat_required', '=', False), ('vat_required', '=', partner.vat_subjected), + '|', ('country_id', '=', None), ('country_id', '=', delivery.country_id.id), + '|', ('country_group_id', '=', None), ('country_group_id.country_ids', '=', delivery.country_id.id) + ] + fiscal_position_ids = self.search(cr, uid, domain, context=context) + if fiscal_position_ids: + return fiscal_position_ids[0] + return False class account_fiscal_position_tax(osv.osv): _name = 'account.fiscal.position.tax' @@ -206,6 +239,7 @@ class res_partner(osv.osv): return self.write(cr, uid, ids, {'last_reconciliation_date': time.strftime('%Y-%m-%d %H:%M:%S')}, context=context) _columns = { + 'vat_subjected': fields.boolean('VAT Legal Statement', help="Check this box if the partner is subjected to the VAT. It will be used for the VAT legal statement."), 'credit': fields.function(_credit_debit_get, fnct_search=_credit_search, string='Total Receivable', multi='dc', help="Total amount this customer owes you."), 'debit': fields.function(_credit_debit_get, fnct_search=_debit_search, string='Total Payable', multi='dc', help="Total amount you have to pay to this supplier."), diff --git a/addons/account/partner_view.xml b/addons/account/partner_view.xml index c2a01c7e40a..309296c7955 100644 --- a/addons/account/partner_view.xml +++ b/addons/account/partner_view.xml @@ -12,6 +12,13 @@ + + + + + + + @@ -44,6 +51,7 @@ account.fiscal.position + diff --git a/addons/base_vat/base_vat.py b/addons/base_vat/base_vat.py index 9b3862508b0..b7ac9592dd3 100644 --- a/addons/base_vat/base_vat.py +++ b/addons/base_vat/base_vat.py @@ -133,10 +133,6 @@ class res_partner(osv.osv): def vat_change(self, cr, uid, ids, value, context=None): return {'value': {'vat_subjected': bool(value)}} - _columns = { - 'vat_subjected': fields.boolean('VAT Legal Statement', help="Check this box if the partner is subjected to the VAT. It will be used for the VAT legal statement.") - } - def _commercial_fields(self, cr, uid, context=None): return super(res_partner, self)._commercial_fields(cr, uid, context=context) + ['vat_subjected'] diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 87ad2d80be0..5e44ad5b0f4 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -320,6 +320,16 @@ class sale_order(osv.osv): context_lang.update({'lang': partner_lang}) return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note + def onchange_delivery_id(self, cr, uid, ids, company_id, partner_id, delivery_id, fiscal_position, context=None): + r = {'value': {}} + if not fiscal_position: + if not company_id: + company_id = self._get_default_company(cr, uid, context=context) + fiscal_position = self.pool['account.fiscal.position'].get_fiscal_position(cr, uid, company_id, partner_id, delivery_id, context=context) + if fiscal_position: + r['value']['fiscal_position'] = fiscal_position + return r + def onchange_partner_id(self, cr, uid, ids, part, context=None): if not part: return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term': False, 'fiscal_position': False}} @@ -328,15 +338,15 @@ class sale_order(osv.osv): addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact']) pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False payment_term = part.property_payment_term and part.property_payment_term.id or False - fiscal_position = part.property_account_position and part.property_account_position.id or False dedicated_salesman = part.user_id and part.user_id.id or uid val = { 'partner_invoice_id': addr['invoice'], 'partner_shipping_id': addr['delivery'], 'payment_term': payment_term, - 'fiscal_position': fiscal_position, 'user_id': dedicated_salesman, } + delivery_onchange = self.onchange_delivery_id(cr, uid, ids, False, part.id, addr['delivery'], False, context=context) + val.update(delivery_onchange['value']) if pricelist: val['pricelist_id'] = pricelist sale_note = self.get_salenote(cr, uid, ids, part.id, context=context) @@ -345,11 +355,14 @@ class sale_order(osv.osv): def create(self, cr, uid, vals, context=None): if context is None: - context = {} + context = {} if vals.get('name', '/') == '/': vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'sale.order') or '/' - if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']): - defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context)['value'] + if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id', 'fiscal_position']): + defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context=context)['value'] + if not vals.get('fiscal_position') and vals.get('partner_shipping_id'): + delivery_onchange = self.onchange_delivery_id(cr, uid, [], vals.get('company_id'), None, vals['partner_id'], vals.get('partner_shipping_id'), context=context) + defaults.update(delivery_onchange['value']) vals = dict(defaults, **vals) context.update({'mail_create_nolog': True}) new_id = super(sale_order, self).create(cr, uid, vals, context=context) diff --git a/addons/sale/sale_view.xml b/addons/sale/sale_view.xml index d5d072c4513..f61c518918e 100644 --- a/addons/sale/sale_view.xml +++ b/addons/sale/sale_view.xml @@ -102,7 +102,7 @@ - + diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index f187e6799ac..656f581bc63 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -456,6 +456,7 @@ class website_sale(http.Controller): if partner_id and request.website.partner_id.id != partner_id: orm_partner.write(cr, SUPERUSER_ID, [partner_id], billing_info, context=context) else: + # create partner partner_id = orm_partner.create(cr, SUPERUSER_ID, billing_info, context=context) # create a new shipping partner @@ -472,7 +473,9 @@ class website_sale(http.Controller): 'partner_invoice_id': partner_id, 'partner_shipping_id': shipping_id or partner_id } - order_info.update(registry.get('sale.order').onchange_partner_id(cr, SUPERUSER_ID, [], partner_id, context=context)['value']) + order_info.update(order_obj.onchange_partner_id(cr, SUPERUSER_ID, [], partner_id, context=context)['value']) + order_info.update(order_obj.onchange_delivery_id(cr, SUPERUSER_ID, [], order.company_id.id, partner_id, shipping_id, None, context=context)['value']) + order_info.pop('user_id') order_obj.write(cr, SUPERUSER_ID, [order.id], order_info, context=context) @@ -512,6 +515,8 @@ class website_sale(http.Controller): self.checkout_form_save(values["checkout"]) request.session['sale_last_order_id'] = order.id + request.website.sale_get_order(update_pricelist=True, context=context) + return request.redirect("/shop/payment") #------------------------------------------------------ diff --git a/addons/website_sale/models/sale_order.py b/addons/website_sale/models/sale_order.py index 165030f9f1a..7c0700a555a 100644 --- a/addons/website_sale/models/sale_order.py +++ b/addons/website_sale/models/sale_order.py @@ -128,7 +128,7 @@ class website(orm.Model): def sale_product_domain(self, cr, uid, ids, context=None): return [("sale_ok", "=", True)] - def sale_get_order(self, cr, uid, ids, force_create=False, code=None, context=None): + def sale_get_order(self, cr, uid, ids, force_create=False, code=None, update_pricelist=None, context=None): sale_order_obj = self.pool['sale.order'] sale_order_id = request.session.get('sale_order_id') sale_order = None @@ -157,26 +157,20 @@ class website(orm.Model): request.session['sale_order_id'] = None return None - def update_pricelist(pricelist_id): - values = {'pricelist_id': pricelist_id} - values.update(sale_order.onchange_pricelist_id(pricelist_id, None)['value']) - sale_order.write(values) - for line in sale_order.order_line: - sale_order._cart_update(product_id=line.product_id.id, add_qty=0) - # check for change of pricelist with a coupon if code and code != sale_order.pricelist_id.code: pricelist_ids = self.pool['product.pricelist'].search(cr, SUPERUSER_ID, [('code', '=', code)], context=context) if pricelist_ids: pricelist_id = pricelist_ids[0] request.session['sale_order_code_pricelist_id'] = pricelist_id - update_pricelist(pricelist_id) + update_pricelist = True request.session['sale_order_code_pricelist_id'] = False + pricelist_id = request.session.get('sale_order_code_pricelist_id') or partner.property_product_pricelist.id + # check for change of partner_id ie after signup if sale_order.partner_id.id != partner.id and request.website.partner_id.id != partner.id: flag_pricelist = False - pricelist_id = request.session.get('sale_order_code_pricelist_id') or partner.property_product_pricelist.id if pricelist_id != sale_order.pricelist_id.id: flag_pricelist = True fiscal_position = sale_order.fiscal_position and sale_order.fiscal_position.id or False @@ -190,7 +184,15 @@ class website(orm.Model): sale_order_obj.write(cr, SUPERUSER_ID, [sale_order_id], values, context=context) if flag_pricelist or values.get('fiscal_position') != fiscal_position: - update_pricelist(pricelist_id) + update_pricelist = True + + # update the pricelist + if update_pricelist: + values = {'pricelist_id': pricelist_id} + values.update(sale_order.onchange_pricelist_id(pricelist_id, None)['value']) + sale_order.write(values) + for line in sale_order.order_line: + sale_order._cart_update(product_id=line.product_id.id, add_qty=0) # update browse record if (code and code != sale_order.pricelist_id.code) or sale_order.partner_id.id != partner.id: diff --git a/openerp/addons/base/res/res_country.py b/openerp/addons/base/res/res_country.py index 10f82df2ee8..f086f1440e8 100644 --- a/openerp/addons/base/res/res_country.py +++ b/openerp/addons/base/res/res_country.py @@ -84,6 +84,14 @@ addresses belonging to this country.\n\nYou can use the python-style string pate context=context) +class CountryGroup(osv.osv): + _description="Country Group" + _name = 'res.country.group' + _columns = { + 'name': fields.char('Name', required=True), + 'country_ids': fields.many2many('res.country', string='Countries'), + } + class CountryState(osv.osv): _description="Country state" _name = 'res.country.state' diff --git a/openerp/addons/base/res/res_country_data.xml b/openerp/addons/base/res/res_country_data.xml index 633b4cc42c4..1bdd6a73460 100644 --- a/openerp/addons/base/res/res_country_data.xml +++ b/openerp/addons/base/res/res_country_data.xml @@ -1516,5 +1516,18 @@ + + + + + Europe + + diff --git a/openerp/addons/base/res/res_country_view.xml b/openerp/addons/base/res/res_country_view.xml index ad46235c5c6..f1316844dd7 100644 --- a/openerp/addons/base/res/res_country_view.xml +++ b/openerp/addons/base/res/res_country_view.xml @@ -50,6 +50,38 @@ + + res.country.group.tree + res.country.group + + + + + + + + + res.country.group.form + res.country.group + +
    + + + + +
    + + + Country Group + ir.actions.act_window + res.country.group + form + Display and manage the list of all countries group. You can create or delete country group to make sure the ones you are working on will be maintained. + + + + + diff --git a/openerp/addons/base/security/ir.model.access.csv b/openerp/addons/base/security/ir.model.access.csv index c70733b5140..bbf579486a4 100644 --- a/openerp/addons/base/security/ir.model.access.csv +++ b/openerp/addons/base/security/ir.model.access.csv @@ -45,8 +45,10 @@ "access_res_company_group_user","res_company group_user","model_res_company",,1,0,0,0 "access_res_country_group_all","res_country group_user_all","model_res_country",,1,0,0,0 "access_res_country_state_group_all","res_country_state group_user_all","model_res_country_state",,1,0,0,0 +"access_res_country_group_group_all","res_country_group group_user_all","model_res_country_group",,1,0,0,0 "access_res_country_group_user","res_country group_user","model_res_country","group_partner_manager",1,1,1,1 "access_res_country_state_group_user","res_country_state group_user","model_res_country_state","group_partner_manager",1,1,1,1 +"access_res_country_group_group_user","res_country_group group_user","model_res_country_group","group_partner_manager",1,1,1,1 "access_res_currency_group_all","res_currency group_all","model_res_currency",,1,0,0,0 "access_res_currency_rate_group_all","res_currency_rate group_all","model_res_currency_rate",,1,0,0,0 "access_res_currency_rate_type_group_all","res_currency_rate_type group_all","model_res_currency_rate_type",,1,0,0,0 From ebd74cedcc91bced28d77935eca134ba330a3847 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 30 Jun 2014 01:41:28 +0200 Subject: [PATCH 06/13] [FIX] purchase_requisition test of PO's --- .../purchase_requisition/test/purchase_requisition.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/purchase_requisition/test/purchase_requisition.yml b/addons/purchase_requisition/test/purchase_requisition.yml index 5ac152d3d29..355e13e0677 100644 --- a/addons/purchase_requisition/test/purchase_requisition.yml +++ b/addons/purchase_requisition/test/purchase_requisition.yml @@ -69,11 +69,11 @@ purchase_requisition = self.pool.get("purchase.requisition") purchase_ids = self.search(cr, uid, [('requisition_id','=',ref("requisition1"))]) assert purchase_ids, "RFQ is not created." - rfq = self.browse(cr, uid, purchase_ids[0], context=context) - requisition = rfq.requisition_id - supplier = rfq.partner_id - assert supplier.id == ref('base.res_partner_12'), "RFQ Partner is not correspond." - assert len(rfq.order_line) == len(requisition.line_ids), "Lines are not correspond." + for rfq in self.browse(cr, uid, purchase_ids, context=context): + if rfq.partner_id.id == ref('base.res_partner_12'): + break + else: + assert False, "No PO found for res_partner_12." - I confirmed RFQ which has best price. - From 6cb138026a6b889e855391e67810ff1362614c62 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 30 Jun 2014 11:22:22 +0200 Subject: [PATCH 07/13] [FIX] odoo.py dont force push simple Everybody should upgrade to v2 --- odoo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odoo.py b/odoo.py index a998c3f8db6..b8f93ad19d5 100755 --- a/odoo.py +++ b/odoo.py @@ -68,7 +68,7 @@ def cmd_setup_git(): git_dir = os.getcwd() if git_dir: # push sane config for git < 2.0, and hooks - run('git','config','push.default','simple') + #run('git','config','push.default','simple') # alias run('git','config','alias.st','status') # merge bzr style From 359ea4a8ffaae81a33fa061ebf6d77cf1e3259b4 Mon Sep 17 00:00:00 2001 From: Cedric Snauwaert Date: Mon, 30 Jun 2014 12:27:34 +0200 Subject: [PATCH 08/13] [FIX]do not double bind click on menu due to on_hashchange --- addons/web/static/src/js/chrome.js | 1 - 1 file changed, 1 deletion(-) diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index 9fcbdff1f89..3534de032eb 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -1377,7 +1377,6 @@ instance.web.WebClient = instance.web.Client.extend({ var state = event.getState(true); if(!state.action && state.menu_id) { self.menu.is_bound.done(function() { - self.menu.do_reload(); self.menu.menu_click(state.menu_id); }); } else { From 7a88100961bcce87e4ec9f9acbccb94d675f4e7c Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 30 Jun 2014 14:25:20 +0200 Subject: [PATCH 09/13] [IMP] account.invoice.report: default `Year` filter should include draft invoices without `date` This affects the result when Draft invoices are included, so that they actually appear even without any `date` value, as they are often meant for the current year. --- addons/account/report/account_invoice_report_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/account/report/account_invoice_report_view.xml b/addons/account/report/account_invoice_report_view.xml index 01e3dc72d68..35cb43467f2 100644 --- a/addons/account/report/account_invoice_report_view.xml +++ b/addons/account/report/account_invoice_report_view.xml @@ -21,7 +21,7 @@ - + From 63d8fc6e8c491131803b98828eba0a0272904a67 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 30 Jun 2014 14:33:00 +0200 Subject: [PATCH 10/13] [IMP] google_drive: `google_drive_client_id` param should only be readable by uid 1 It is an internal ID that the user or web client never needs to read, it is used only server-side. --- addons/google_drive/google_drive_data.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/google_drive/google_drive_data.xml b/addons/google_drive/google_drive_data.xml index f3f916b201b..72e541df56e 100644 --- a/addons/google_drive/google_drive_data.xml +++ b/addons/google_drive/google_drive_data.xml @@ -5,7 +5,7 @@ google_drive_client_id 598905559630.apps.googleusercontent.com - + From 959caf2596211714f75cebd0990356b91cd80021 Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Mon, 30 Jun 2014 15:32:53 +0200 Subject: [PATCH 11/13] [IMP] cache the load_menus method to improve website performances --- openerp/addons/base/ir/ir_ui_menu.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openerp/addons/base/ir/ir_ui_menu.py b/openerp/addons/base/ir/ir_ui_menu.py index 8ace579defb..718dcb0c36c 100644 --- a/openerp/addons/base/ir/ir_ui_menu.py +++ b/openerp/addons/base/ir/ir_ui_menu.py @@ -349,6 +349,7 @@ class ir_ui_menu(osv.osv): menu_domain = [('parent_id', '=', False)] return self.search(cr, uid, menu_domain, context=context) + @tools.ormcache() def load_menus(self, cr, uid, context=None): """ Loads all menu items (all applications and their sub-menus). From 3bb7866384adf8ec84d1e279788d64031979f8fb Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 30 Jun 2014 16:32:02 +0200 Subject: [PATCH 12/13] [FIX] test phantomjs clear localstorage before running tests --- openerp/tests/phantomtest.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openerp/tests/phantomtest.js b/openerp/tests/phantomtest.js index 84f886c5dbb..49da52e07d9 100644 --- a/openerp/tests/phantomtest.js +++ b/openerp/tests/phantomtest.js @@ -116,6 +116,8 @@ function PhantomTest() { phantom.exit(1); } else { console.log('loaded', url, status); + // clear localstorage leftovers + self.page.evaluate(function () { localStorage.clear() }); // process ready waitFor(function() { console.log("PhantomTest.run: wait for condition:", ready); From 0ef85c5a60db0d168b5e139bb216e035207f211a Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Mon, 30 Jun 2014 17:23:38 +0200 Subject: [PATCH 13/13] [FIX] test phantomjs manually delete localstorage --- openerp/tests/common.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openerp/tests/common.py b/openerp/tests/common.py index 4d4417ee781..50b85555d04 100644 --- a/openerp/tests/common.py +++ b/openerp/tests/common.py @@ -5,6 +5,7 @@ helpers and classes to write tests. """ import errno +import glob import json import logging import os @@ -170,7 +171,6 @@ class HttpCase(TransactionCase): self.session_id = self.session.sid self.session.db = DB openerp.http.root.session_store.save(self.session) - self.localstorage_path = mkdtemp() # setup an url opener helper self.opener = urllib2.OpenerDirector() self.opener.add_handler(urllib2.UnknownHandler()) @@ -181,7 +181,6 @@ class HttpCase(TransactionCase): self.opener.addheaders.append(('Cookie', 'session_id=%s' % self.session_id)) def tearDown(self): - rmtree(self.localstorage_path) self.registry.leave_test_mode() super(HttpCase, self).tearDown() @@ -254,6 +253,11 @@ class HttpCase(TransactionCase): def phantom_run(self, cmd, timeout): _logger.info('phantom_run executing %s', ' '.join(cmd)) + + ls_glob = os.path.expanduser('~/.qws/share/data/Ofi Labs/PhantomJS/http_localhost_%s.*'%PORT) + for i in glob.glob(ls_glob): + _logger.info('phantomjs unlink localstorage %s', i) + os.unlink(i) try: phantom = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) except OSError: @@ -296,7 +300,6 @@ class HttpCase(TransactionCase): # phantom.args[1] == options cmd = [ 'phantomjs', - '--local-storage-path', self.localstorage_path, jsfile, phantomtest, json.dumps(options) ] self.phantom_run(cmd, timeout)