From fec027e83203e6705f690469cd39101e4eba3b06 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 15 Jan 2013 11:40:15 +0100 Subject: [PATCH 001/448] [IMP] reorder modules menus bzr revid: chs@openerp.com-20130115104015-5ytx00t0b5230aaf --- openerp/addons/base/module/module_view.xml | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/openerp/addons/base/module/module_view.xml b/openerp/addons/base/module/module_view.xml index 3a43a2591ba..3923c9de3bd 100644 --- a/openerp/addons/base/module/module_view.xml +++ b/openerp/addons/base/module/module_view.xml @@ -171,23 +171,18 @@ - - Modules ir.module.module form kanban,tree,form -

No module found!

You should try others search criteria.

- + @@ -195,20 +190,14 @@ Apps apps - - + Updates apps.updates {} - - + From 7e1a76cdd846a8c455d402fcd76d8baa58a9b2f8 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Tue, 15 Jan 2013 11:40:47 +0100 Subject: [PATCH 002/448] [FIX] setup.py: correct windows install bzr revid: chs@openerp.com-20130115104047-6hrl825bn5pkmoo6 --- setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup.py b/setup.py index b144924d022..e4e6001d302 100755 --- a/setup.py +++ b/setup.py @@ -43,6 +43,12 @@ def data(): base = os.path.join('pytz', root[len(tzdir) + 1:]) r[base] = [os.path.join(root, f) for f in filenames] + import docutils + dudir = os.path.dirname(docutils.__file__) + for root, _, filenames in os.walk(dudir): + base = os.path.join('docutils', root[len(dudir) + 1:]) + r[base] = [os.path.join(root, f) for f in filenames if not f.endswith(('.py', '.pyc', '.pyo'))] + return r.items() def gen_manifest(): From e2639618f666605e385befb578db3f889a8c035f Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 17 Jan 2013 10:27:22 +0100 Subject: [PATCH 003/448] [DOC] remove openerpdev intersphinx, add ref to module doc bzr revid: xmo@openerp.com-20130117092722-6rfhkcu4igrgeo06 --- addons/web/doc/conf.py | 1 - addons/web/doc/module.rst | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/web/doc/conf.py b/addons/web/doc/conf.py index 54434190707..7fe38022ab1 100644 --- a/addons/web/doc/conf.py +++ b/addons/web/doc/conf.py @@ -253,6 +253,5 @@ todo_include_todos = True intersphinx_mapping = { 'python': ('http://docs.python.org/', None), 'openerpserver': ('http://doc.openerp.com/trunk/developers/server', None), - 'openerpdev': ('http://doc.openerp.com/trunk/developers', None), 'openerpcommand': ('http://doc.openerp.com/trunk/developers/command', None), } diff --git a/addons/web/doc/module.rst b/addons/web/doc/module.rst index 1bdb7c14799..8306e885f65 100644 --- a/addons/web/doc/module.rst +++ b/addons/web/doc/module.rst @@ -1,3 +1,5 @@ +.. _module: + Building an OpenERP Web module ============================== From 3574038ffdf78e8c63430ecc92a4f2d916207fda Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 17 Jan 2013 14:29:24 +0100 Subject: [PATCH 004/448] [FIX] apps: install_from_url: fix install condition + better logging bzr revid: chs@openerp.com-20130117132924-ejzbkh5przqi6hwf --- openerp/addons/base/module/module.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/module/module.py b/openerp/addons/base/module/module.py index 9e8b68451c5..123bf4124ed 100644 --- a/openerp/addons/base/module/module.py +++ b/openerp/addons/base/module/module.py @@ -2,7 +2,7 @@ ############################################################################## # # OpenERP, Open Source Management Solution -# Copyright (C) 2004-2012 OpenERP S.A. (). +# Copyright (C) 2004-2013 OpenERP S.A. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -656,6 +656,7 @@ class module(osv.osv): def install_from_urls(self, cr, uid, urls, context=None): OPENERP = 'openerp' tmp = tempfile.mkdtemp() + _logger.debug('Install from url: %r', urls) try: # 1. Download & unzip missing modules for module_name, url in urls.items(): @@ -672,12 +673,13 @@ class module(osv.osv): zipfile.ZipFile(StringIO(content)).extractall(tmp) assert os.path.isdir(os.path.join(tmp, module_name)) - # 2a. Copy/Replace module source in addons path + # 2a. Copy/Replace module source in addons path for module_name, url in urls.items(): if module_name == OPENERP or not url: continue # OPENERP is special case, handled below, and no URL means local module module_path = modules.get_module_path(module_name, downloaded=True, display_warning=False) bck = backup(module_path, False) + _logger.info('Copy downloaded module `%s` to `%s`', module_name, module_path) shutil.move(os.path.join(tmp, module_name), module_path) if bck: shutil.rmtree(bck) @@ -697,15 +699,22 @@ class module(osv.osv): # then replace the server by the new "base" module server_dir = openerp.tools.config['root_path'] # XXX or dirname() bck = backup(server_dir) + _logger.info('Copy downloaded module `openerp` to `%s`', server_dir) shutil.move(os.path.join(tmp, OPENERP), server_dir) #if bck: # shutil.rmtree(bck) self.update_list(cr, uid, context=context) - ids = self.search(cr, uid, [('name', 'in', urls.keys())], context=context) - if self.search_count(cr, uid, [('id', 'in', ids), ('state', '=', 'installed')], context=context): - # if any to update + with_urls = [m for m, u in urls.items() if u] + downloaded_ids = self.search(cr, uid, [('name', 'in', with_urls)], context=context) + already_installed = self.search(cr, uid, [('id', 'in', downloaded_ids), ('state', '=', 'installed')], context=context) + + to_install_ids = self.search(cr, uid, [('name', 'in', urls.keys()), ('state', '=', 'uninstalled')], context=context) + post_install_action = self.button_immediate_install(cr, uid, to_install_ids, context=context) + + if already_installed: + # in this case, force server restart to reload python code... cr.commit() openerp.service.restart_server() return { @@ -713,7 +722,7 @@ class module(osv.osv): 'tag': 'home', 'params': {'wait': True}, } - return self.button_immediate_install(cr, uid, ids, context=context) + return post_install_action finally: shutil.rmtree(tmp) From 4857a8a020d81ebd375122fb478726103c3f2393 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 17 Jan 2013 14:51:08 +0100 Subject: [PATCH 005/448] [FIX] module loading: call adapt_version() instead of computing it ourself bzr revid: chs@openerp.com-20130117135108-a4wt4wtbtfdwiui3 --- openerp/modules/loading.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openerp/modules/loading.py b/openerp/modules/loading.py index fb68642f137..638a40c9fa8 100644 --- a/openerp/modules/loading.py +++ b/openerp/modules/loading.py @@ -3,7 +3,7 @@ # # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). -# Copyright (C) 2010-2012 OpenERP s.a. (). +# Copyright (C) 2010-2013 OpenERP s.a. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -36,14 +36,12 @@ import openerp.modules.graph import openerp.modules.migration import openerp.osv as osv import openerp.pooler as pooler -import openerp.release as release import openerp.tools as tools from openerp import SUPERUSER_ID -from openerp import SUPERUSER_ID from openerp.tools.translate import _ from openerp.modules.module import initialize_sys_path, \ - load_openerp_module, init_module_models + load_openerp_module, init_module_models, adapt_version _logger = logging.getLogger(__name__) @@ -213,7 +211,7 @@ def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules= migrations.migrate_module(package, 'post') - ver = release.major_version + '.' + package.data['version'] + ver = adapt_version(package.data['version']) # Set new modules and dependencies modobj.write(cr, SUPERUSER_ID, [module_id], {'state': 'installed', 'latest_version': ver}) # Update translations for all installed languages From 66cadcdb18c9ce0f48a05cf7d13cb98ed2f171e5 Mon Sep 17 00:00:00 2001 From: Anael Closson Date: Fri, 10 May 2013 17:19:34 +0200 Subject: [PATCH 007/448] [FIX] OPW 591344 : internal reports don't show images Cleaner method, and shows images. bzr revid: acl@openerp.com-20130510151934-e2kvwiexdzp3zc12 --- openerp/report/render/rml2pdf/trml2pdf.py | 54 +++++++---------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/openerp/report/render/rml2pdf/trml2pdf.py b/openerp/report/render/rml2pdf/trml2pdf.py index e8d6375c98e..153eff14b65 100644 --- a/openerp/report/render/rml2pdf/trml2pdf.py +++ b/openerp/report/render/rml2pdf/trml2pdf.py @@ -82,53 +82,33 @@ def _open_image(filename, path=None): pass raise IOError("File %s cannot be found in image path" % filename) + class NumberedCanvas(canvas.Canvas): def __init__(self, *args, **kwargs): canvas.Canvas.__init__(self, *args, **kwargs) - self._codes = [] - self._flag=False - self._pageCount=0 - self._currentPage =0 - self._pageCounter=0 - self.pages={} + self._saved_page_states = [] def showPage(self): - self._currentPage +=1 - if not self._flag: - self._pageCount += 1 - else: - self.pages.update({self._currentPage:self._pageCount}) - self._codes.append({'code': self._code, 'stack': self._codeStack}) + self._saved_page_states.append(dict(self.__dict__)) self._startPage() - self._flag=False - - def pageCount(self): - if self.pages.get(self._pageCounter,False): - self._pageNumber=0 - self._pageCounter +=1 - key=self._pageCounter - if not self.pages.get(key,False): - while not self.pages.get(key,False): - key += 1 - self.setFont("Helvetica", 8) - self.drawRightString((self._pagesize[0]-30), (self._pagesize[1]-40), - " %(this)i / %(total)i" % { - 'this': self._pageNumber+1, - 'total': self.pages.get(key,False), - } - ) def save(self): """add page info to each page (page x of y)""" - # reset page counter - self._pageNumber = 0 - for code in self._codes: - self._code = code['code'] - self._codeStack = code['stack'] - self.pageCount() + num_pages = len(self._saved_page_states) + for state in self._saved_page_states: + self.__dict__.update(state) + self.draw_page_number(num_pages) canvas.Canvas.showPage(self) -# self.restoreState() - self._doc.SaveToFile(self._filename, self) + canvas.Canvas.save(self) + + def draw_page_number(self, page_count): + self.drawRightString((self._pagesize[0]-30), (self._pagesize[1]-40), + " %(this)i / %(total)i" % { + 'this': self._pageNumber, + 'total': page_count, + } + ) + class PageCount(platypus.Flowable): def __init__(self, story_count=0): From 46e89d6dd1427ac5c7fa144f3b36e6ea1779771d Mon Sep 17 00:00:00 2001 From: Mohammed Shekha Date: Thu, 20 Jun 2013 16:16:06 +0530 Subject: [PATCH 008/448] [FIX]Fixed the issue of recurrent rule, there is no need of passing 'Z' after end_date as end_date converted to UNTIL parameter in rrule, and date given in UNTIL is not converted to any other timezone until you specifically passed timezone difference or timezone name like 20130620T121012+3:30 or 20130620T121012TZOFFSET, currently Z creates issue when recurrent rule is created with end_date, TypeError: can't compare offset-naive and offset-aware datetimes. bzr revid: msh@openerp.com-20130620104606-g3dyr1mafzo8fpgg --- addons/base_calendar/base_calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py index 25bfcac448c..41a142cde68 100644 --- a/addons/base_calendar/base_calendar.py +++ b/addons/base_calendar/base_calendar.py @@ -1269,7 +1269,7 @@ rule or repeating pattern of time to exclude from the recurring rule."), def get_end_date(data): if data.get('end_date'): - data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959Z' + data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959' return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\ ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '') From 3276861ff6125d02e08b5c5b12b75925e056c4bc Mon Sep 17 00:00:00 2001 From: "Xavier Fernandez http://www.smile.fr" <> Date: Wed, 28 Aug 2013 17:21:17 +0200 Subject: [PATCH 009/448] [FIX] port Xavier Fernandez's fix for Bug #971412: Translations export has no fixed order lp bug: https://launchpad.net/bugs/971412 fixed bzr revid: ls@numerigraphe.com-20130828152117-e1h0yljgbn538v9o --- openerp/tools/translate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 41113b8ccd9..53762b87970 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -458,7 +458,8 @@ def trans_export(lang, modules, buffer, format, cr): row.setdefault('tnrs', []).append((type, name, res_id)) row.setdefault('comments', set()).update(comments) - for src, row in grouped_rows.items(): + for src in sorted(grouped_rows): + row = grouped_rows[src] if not lang: # translation template, so no translation value row['translation'] = '' From 1f3a482f4fc309262802b980e143234a56b9503d Mon Sep 17 00:00:00 2001 From: Lionel Sausin Date: Thu, 29 Aug 2013 09:50:35 +0200 Subject: [PATCH 010/448] [REF] avoid the lookup in sorted translation export as suggested by Guewen Baconnier @ Camptocamp bzr revid: ls@numerigraphe.com-20130829075035-lml81a7bbn66c8qu --- openerp/tools/translate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 53762b87970..6e9eb50f162 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -458,8 +458,7 @@ def trans_export(lang, modules, buffer, format, cr): row.setdefault('tnrs', []).append((type, name, res_id)) row.setdefault('comments', set()).update(comments) - for src in sorted(grouped_rows): - row = grouped_rows[src] + for src, row in sorted(grouped_rows.items()): if not lang: # translation template, so no translation value row['translation'] = '' From 0dcc62426c3689191798fbe33717eea5d4659a96 Mon Sep 17 00:00:00 2001 From: Yanina Aular Date: Fri, 15 Nov 2013 17:57:17 -0430 Subject: [PATCH 011/448] [FIX] function field with wrong type int instead of integer bzr revid: yanina.aular@vauxoo.com-20131115222717-12bplq7rnv3rorbm --- addons/project/project.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/project/project.py b/addons/project/project.py index 5bd2e604368..c7b394ad376 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -288,7 +288,8 @@ class project(osv.osv): help="The kind of document created when an email is received on this project's email alias"), 'privacy_visibility': fields.selection(_visibility_selection, 'Privacy / Visibility', required=True), 'state': fields.selection([('template', 'Template'),('draft','New'),('open','In Progress'), ('cancelled', 'Cancelled'),('pending','Pending'),('close','Closed')], 'Status', required=True,), - 'doc_count':fields.function(_get_attached_docs, string="Number of documents attached", type='int') + 'doc_count':fields.function(_get_attached_docs, string="Number of documents attached", + type='integer') } def _get_type_common(self, cr, uid, context): From 02224a3f112426c9e15a8af7cde2e7a5406d1885 Mon Sep 17 00:00:00 2001 From: "Dharti Ratani (Open ERP)" Date: Thu, 12 Dec 2013 10:55:44 +0530 Subject: [PATCH 012/448] [FIX]Added the refernce of the supplier in price_get method while creating purchase order from purchase requisition, as without supplier the price unit in PO was 0 when the pricelist item was based on Supplier Price in the product form bzr revid: dhr@tinyerp.com-20131212052544-skljxsrxea4o8efm --- addons/purchase_requisition/purchase_requisition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/purchase_requisition/purchase_requisition.py b/addons/purchase_requisition/purchase_requisition.py index c1d80c7bf46..c31f8a69a37 100644 --- a/addons/purchase_requisition/purchase_requisition.py +++ b/addons/purchase_requisition/purchase_requisition.py @@ -109,7 +109,7 @@ class purchase_requisition(osv.osv): seller_delay = product_supplier.delay seller_qty = product_supplier.qty supplier_pricelist = supplier.property_product_pricelist_purchase or False - seller_price = pricelist.price_get(cr, uid, [supplier_pricelist.id], product.id, qty, False, {'uom': default_uom_po_id})[supplier_pricelist.id] + seller_price = pricelist.price_get(cr, uid, [supplier_pricelist.id], product.id, qty, supplier.id, {'uom': default_uom_po_id})[supplier_pricelist.id] if seller_qty: qty = max(qty,seller_qty) date_planned = self._planned_date(requisition_line.requisition_id, seller_delay) From f6349ffb7fd9c075573953be2ac0f0dc20520743 Mon Sep 17 00:00:00 2001 From: Leonardo Pistone Date: Wed, 18 Dec 2013 17:45:26 +0100 Subject: [PATCH 013/448] [merge] [fix] action should return True lp bug: https://launchpad.net/bugs/1262265 fixed bzr revid: leonardo.pistone@camptocamp.com-20131218164526-rgyo6p9hlpk28sxg --- addons/mrp_repair/mrp_repair.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/mrp_repair/mrp_repair.py b/addons/mrp_repair/mrp_repair.py index 9c0ab0e2c18..9335d395714 100644 --- a/addons/mrp_repair/mrp_repair.py +++ b/addons/mrp_repair/mrp_repair.py @@ -350,7 +350,8 @@ class mrp_repair(osv.osv): return self.write(cr,uid,ids,{'state':'cancel'}) def wkf_invoice_create(self, cr, uid, ids, *args): - return self.action_invoice_create(cr, uid, ids) + self.action_invoice_create(cr, uid, ids) + return True def action_invoice_create(self, cr, uid, ids, group=False, context=None): """ Creates invoice(s) for repair order. From 262a405b0ab1018bde19e61d38a4525582831a16 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 3 Jan 2014 14:07:03 +0100 Subject: [PATCH 014/448] Remove old FR VAT taxes 19.6% and 7.0%. Remove the FR VAT tax 5.0% that has never seen the light ! bzr revid: alexis@via.ecp.fr-20140103130703-kpf5rojc4azwq8w1 --- addons/l10n_fr/fr_fiscal_templates.xml | 79 ----- addons/l10n_fr/fr_tax.xml | 387 ------------------------- 2 files changed, 466 deletions(-) diff --git a/addons/l10n_fr/fr_fiscal_templates.xml b/addons/l10n_fr/fr_fiscal_templates.xml index bf49d15e08c..cca96422c22 100644 --- a/addons/l10n_fr/fr_fiscal_templates.xml +++ b/addons/l10n_fr/fr_fiscal_templates.xml @@ -40,11 +40,6 @@ - - - - - @@ -57,22 +52,12 @@ - - - - - - - - - - @@ -91,17 +76,6 @@ - - - - - - - - - - - @@ -124,17 +98,6 @@ - - - - - - - - - - - @@ -146,17 +109,6 @@ - - - - - - - - - - - @@ -177,11 +129,6 @@ - - - - - @@ -194,22 +141,12 @@ - - - - - - - - - - @@ -224,11 +161,6 @@ - - - - - @@ -241,23 +173,12 @@ - - - - - - - - - - - diff --git a/addons/l10n_fr/fr_tax.xml b/addons/l10n_fr/fr_tax.xml index 8633cca7141..22e1e12f03c 100644 --- a/addons/l10n_fr/fr_tax.xml +++ b/addons/l10n_fr/fr_tax.xml @@ -29,28 +29,6 @@ sale - - - - TVA collectée (vente) 19,6% - 19.6 - - percent - - - - - - - - - - - - - - sale - @@ -96,51 +74,7 @@ sale - - - TVA collectée (vente) 7,0% - 7.0 - - percent - - - - - - - - - - - - - - sale - - - - TVA collectée (vente) 5,0% - 5.0 - - percent - - - - - - - - - - - - - - sale - - - TVA collectée (vente) 5,5% 5.5 @@ -208,28 +142,6 @@ purchase - - - TVA déductible (achat) 19,6% - ACH-19.6 - - percent - - - - - - - - - - - - - - purchase - - TVA déductible (achat) 8,5% @@ -274,51 +186,7 @@ purchase - - - TVA déductible (achat) 7,0% - ACH-7.0 - - percent - - - - - - - - - - - - - - purchase - - - - TVA déductible (achat) 5,0% - ACH-5.0 - - percent - - - - - - - - - - - - - - purchase - - - TVA déductible (achat) 5,5% ACH-5.5 @@ -387,29 +255,6 @@ purchase - - - TVA déductible (achat) 19,6% TTC - ACH-19.6-TTC - - - percent - - - - - - - - - - - - - - purchase - - TVA déductible (achat) 8,5% TTC @@ -456,53 +301,7 @@ purchase - - - TVA déductible (achat) 7,0% TTC - ACH-7.0-TTC - - - percent - - - - - - - - - - - - - - purchase - - - - TVA déductible (achat) 5,0% TTC - ACH-5.0-TTC - - - percent - - - - - - - - - - - - - - purchase - - - TVA déductible (achat) 5,5% TTC ACH-5.5-TTC @@ -573,28 +372,6 @@ purchase - - - TVA déd./immobilisation (achat) 19,6% - IMMO-19.6 - - percent - - - - - - - - - - - - - - purchase - - TVA déd./immobilisation (achat) 8,5% @@ -639,51 +416,7 @@ purchase - - - TVA déd./immobilisation (achat) 7,0% - IMMO-7.0 - - percent - - - - - - - - - - - - - - purchase - - - - TVA déd./immobilisation (achat) 5,0% - IMMO-5.0 - - percent - - - - - - - - - - - - - - purchase - - - TVA déd./immobilisation (achat) 5,5% IMMO-5.5 @@ -751,28 +484,6 @@ purchase - - - TVA due s/ acq. intracommunautaire (achat) 19,6% - ACH_UE_due-19.6 - - percent - - - - - - - - - - - - - - purchase - - TVA due s/ acq. intracommunautaire (achat) 8,5% @@ -817,51 +528,7 @@ purchase - - - TVA due s/ acq. intracommunautaire (achat) 7,0% - ACH_UE_due-7.0 - - percent - - - - - - - - - - - - - - purchase - - - - TVA due s/ acq. intracommunautaire (achat) 5,0% - ACH_UE_due-5.0 - - percent - - - - - - - - - - - - - - purchase - - - TVA due s/ acq. intracommunautaire (achat) 5,5% ACH_UE_due-5.5 @@ -925,24 +592,6 @@ purchase - - - TVA déd. s/ acq. intracommunautaire (achat) 19,6% - ACH_UE_ded.-19.6 - - percent - - - - - - - - - - purchase - - TVA déd. s/ acq. intracommunautaire (achat) 8,5% @@ -979,43 +628,7 @@ purchase - - - TVA déd. s/ acq. intracommunautaire (achat) 7,0% - ACH_UE_ded.-7.0 - - percent - - - - - - - - - - purchase - - - - TVA déd. s/ acq. intracommunautaire (achat) 5,0% - ACH_UE_ded.-5.0 - - percent - - - - - - - - - - purchase - - - TVA déd. s/ acq. intracommunautaire (achat) 5,5% ACH_UE_ded.-5.5 From c8d596e9849aa19edc4cf89c6ded798efc241213 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Wed, 15 Jan 2014 18:12:05 +0100 Subject: [PATCH 015/448] [IMP] release.py: add "series" variable bzr revid: chs@openerp.com-20140115171205-458hinraek04wfr2 --- openerp/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/release.py b/openerp/release.py index 1ad35340496..1a9c863660c 100644 --- a/openerp/release.py +++ b/openerp/release.py @@ -32,7 +32,7 @@ RELEASE_LEVELS_DISPLAY = {ALPHA: ALPHA, # (6,1,0,'candidate',2) < (6,1,0,'final',0) < (6,1,2,'final',0) version_info = (8, 0, 0, ALPHA, 1) version = '.'.join(map(str, version_info[:2])) + RELEASE_LEVELS_DISPLAY[version_info[3]] + str(version_info[4] or '') -serie = major_version = '.'.join(map(str, version_info[:2])) +series = serie = major_version = '.'.join(map(str, version_info[:2])) description = 'OpenERP Server' long_desc = '''OpenERP is a complete ERP and CRM. The main features are accounting (analytic @@ -50,6 +50,6 @@ author = 'OpenERP S.A.' author_email = 'info@openerp.com' license = 'AGPL-3' -nt_service_name = "openerp-server-" + serie +nt_service_name = "openerp-server-" + series # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 5d87753ef2094faea8796e4f5ce199eae52b1682 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Wed, 15 Jan 2014 18:13:23 +0100 Subject: [PATCH 016/448] [ADD] new cli argument: -D/--data-dir bzr revid: chs@openerp.com-20140115171323-eiy2h61qi098n0q3 --- openerp/tools/config.py | 23 +- openerp/vendors/__init__.py | 0 openerp/vendors/appdirs.py | 477 ++++++++++++++++++++++++++++++++++++ 3 files changed, 496 insertions(+), 4 deletions(-) create mode 100644 openerp/vendors/__init__.py create mode 100644 openerp/vendors/appdirs.py diff --git a/openerp/tools/config.py b/openerp/tools/config.py index ebfd8cea20d..101724f4159 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -3,7 +3,7 @@ # # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). -# Copyright (C) 2010-2012 OpenERP s.a. (). +# Copyright (C) 2010-2014 OpenERP s.a. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -29,6 +29,7 @@ import openerp.conf import openerp.loglevels as loglevels import logging import openerp.release as release +from openerp.vendors import appdirs class MyOption (optparse.Option, object): """ optparse Option with two additional attributes. @@ -59,6 +60,9 @@ def check_ssl(): DEFAULT_LOG_HANDLER = [':INFO'] +def _get_default_datadir(): + return appdirs.user_data_dir(appname='OpenERP', appauthor=release.author) + class configmanager(object): def __init__(self, fname=None): # Options not exposed on the command line. Command line options will be added @@ -106,6 +110,9 @@ class configmanager(object): help="specify additional addons paths (separated by commas).", action="callback", callback=self._check_addons_path, nargs=1, type="string") group.add_option("--load", dest="server_wide_modules", help="Comma-separated list of server-wide modules default=web") + + group.add_option("-D", "--data-dir", dest="data_dir", my_default=_get_default_datadir(), + help="Directory where to store OpenERP data") parser.add_option_group(group) # XML-RPC / HTTP @@ -348,6 +355,7 @@ class configmanager(object): # (../etc from the server) # if the server is run by an unprivileged user, he has to specify location of a config file where he has the rights to write, # else he won't be able to save the configurations, or even to start the server... + # TODO use appdirs if os.name == 'nt': rcfilepath = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'openerp-server.conf') else: @@ -358,7 +366,6 @@ class configmanager(object): or os.environ.get('OPENERP_SERVER') or rcfilepath) self.load() - # Verify that we want to log or not, if not the output will go to stdout if self.options['logfile'] in ('None', 'False'): self.options['logfile'] = False @@ -387,7 +394,6 @@ class configmanager(object): elif isinstance(self.options[arg], basestring) and self.casts[arg].type in optparse.Option.TYPE_CHECKER: self.options[arg] = optparse.Option.TYPE_CHECKER[self.casts[arg].type](self.casts[arg], arg, self.options[arg]) - if isinstance(self.options['log_handler'], basestring): self.options['log_handler'] = self.options['log_handler'].split(',') @@ -399,7 +405,8 @@ class configmanager(object): 'list_db', 'xmlrpcs', 'proxy_mode', 'test_file', 'test_enable', 'test_commit', 'test_report_directory', 'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', 'unaccent', - 'workers', 'limit_memory_hard', 'limit_memory_soft', 'limit_time_cpu', 'limit_time_real', 'limit_request', 'auto_reload' + 'workers', 'limit_memory_hard', 'limit_memory_soft', 'limit_time_cpu', 'limit_time_real', 'limit_request', + 'auto_reload', 'data_dir', ] for arg in keys: @@ -617,6 +624,14 @@ class configmanager(object): def __getitem__(self, key): return self.options[key] + @property + def addons_data_dir(self): + return os.path.join(self['data_dir'], 'addons', release.series) + + @property + def session_dir(self): + return os.path.join(self['data_dir'], 'sessions') + config = configmanager() diff --git a/openerp/vendors/__init__.py b/openerp/vendors/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/openerp/vendors/appdirs.py b/openerp/vendors/appdirs.py new file mode 100644 index 00000000000..1d24dd8586a --- /dev/null +++ b/openerp/vendors/appdirs.py @@ -0,0 +1,477 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2005-2010 ActiveState Software Inc. +# Copyright (c) 2013 Eddy Petrișor + +"""Utilities for determining application-specific dirs. + +See for details and usage. +""" +# Dev Notes: +# - MSDN on where to store app data files: +# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120 +# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html +# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + +__version_info__ = (1, 3, 0) +__version__ = '.'.join(map(str, __version_info__)) + + +import sys +import os + +PY3 = sys.version_info[0] == 3 + +if PY3: + unicode = str + + + +def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only required and used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user data directories are: + Mac OS X: ~/Library/Application Support/ + Unix: ~/.local/share/ # or in $XDG_DATA_HOME, if defined + Win XP (not roaming): C:\Documents and Settings\\Application Data\\ + Win XP (roaming): C:\Documents and Settings\\Local Settings\Application Data\\ + Win 7 (not roaming): C:\Users\\AppData\Local\\ + Win 7 (roaming): C:\Users\\AppData\Roaming\\ + + For Unix, we follow the XDG spec and support $XDG_DATA_HOME. + That means, by deafult "~/.local/share/". + """ + if sys.platform == "win32": + if appauthor is None: + appauthor = appname + const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA" + path = os.path.normpath(_get_win_folder(const)) + if appname: + path = os.path.join(path, appauthor, appname) + elif sys.platform == 'darwin': + path = os.path.expanduser('~/Library/Application Support/') + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def site_data_dir(appname=None, appauthor=None, version=None, multipath=False): + """Return full path to the user-shared data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only required and used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "multipath" is an optional parameter only applicable to *nix + which indicates that the entire list of data dirs should be + returned. By default, the first item from XDG_DATA_DIRS is + returned, or '/usr/local/share/', + if XDG_DATA_DIRS is not set + + Typical user data directories are: + Mac OS X: /Library/Application Support/ + Unix: /usr/local/share/ or /usr/share/ + Win XP: C:\Documents and Settings\All Users\Application Data\\ + Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.) + Win 7: C:\ProgramData\\ # Hidden, but writeable on Win 7. + + For Unix, this is using the $XDG_DATA_DIRS[0] default. + + WARNING: Do not use this on Windows. See the Vista-Fail note above for why. + """ + if sys.platform == "win32": + if appauthor is None: + appauthor = appname + path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA")) + if appname: + path = os.path.join(path, appauthor, appname) + elif sys.platform == 'darwin': + path = os.path.expanduser('/Library/Application Support') + if appname: + path = os.path.join(path, appname) + else: + # XDG default for $XDG_DATA_DIRS + # only first, if multipath is False + path = os.getenv('XDG_DATA_DIRS', + os.pathsep.join(['/usr/local/share', '/usr/share'])) + pathlist = [ os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep) ] + if appname: + if version: + appname = os.path.join(appname, version) + pathlist = [ os.sep.join([x, appname]) for x in pathlist ] + + if multipath: + path = os.pathsep.join(pathlist) + else: + path = pathlist[0] + return path + + if appname and version: + path = os.path.join(path, version) + return path + + +def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): + r"""Return full path to the user-specific config dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only required and used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "roaming" (boolean, default False) can be set True to use the Windows + roaming appdata directory. That means that for users on a Windows + network setup for roaming profiles, this user data will be + sync'd on login. See + + for a discussion of issues. + + Typical user data directories are: + Mac OS X: same as user_data_dir + Unix: ~/.config/ # or in $XDG_CONFIG_HOME, if defined + Win *: same as user_data_dir + + For Unix, we follow the XDG spec and support $XDG_DATA_HOME. + That means, by deafult "~/.local/share/". + """ + if sys.platform in [ "win32", "darwin" ]: + path = user_data_dir(appname, appauthor, None, roaming) + else: + path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config")) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + + +def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): + """Return full path to the user-shared data dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only required and used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "multipath" is an optional parameter only applicable to *nix + which indicates that the entire list of config dirs should be + returned. By default, the first item from XDG_CONFIG_DIRS is + returned, or '/etc/xdg/', if XDG_CONFIG_DIRS is not set + + Typical user data directories are: + Mac OS X: same as site_data_dir + Unix: /etc/xdg/ or $XDG_CONFIG_DIRS[i]/ for each value in + $XDG_CONFIG_DIRS + Win *: same as site_data_dir + Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.) + + For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False + + WARNING: Do not use this on Windows. See the Vista-Fail note above for why. + """ + if sys.platform in [ "win32", "darwin" ]: + path = site_data_dir(appname, appauthor) + if appname and version: + path = os.path.join(path, version) + else: + # XDG default for $XDG_CONFIG_DIRS + # only first, if multipath is False + path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg') + pathlist = [ os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep) ] + if appname: + if version: + appname = os.path.join(appname, version) + pathlist = [ os.sep.join([x, appname]) for x in pathlist ] + + if multipath: + path = os.pathsep.join(pathlist) + else: + path = pathlist[0] + return path + +def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): + r"""Return full path to the user-specific cache dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only required and used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "opinion" (boolean) can be False to disable the appending of + "Cache" to the base app data dir for Windows. See + discussion below. + + Typical user cache directories are: + Mac OS X: ~/Library/Caches/ + Unix: ~/.cache/ (XDG default) + Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Cache + Vista: C:\Users\\AppData\Local\\\Cache + + On Windows the only suggestion in the MSDN docs is that local settings go in + the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming + app data dir (the default returned by `user_data_dir` above). Apps typically + put cache data somewhere *under* the given dir here. Some examples: + ...\Mozilla\Firefox\Profiles\\Cache + ...\Acme\SuperApp\Cache\1.0 + OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value. + This can be disabled with the `opinion=False` option. + """ + if sys.platform == "win32": + if appauthor is None: + appauthor = appname + path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA")) + if appname: + path = os.path.join(path, appauthor, appname) + if opinion: + path = os.path.join(path, "Cache") + elif sys.platform == 'darwin': + path = os.path.expanduser('~/Library/Caches') + if appname: + path = os.path.join(path, appname) + else: + path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache')) + if appname: + path = os.path.join(path, appname) + if appname and version: + path = os.path.join(path, version) + return path + +def user_log_dir(appname=None, appauthor=None, version=None, opinion=True): + r"""Return full path to the user-specific log dir for this application. + + "appname" is the name of application. + If None, just the system directory is returned. + "appauthor" (only required and used on Windows) is the name of the + appauthor or distributing body for this application. Typically + it is the owning company name. This falls back to appname. + "version" is an optional version path element to append to the + path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this + would typically be ".". + Only applied when appname is present. + "opinion" (boolean) can be False to disable the appending of + "Logs" to the base app data dir for Windows, and "log" to the + base cache dir for Unix. See discussion below. + + Typical user cache directories are: + Mac OS X: ~/Library/Logs/ + Unix: ~/.cache//log # or under $XDG_CACHE_HOME if defined + Win XP: C:\Documents and Settings\\Local Settings\Application Data\\\Logs + Vista: C:\Users\\AppData\Local\\\Logs + + On Windows the only suggestion in the MSDN docs is that local settings + go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in + examples of what some windows apps use for a logs dir.) + + OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA` + value for Windows and appends "log" to the user cache dir for Unix. + This can be disabled with the `opinion=False` option. + """ + if sys.platform == "darwin": + path = os.path.join( + os.path.expanduser('~/Library/Logs'), + appname) + elif sys.platform == "win32": + path = user_data_dir(appname, appauthor, version); version=False + if opinion: + path = os.path.join(path, "Logs") + else: + path = user_cache_dir(appname, appauthor, version); version=False + if opinion: + path = os.path.join(path, "log") + if appname and version: + path = os.path.join(path, version) + return path + + +class AppDirs(object): + """Convenience wrapper for getting application dirs.""" + def __init__(self, appname, appauthor=None, version=None, + roaming=False, multipath=False): + self.appname = appname + self.appauthor = appauthor + self.version = version + self.roaming = roaming + self.multipath = multipath + @property + def user_data_dir(self): + return user_data_dir(self.appname, self.appauthor, + version=self.version, roaming=self.roaming) + @property + def site_data_dir(self): + return site_data_dir(self.appname, self.appauthor, + version=self.version, multipath=self.multipath) + @property + def user_config_dir(self): + return user_config_dir(self.appname, self.appauthor, + version=self.version, roaming=self.roaming) + @property + def site_config_dir(self): + return site_data_dir(self.appname, self.appauthor, + version=self.version, multipath=self.multipath) + @property + def user_cache_dir(self): + return user_cache_dir(self.appname, self.appauthor, + version=self.version) + @property + def user_log_dir(self): + return user_log_dir(self.appname, self.appauthor, + version=self.version) + + + + +#---- internal support stuff + +def _get_win_folder_from_registry(csidl_name): + """This is a fallback technique at best. I'm not sure if using the + registry for this guarantees us the correct answer for all CSIDL_* + names. + """ + import _winreg + + shell_folder_name = { + "CSIDL_APPDATA": "AppData", + "CSIDL_COMMON_APPDATA": "Common AppData", + "CSIDL_LOCAL_APPDATA": "Local AppData", + }[csidl_name] + + key = _winreg.OpenKey(_winreg.HKEY_CURRENT_USER, + r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") + dir, type = _winreg.QueryValueEx(key, shell_folder_name) + return dir + +def _get_win_folder_with_pywin32(csidl_name): + from win32com.shell import shellcon, shell + dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0) + # Try to make this a unicode path because SHGetFolderPath does + # not return unicode strings when there is unicode data in the + # path. + try: + dir = unicode(dir) + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in dir: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + try: + import win32api + dir = win32api.GetShortPathName(dir) + except ImportError: + pass + except UnicodeError: + pass + return dir + +def _get_win_folder_with_ctypes(csidl_name): + import ctypes + + csidl_const = { + "CSIDL_APPDATA": 26, + "CSIDL_COMMON_APPDATA": 35, + "CSIDL_LOCAL_APPDATA": 28, + }[csidl_name] + + buf = ctypes.create_unicode_buffer(1024) + ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) + + # Downgrade to short path name if have highbit chars. See + # . + has_high_char = False + for c in buf: + if ord(c) > 255: + has_high_char = True + break + if has_high_char: + buf2 = ctypes.create_unicode_buffer(1024) + if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): + buf = buf2 + + return buf.value + +if sys.platform == "win32": + try: + import win32com.shell + _get_win_folder = _get_win_folder_with_pywin32 + except ImportError: + try: + import ctypes + _get_win_folder = _get_win_folder_with_ctypes + except ImportError: + _get_win_folder = _get_win_folder_from_registry + + + +#---- self test code + +if __name__ == "__main__": + appname = "MyApp" + appauthor = "MyCompany" + + props = ("user_data_dir", "site_data_dir", + "user_config_dir", "site_config_dir", + "user_cache_dir", "user_log_dir") + + print("-- app dirs (with optional 'version')") + dirs = AppDirs(appname, appauthor, version="1.0") + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + + print("\n-- app dirs (without optional 'version')") + dirs = AppDirs(appname, appauthor) + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + + print("\n-- app dirs (without optional 'appauthor')") + dirs = AppDirs(appname) + for prop in props: + print("%s: %s" % (prop, getattr(dirs, prop))) + From 5269664102a0b8a607d7f29f6102ebfce023922b Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Wed, 15 Jan 2014 19:03:13 +0100 Subject: [PATCH 017/448] [FIX] correct uses of addons_path bzr revid: chs@openerp.com-20140115180313-pqcrfmstci2w21y8 --- .../base/module/wizard/base_module_import.py | 2 - openerp/cli/server.py | 2 +- openerp/modules/module.py | 14 ++++--- openerp/service/server.py | 5 +-- openerp/tests/common.py | 1 - openerp/tools/translate.py | 37 +++++-------------- 6 files changed, 22 insertions(+), 39 deletions(-) diff --git a/openerp/addons/base/module/wizard/base_module_import.py b/openerp/addons/base/module/wizard/base_module_import.py index 6d6f0e1ef8d..2ba789f3332 100644 --- a/openerp/addons/base/module/wizard/base_module_import.py +++ b/openerp/addons/base/module/wizard/base_module_import.py @@ -28,8 +28,6 @@ from openerp import tools from openerp.osv import osv, fields from openerp.tools.translate import _ -ADDONS_PATH = tools.config['addons_path'].split(",")[-1] - class base_module_import(osv.osv_memory): """ Import Module """ diff --git a/openerp/cli/server.py b/openerp/cli/server.py index ee2e90bccbd..7041d7a4c3c 100644 --- a/openerp/cli/server.py +++ b/openerp/cli/server.py @@ -72,7 +72,7 @@ def report_configuration(): """ config = openerp.tools.config _logger.info("OpenERP version %s", __version__) - for name, value in [('addons paths', config['addons_path']), + for name, value in [('addons paths', openerp.modules.module.ad_paths), ('database hostname', config['db_host'] or 'localhost'), ('database port', config['db_port'] or '5432'), ('database user', config['db_user'])]: diff --git a/openerp/modules/module.py b/openerp/modules/module.py index cf3ff61805e..98511234b75 100644 --- a/openerp/modules/module.py +++ b/openerp/modules/module.py @@ -3,7 +3,7 @@ # # OpenERP, Open Source Management Solution # Copyright (C) 2004-2009 Tiny SPRL (). -# Copyright (C) 2010-2012 OpenERP s.a. (). +# Copyright (C) 2010-2014 OpenERP s.a. (). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -45,7 +45,6 @@ import logging _logger = logging.getLogger(__name__) _test_logger = logging.getLogger('openerp.tests') -_ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base) ad_paths = [] # Modules already loaded @@ -97,8 +96,13 @@ def initialize_sys_path(): if ad_paths: return - ad_paths = map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(',')) - ad_paths.append(os.path.abspath(_ad)) # for get_module_path + ad_paths = [tools.config.addons_data_dir] + ad_paths += map(lambda m: os.path.abspath(tools.ustr(m.strip())), tools.config['addons_path'].split(',')) + + # add base module path + base_path = os.path.abspath(os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons')) + ad_paths += [base_path] + sys.meta_path.append(AddonsImportHook()) def get_module_path(module, downloaded=False, display_warning=True): @@ -115,7 +119,7 @@ def get_module_path(module, downloaded=False, display_warning=True): return opj(adp, module) if downloaded: - return opj(_ad, module) + return opj(tools.config.addons_data_dir, module) if display_warning: _logger.warning('module %s: module not found', module) return False diff --git a/openerp/service/server.py b/openerp/service/server.py index 87f10010645..e7fcb610152 100644 --- a/openerp/service/server.py +++ b/openerp/service/server.py @@ -108,15 +108,14 @@ class AutoReload(object): self.handler = EventHandler(self) self.notifier = pyinotify.Notifier(self.wm, self.handler, timeout=0) mask = pyinotify.IN_MODIFY | pyinotify.IN_CREATE # IN_MOVED_FROM, IN_MOVED_TO ? - for path in openerp.tools.config.options["addons_path"].split(','): + for path in openerp.modules.modules.ad_paths: _logger.info('Watching addons folder %s', path) self.wm.add_watch(path, mask, rec=True) def process_data(self, files): xml_files = [i for i in files if i.endswith('.xml')] - addons_path = openerp.tools.config.options["addons_path"].split(',') for i in xml_files: - for path in addons_path: + for path in openerp.modules.modules.ad_paths: if i.startswith(path): # find out wich addons path the file belongs to # and extract it's module name diff --git a/openerp/tests/common.py b/openerp/tests/common.py index df00e61f140..7bd8bf76cc9 100644 --- a/openerp/tests/common.py +++ b/openerp/tests/common.py @@ -11,7 +11,6 @@ import xmlrpclib import openerp # The openerp library is supposed already configured. -ADDONS_PATH = openerp.tools.config['addons_path'] PORT = openerp.tools.config['xmlrpc_port'] DB = openerp.tools.config['db_name'] diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index f82fa4b87ad..72cf4ec1d0b 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -778,49 +778,32 @@ def trans_generate(lang, modules, cr): if model_obj._sql_constraints: push_local_constraints(module, model_obj, 'sql_constraints') - def get_module_from_path(path, mod_paths=None): - if not mod_paths: - # First, construct a list of possible paths - def_path = os.path.abspath(os.path.join(config.config['root_path'], 'addons')) # default addons path (base) - ad_paths= map(lambda m: os.path.abspath(m.strip()),config.config['addons_path'].split(',')) - mod_paths=[def_path] - for adp in ad_paths: - mod_paths.append(adp) - if not os.path.isabs(adp): - mod_paths.append(adp) - elif adp.startswith(def_path): - mod_paths.append(adp[len(def_path)+1:]) - for mp in mod_paths: - if path.startswith(mp) and (os.path.dirname(path) != mp): - path = path[len(mp)+1:] - return path.split(os.path.sep)[0] - return 'base' # files that are not in a module are considered as being in 'base' module modobj = registry['ir.module.module'] installed_modids = modobj.search(cr, uid, [('state', '=', 'installed')]) installed_modules = map(lambda m: m['name'], modobj.read(cr, uid, installed_modids, ['name'])) - root_path = os.path.join(config.config['root_path'], 'addons') - - apaths = map(os.path.abspath, map(str.strip, config.config['addons_path'].split(','))) - if root_path in apaths: - path_list = apaths - else : - path_list = [root_path,] + apaths - + path_list = list(openerp.modules.module.ad_paths) # Also scan these non-addon paths for bin_path in ['osv', 'report' ]: path_list.append(os.path.join(config.config['root_path'], bin_path)) _logger.debug("Scanning modules at paths: ", path_list) - mod_paths = [] + mod_paths = list(path_list) + + def get_module_from_path(path): + for mp in mod_paths: + if path.startswith(mp) and (os.path.dirname(path) != mp): + path = path[len(mp)+1:] + return path.split(os.path.sep)[0] + return 'base' # files that are not in a module are considered as being in 'base' module def verified_module_filepaths(fname, path, root): fabsolutepath = join(root, fname) frelativepath = fabsolutepath[len(path):] display_path = "addons%s" % frelativepath - module = get_module_from_path(fabsolutepath, mod_paths=mod_paths) + module = get_module_from_path(fabsolutepath) if ('all' in modules or module in modules) and module in installed_modules: return module, fabsolutepath, frelativepath, display_path return None, None, None, None From a73c02075afad47da4edae53659c14d71e1a303f Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 16 Jan 2014 11:02:43 +0100 Subject: [PATCH 018/448] [FIX] ensure data-dir (and subdirs) exists bzr revid: chs@openerp.com-20140116100243-ltp6yunvm1bnvn5q --- openerp/tools/config.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 101724f4159..bd87a0d4b44 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -626,11 +626,17 @@ class configmanager(object): @property def addons_data_dir(self): - return os.path.join(self['data_dir'], 'addons', release.series) + d = os.path.join(self['data_dir'], 'addons', release.series) + if not os.path.exists(d): + os.makedirs(d) + return d @property def session_dir(self): - return os.path.join(self['data_dir'], 'sessions') + d = os.path.join(self['data_dir'], 'sessions') + if not os.path.exists(d): + os.makedirs(d) + return d config = configmanager() From db10e1d3a51cd0e315ee8e4fcc80ffe9b6499fbf Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 16 Jan 2014 17:41:42 +0100 Subject: [PATCH 019/448] [FIX] http: store sessions in data-dir bzr revid: chs@openerp.com-20140116164142-ikcw82lyfopoj46k --- openerp/http.py | 23 +---------------------- openerp/tools/config.py | 2 +- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/openerp/http.py b/openerp/http.py index 7069cbdeefe..9b632348280 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -830,33 +830,12 @@ class DisableCacheMiddleware(object): start_response(status, new_headers) return self.app(environ, start_wrapped) -def session_path(): - try: - import pwd - username = pwd.getpwuid(os.geteuid()).pw_name - except ImportError: - try: - username = getpass.getuser() - except Exception: - username = "unknown" - path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username) - try: - os.mkdir(path, 0700) - except OSError as exc: - if exc.errno == errno.EEXIST: - # directory exists: ensure it has the correct permissions - # this will fail if the directory is not owned by the current user - os.chmod(path, 0700) - else: - raise - return path - class Root(object): """Root WSGI application for the OpenERP Web Client. """ def __init__(self): # Setup http sessions - path = session_path() + path = os.path.join(openerp.tools.config.session_dir _logger.debug('HTTP sessions stored in: %s', path) self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession) diff --git a/openerp/tools/config.py b/openerp/tools/config.py index bd87a0d4b44..a9e4a8a3276 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -633,7 +633,7 @@ class configmanager(object): @property def session_dir(self): - d = os.path.join(self['data_dir'], 'sessions') + d = os.path.join(self['data_dir'], 'sessions', release.series) if not os.path.exists(d): os.makedirs(d) return d From c48f11041bf46ccbc880aba6c3c90f79e932c68f Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 16 Jan 2014 17:44:06 +0100 Subject: [PATCH 020/448] [IMP] chmod data-dir bzr revid: chs@openerp.com-20140116164406-p2gtb2uziwhchp68 --- openerp/tools/config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openerp/tools/config.py b/openerp/tools/config.py index a9e4a8a3276..90013cfedd0 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -628,14 +628,18 @@ class configmanager(object): def addons_data_dir(self): d = os.path.join(self['data_dir'], 'addons', release.series) if not os.path.exists(d): - os.makedirs(d) + os.makedirs(d, 0700) + else: + os.chmod(d, 0700) return d @property def session_dir(self): d = os.path.join(self['data_dir'], 'sessions', release.series) if not os.path.exists(d): - os.makedirs(d) + os.makedirs(d, 0700) + else: + os.chmod(d, 0700) return d config = configmanager() From 3275de59815c2846d6385637c920d0724934c326 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 16 Jan 2014 19:54:15 +0100 Subject: [PATCH 021/448] [IMP] ir.attachment: filestore is now in data-dir and by default bzr revid: chs@openerp.com-20140116185415-ajia02bsty9joox7 --- openerp/addons/base/ir/ir_attachment.py | 65 ++++++----- .../addons/base/tests/test_ir_attachment.py | 108 +++++++++--------- openerp/service/db.py | 7 +- 3 files changed, 95 insertions(+), 85 deletions(-) diff --git a/openerp/addons/base/ir/ir_attachment.py b/openerp/addons/base/ir/ir_attachment.py index f6bb00fed5f..616b4168c60 100644 --- a/openerp/addons/base/ir/ir_attachment.py +++ b/openerp/addons/base/ir/ir_attachment.py @@ -63,19 +63,38 @@ class ir_attachment(osv.osv): data[attachment.id] = False return data + def _storage(self, cr, uid, context=None): + return self.pool['ir.config_parameter'].get_param(cr, SUPERUSER_ID, 'ir_attachment.location', 'file') + + @tools.ormcache() + def _filestore(self, cr, uid, context=None): + dbuuid = self.pool['ir.config_paramater'].get_param(cr, SUPERUSER_ID, 'database.uuid') + return os.path.join(tools.config['data_dir'], 'filestore', dbuuid) + # 'data' field implementation def _full_path(self, cr, uid, location, path): - # location = 'file:filestore' - assert location.startswith('file:'), "Unhandled filestore location %s" % location - location = location[5:] - - # sanitize location name and path - location = re.sub('[.]','',location) - location = location.strip('/\\') - - path = re.sub('[.]','',path) + # sanitize ath + path = re.sub('[.]', '', path) path = path.strip('/\\') - return os.path.join(tools.config['root_path'], location, cr.dbname, path) + return os.path.join(self._filestore(cr, uid), path) + + def _get_path(self, cr, uid, location, bin_data): + sha = hashlib.sha1(bin_data).hexdigest() + + # retro compatibility + fname = sha[:3] + '/' + sha + full_path = self._full_path(cr, uid, location, fname) + if os.path.isfile(full_path): + return fname, full_path # keep existing path + + # scatter files across 256 dirs + # we use '/' in the db (even on windows) + fname = sha[:2] + '/' + sha + full_path = self._full_path(cr, uid, location, fname) + dirname = os.path.dirname(full_path) + if not os.path.isdir(dirname): + os.makedirs(dirname) + return fname, full_path def _file_read(self, cr, uid, location, fname, bin_size=False): full_path = self._full_path(cr, uid, location, fname) @@ -91,18 +110,13 @@ class ir_attachment(osv.osv): def _file_write(self, cr, uid, location, value): bin_value = value.decode('base64') - fname = hashlib.sha1(bin_value).hexdigest() - # scatter files across 1024 dirs - # we use '/' in the db (even on windows) - fname = fname[:3] + '/' + fname - full_path = self._full_path(cr, uid, location, fname) - try: - dirname = os.path.dirname(full_path) - if not os.path.isdir(dirname): - os.makedirs(dirname) - open(full_path,'wb').write(bin_value) - except IOError: - _logger.error("_file_write writing %s",full_path) + fname, full_path = self._get_path(cr, uid, location, bin_value) + if not os.path.exists(full_path): + try: + with open(full_path, 'wb') as fp: + fp.write(bin_value) + except IOError: + _logger.error("_file_write writing %s", full_path) return fname def _file_delete(self, cr, uid, location, fname): @@ -121,7 +135,7 @@ class ir_attachment(osv.osv): if context is None: context = {} result = {} - location = self.pool.get('ir.config_parameter').get_param(cr, uid, 'ir_attachment.location') + location = self._storage(cr, uid, context) bin_size = context.get('bin_size') for attach in self.browse(cr, uid, ids, context=context): if location and attach.store_fname: @@ -136,7 +150,7 @@ class ir_attachment(osv.osv): return True if context is None: context = {} - location = self.pool.get('ir.config_parameter').get_param(cr, uid, 'ir_attachment.location') + location = self._storage(cr, uid, context) file_size = len(value.decode('base64')) if location: attach = self.browse(cr, uid, id, context=context) @@ -284,7 +298,7 @@ class ir_attachment(osv.osv): if isinstance(ids, (int, long)): ids = [ids] self.check(cr, uid, ids, 'unlink', context=context) - location = self.pool.get('ir.config_parameter').get_param(cr, uid, 'ir_attachment.location') + location = self._storage(cr, uid, context) if location: for attach in self.browse(cr, uid, ids, context=context): if attach.store_fname: @@ -302,4 +316,3 @@ class ir_attachment(osv.osv): cr, uid, 'base', 'action_attachment', context=context) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/openerp/addons/base/tests/test_ir_attachment.py b/openerp/addons/base/tests/test_ir_attachment.py index 4d96ec42b28..fd2b4304eae 100644 --- a/openerp/addons/base/tests/test_ir_attachment.py +++ b/openerp/addons/base/tests/test_ir_attachment.py @@ -1,89 +1,91 @@ import hashlib import os -import unittest2 - import openerp import openerp.tests.common -class test_ir_attachment(openerp.tests.common.TransactionCase): +HASH_SPLIT = 2 # FIXME: testing implementations detail is not a good idea - def test_00_attachment_flow(self): +class test_ir_attachment(openerp.tests.common.TransactionCase): + def setUp(self): registry, cr, uid = self.registry, self.cr, self.uid - root_path = openerp.tools.config['root_path'] - ira = registry('ir.attachment') + self.ira = registry('ir.attachment') + self.filestore = self.ira._filestore(cr, uid) # Blob1 - blob1 = 'blob1' - blob1_b64 = blob1.encode('base64') - blob1_hash = hashlib.sha1(blob1).hexdigest() - blob1_fname = blob1_hash[:3] + '/' + blob1_hash + self.blob1 = 'blob1' + self.blob1_b64 = self.blob1.encode('base64') + blob1_hash = hashlib.sha1(self.blob1).hexdigest() + self.blob1_fname = blob1_hash[:HASH_SPLIT] + '/' + blob1_hash # Blob2 blob2 = 'blob2' - blob2_b64 = blob2.encode('base64') - blob2_hash = hashlib.sha1(blob2).hexdigest() - blob2_fname = blob2_hash[:3] + '/' + blob2_hash + self.blob2_b64 = blob2.encode('base64') + + def test_01_store_in_db(self): + registry, cr, uid = self.registry, self.cr, self.uid + + # force storing in database + registry('ir.config_parameter').set_param(cr, uid, 'ir_attachment.location', '') # 'ir_attachment.location' is undefined test database storage - a1 = ira.create(cr, uid, {'name': 'a1', 'datas': blob1_b64}) - a1_read = ira.read(cr, uid, [a1], ['datas']) - self.assertEqual(a1_read[0]['datas'], blob1_b64) + a1 = self.ira.create(cr, uid, {'name': 'a1', 'datas': self.blob1_b64}) + a1_read = self.ira.read(cr, uid, [a1], ['datas']) + self.assertEqual(a1_read[0]['datas'], self.blob1_b64) - cr.execute("select id,db_datas from ir_attachment where id = %s", (a1,) ) - a1_db_datas = str(cr.fetchall()[0][1]) - self.assertEqual(a1_db_datas, blob1_b64) + a1_db_datas = self.ira.browse(cr, uid, a1).db_datas + self.assertEqual(a1_db_datas, self.blob1_b64) - # define a location for filestore - registry('ir.config_parameter').set_param(cr, uid, 'ir_attachment.location', 'file:///filestore') + def test_02_store_on_disk(self): + registry, cr, uid = self.registry, self.cr, self.uid - # Test file storage - a2 = ira.create(cr, uid, {'name': 'a2', 'datas': blob1_b64}) - a2_read = ira.read(cr, uid, [a2], ['datas']) - self.assertEqual(a2_read[0]['datas'], blob1_b64) + a2 = self.ira.create(cr, uid, {'name': 'a2', 'datas': self.blob1_b64}) + a2_store_fname = self.ira.browse(cr, uid, a2).store_fname - cr.execute("select id,store_fname from ir_attachment where id = %s", (a2,) ) - a2_store_fname = cr.fetchall()[0][1] - self.assertEqual(a2_store_fname, blob1_fname) + self.assertEqual(a2_store_fname, self.blob1_fname) + self.assertTrue(os.path.isdir(os.path.join(self.filestore, a2_store_fname))) - a2_fn = os.path.join(root_path, 'filestore', cr.dbname, blob1_hash[:3], blob1_hash) - fc = file(a2_fn).read() - self.assertEqual(fc, blob1) + def test_03_no_duplication(self): + registry, cr, uid = self.registry, self.cr, self.uid - # create a3 with same blob - a3 = ira.create(cr, uid, {'name': 'a3', 'datas': blob1_b64}) - a3_read = ira.read(cr, uid, [a3], ['datas']) - self.assertEqual(a3_read[0]['datas'], blob1_b64) + a2 = self.ira.create(cr, uid, {'name': 'a2', 'datas': self.blob1_b64}) + a2_store_fname = self.ira.browse(cr, uid, a2).store_fname + + a3 = self.ira.create(cr, uid, {'name': 'a3', 'datas': self.blob1_b64}) + a3_store_fname = self.ira.browse(cr, uid, a3).store_fname - cr.execute("select id,store_fname from ir_attachment where id = %s", (a3,) ) - a3_store_fname = cr.fetchall()[0][1] self.assertEqual(a3_store_fname, a2_store_fname) - # create a4 blob2 - a4 = ira.create(cr, uid, {'name': 'a4', 'datas': blob2_b64}) - a4_read = ira.read(cr, uid, [a4], ['datas']) - self.assertEqual(a4_read[0]['datas'], blob2_b64) + def test_04_keep_file(self): + registry, cr, uid = self.registry, self.cr, self.uid - a4_fn = os.path.join(root_path, 'filestore', cr.dbname, blob2_hash[:3], blob2_hash) - self.assertTrue(os.path.isfile(a4_fn)) + a2 = self.ira.create(cr, uid, {'name': 'a2', 'datas': self.blob1_b64}) + a3 = self.ira.create(cr, uid, {'name': 'a3', 'datas': self.blob1_b64}) - # delete a3 but file stays - ira.unlink(cr, uid, [a3]) + a2_store_fname = self.ira.browse(cr, uid, a2).store_fname + a2_fn = os.path.join(self.filestore, a2_store_fname) + + self.ira.unlink(cr, uid, [a3]) self.assertTrue(os.path.isfile(a2_fn)) # delete a2 it is unlinked - ira.unlink(cr, uid, [a2]) + self.ira.unlink(cr, uid, [a2]) self.assertFalse(os.path.isfile(a2_fn)) - # update a4 blob2 by blob1 - ira.write(cr, uid, [a4], {'datas': blob1_b64}) - a4_read = ira.read(cr, uid, [a4], ['datas']) - self.assertEqual(a4_read[0]['datas'], blob1_b64) + def test_05_change_data_change_file(self): + registry, cr, uid = self.registry, self.cr, self.uid + + a2 = self.ira.create(cr, uid, {'name': 'a2', 'datas': self.blob1_b64}) + a2_store_fname = self.ira.browse(cr, uid, a2).store_fname + a2_fn = os.path.join(self.filestore, a2_store_fname) - # file of a4 disapear and a2 reappear - self.assertFalse(os.path.isfile(a4_fn)) self.assertTrue(os.path.isfile(a2_fn)) - # everybody applause + self.ira.write(cr, uid, [a2], {'datas': self.blob2_b64}) + self.assertFalse(os.path.isfile(a2_fn)) + new_a2_store_fname = self.ira.browse(cr, uid, a2).store_fname + self.assertNotEqual(a2_store_fname, new_a2_store_fname) + new_a2_fn = os.path.join(self.filestore, new_a2_store_fname) + self.assertTrue(os.path.isfile(new_a2_fn)) diff --git a/openerp/service/db.py b/openerp/service/db.py index 1381b5b1e04..acdb8fe2ed9 100644 --- a/openerp/service/db.py +++ b/openerp/service/db.py @@ -276,15 +276,10 @@ def exp_rename(old_name, new_name): cr.autocommit(True) # avoid transaction block try: cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name)) + _logger.info('RENAME DB: %s -> %s', old_name, new_name) except Exception, e: _logger.error('RENAME DB: %s -> %s failed:\n%s', old_name, new_name, e) raise Exception("Couldn't rename database %s to %s: %s" % (old_name, new_name, e)) - else: - fs = os.path.join(openerp.tools.config['root_path'], 'filestore') - if os.path.exists(os.path.join(fs, old_name)): - os.rename(os.path.join(fs, old_name), os.path.join(fs, new_name)) - - _logger.info('RENAME DB: %s -> %s', old_name, new_name) return True def exp_db_exist(db_name): From 080cbb88a32ba603e33a6ef428d65d32cb4305e5 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 16 Jan 2014 21:26:01 +0100 Subject: [PATCH 022/448] [FIX] works better without SyntaxError bzr revid: chs@openerp.com-20140116202601-ckn20h6yhiufzonq --- openerp/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/http.py b/openerp/http.py index 9b632348280..2bd0f48565f 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -835,7 +835,7 @@ class Root(object): """ def __init__(self): # Setup http sessions - path = os.path.join(openerp.tools.config.session_dir + path = openerp.tools.config.session_dir _logger.debug('HTTP sessions stored in: %s', path) self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession) From 45d24fd92b66a06e36598e682310435b322d8d01 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 16 Jan 2014 22:47:22 +0100 Subject: [PATCH 023/448] [FIX] ir.attachement: typo s/config_paramater/config_parameter/ bzr revid: chs@openerp.com-20140116214722-4804mskx7c21ikk5 --- openerp/addons/base/ir/ir_attachment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_attachment.py b/openerp/addons/base/ir/ir_attachment.py index 616b4168c60..85770669aac 100644 --- a/openerp/addons/base/ir/ir_attachment.py +++ b/openerp/addons/base/ir/ir_attachment.py @@ -68,7 +68,7 @@ class ir_attachment(osv.osv): @tools.ormcache() def _filestore(self, cr, uid, context=None): - dbuuid = self.pool['ir.config_paramater'].get_param(cr, SUPERUSER_ID, 'database.uuid') + dbuuid = self.pool['ir.config_parameter'].get_param(cr, SUPERUSER_ID, 'database.uuid') return os.path.join(tools.config['data_dir'], 'filestore', dbuuid) # 'data' field implementation From 895b3c96f68d74d99d5fa311670a6e75554f5865 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 16 Jan 2014 23:14:03 +0100 Subject: [PATCH 024/448] [FIX] test ir_attachment: call super() bzr revid: chs@openerp.com-20140116221403-h6h9cvt9rn8y2yj1 --- openerp/addons/base/tests/test_ir_attachment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openerp/addons/base/tests/test_ir_attachment.py b/openerp/addons/base/tests/test_ir_attachment.py index fd2b4304eae..9c02ce6265f 100644 --- a/openerp/addons/base/tests/test_ir_attachment.py +++ b/openerp/addons/base/tests/test_ir_attachment.py @@ -8,6 +8,7 @@ HASH_SPLIT = 2 # FIXME: testing implementations detail is not a good idea class test_ir_attachment(openerp.tests.common.TransactionCase): def setUp(self): + super(test_ir_attachment, self).setUp() registry, cr, uid = self.registry, self.cr, self.uid self.ira = registry('ir.attachment') self.filestore = self.ira._filestore(cr, uid) From c55079f1121b215194d6c0d827d068da7b0ea335 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 16 Jan 2014 23:40:58 +0100 Subject: [PATCH 025/448] [IMP] ir.attachment: active db storage by setting setting to "db" bzr revid: chs@openerp.com-20140116224058-v53fozipvi0obmq1 --- openerp/addons/base/ir/ir_attachment.py | 6 +++--- openerp/addons/base/tests/test_ir_attachment.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/ir/ir_attachment.py b/openerp/addons/base/ir/ir_attachment.py index 85770669aac..ca61fb46fc7 100644 --- a/openerp/addons/base/ir/ir_attachment.py +++ b/openerp/addons/base/ir/ir_attachment.py @@ -138,7 +138,7 @@ class ir_attachment(osv.osv): location = self._storage(cr, uid, context) bin_size = context.get('bin_size') for attach in self.browse(cr, uid, ids, context=context): - if location and attach.store_fname: + if location != 'db' and attach.store_fname: result[attach.id] = self._file_read(cr, uid, location, attach.store_fname, bin_size) else: result[attach.id] = attach.db_datas @@ -152,7 +152,7 @@ class ir_attachment(osv.osv): context = {} location = self._storage(cr, uid, context) file_size = len(value.decode('base64')) - if location: + if location != 'db': attach = self.browse(cr, uid, id, context=context) if attach.store_fname: self._file_delete(cr, uid, location, attach.store_fname) @@ -299,7 +299,7 @@ class ir_attachment(osv.osv): ids = [ids] self.check(cr, uid, ids, 'unlink', context=context) location = self._storage(cr, uid, context) - if location: + if location != 'db': for attach in self.browse(cr, uid, ids, context=context): if attach.store_fname: self._file_delete(cr, uid, location, attach.store_fname) diff --git a/openerp/addons/base/tests/test_ir_attachment.py b/openerp/addons/base/tests/test_ir_attachment.py index 9c02ce6265f..a5e546a90c4 100644 --- a/openerp/addons/base/tests/test_ir_attachment.py +++ b/openerp/addons/base/tests/test_ir_attachment.py @@ -27,7 +27,7 @@ class test_ir_attachment(openerp.tests.common.TransactionCase): registry, cr, uid = self.registry, self.cr, self.uid # force storing in database - registry('ir.config_parameter').set_param(cr, uid, 'ir_attachment.location', '') + registry('ir.config_parameter').set_param(cr, uid, 'ir_attachment.location', 'db') # 'ir_attachment.location' is undefined test database storage a1 = self.ira.create(cr, uid, {'name': 'a1', 'datas': self.blob1_b64}) From 143f52039500a420a42ee53d1ef5a411930c9d00 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Thu, 16 Jan 2014 23:42:07 +0100 Subject: [PATCH 026/448] [FIX] test ir_attachment: files are not directories bzr revid: chs@openerp.com-20140116224207-m2s5ljkb1t7q54tk --- openerp/addons/base/tests/test_ir_attachment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/tests/test_ir_attachment.py b/openerp/addons/base/tests/test_ir_attachment.py index a5e546a90c4..891b38b7dc3 100644 --- a/openerp/addons/base/tests/test_ir_attachment.py +++ b/openerp/addons/base/tests/test_ir_attachment.py @@ -44,7 +44,7 @@ class test_ir_attachment(openerp.tests.common.TransactionCase): a2_store_fname = self.ira.browse(cr, uid, a2).store_fname self.assertEqual(a2_store_fname, self.blob1_fname) - self.assertTrue(os.path.isdir(os.path.join(self.filestore, a2_store_fname))) + self.assertTrue(os.path.isfile(os.path.join(self.filestore, a2_store_fname))) def test_03_no_duplication(self): registry, cr, uid = self.registry, self.cr, self.uid From 14815d669ff5ae428a74c7b81327667cdbcf44f9 Mon Sep 17 00:00:00 2001 From: Christophe Simonis Date: Fri, 17 Jan 2014 17:14:24 +0100 Subject: [PATCH 027/448] [FIX] attachments: filestore use dbname instead of dbuuid bzr revid: chs@openerp.com-20140117161424-i1ggvzawkjrabbwc --- openerp/addons/base/ir/ir_attachment.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_attachment.py b/openerp/addons/base/ir/ir_attachment.py index 4fb5f6f16e5..d9089a25f96 100644 --- a/openerp/addons/base/ir/ir_attachment.py +++ b/openerp/addons/base/ir/ir_attachment.py @@ -68,8 +68,7 @@ class ir_attachment(osv.osv): @tools.ormcache() def _filestore(self, cr, uid, context=None): - dbuuid = self.pool['ir.config_parameter'].get_param(cr, SUPERUSER_ID, 'database.uuid') - return os.path.join(tools.config['data_dir'], 'filestore', dbuuid) + return os.path.join(tools.config['data_dir'], 'filestore', cr.dbname) # 'data' field implementation def _full_path(self, cr, uid, location, path): From 476a0b620dcc563420191449daabbecbcc179150 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (Acsone)" Date: Fri, 31 Jan 2014 17:32:10 +0100 Subject: [PATCH 028/448] [FIX] ir_cron: use the same search criteria as when listing job to excecyte when acquiring the lock on the job before its execution to prevent running already executed job bzr revid: laurent.mignon@acsone.eu-20140131163210-2sw8f2e5n4my5o5l --- openerp/addons/base/ir/ir_cron.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 24a522f6309..77269c61550 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -216,12 +216,21 @@ class ir_cron(osv.osv): lock_cr = db.cursor() try: # Try to grab an exclusive lock on the job row from within the task transaction + # Restrict to the same conditions as for the search since the job may have already + # been run by an other thread when cron is running in multi thread lock_cr.execute("""SELECT * FROM ir_cron - WHERE id=%s + WHERE numbercall != 0 + AND active + AND nextcall <= (now() at time zone 'UTC') + AND id=%s FOR UPDATE NOWAIT""", (job['id'],), log_exceptions=False) + locked_job = lock_cr.fetchone() + if not locked_job: + _logger.debug("Job already %s executed by another process/thread. skipping it", job['name']) + continue # Got the lock on the job row, run its code _logger.debug('Starting job `%s`.', job['name']) job_cr = db.cursor() From 6abf20906e7b5d3e319a227301fbcf1a6d4492b7 Mon Sep 17 00:00:00 2001 From: jke-openerp Date: Sat, 1 Feb 2014 21:04:13 +0100 Subject: [PATCH 029/448] [Typo] Replace t-esc in t-field for amount_untaxed in checkout of website_sale. Else display monetary does not work bzr revid: jke@openerp.com-20140201200413-zww5pcobl6vmvqe1 --- addons/website_sale/views/website_sale.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website_sale/views/website_sale.xml b/addons/website_sale/views/website_sale.xml index ed2007e27c7..cc1220a57e7 100644 --- a/addons/website_sale/views/website_sale.xml +++ b/addons/website_sale/views/website_sale.xml @@ -832,7 +832,7 @@
Subtotal:
-
From c18d3504689228807d6703b920b43d6c4e6e6721 Mon Sep 17 00:00:00 2001 From: jke-openerp Date: Sat, 1 Feb 2014 21:47:41 +0100 Subject: [PATCH 030/448] [FIX] Fix bug where button with contenteditable does not work in cross browser. Fix : replace tag 'button' by 'a' with class 'a-submit' bzr revid: jke@openerp.com-20140201204741-ygdxaauq0m5axttg --- addons/website_sale/static/src/js/website_sale.js | 4 ++++ addons/website_sale/views/website_sale.xml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 7f6d24373f7..eedbb5a046c 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -60,6 +60,10 @@ $(document).ready(function () { return false; }); + $('.a-submit').on('click', function () { + $(this).closest('form').submit(); + }); + // change price when they are variants $('form.js_add_cart_json label').on('mouseup', function (ev) { ev.preventDefault(); diff --git a/addons/website_sale/views/website_sale.xml b/addons/website_sale/views/website_sale.xml index ed2007e27c7..d6c86c81e8b 100644 --- a/addons/website_sale/views/website_sale.xml +++ b/addons/website_sale/views/website_sale.xml @@ -331,7 +331,7 @@ }'/>
- + Add to Cart


From ef974c7fe5d221c3b1de68f2b6eee2a30b2c01d7 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (Acsone)" Date: Wed, 5 Feb 2014 10:14:52 +0100 Subject: [PATCH 031/448] Fix log message bzr revid: laurent.mignon@acsone.eu-20140205091452-6mamu14lpevyjy28 --- openerp/addons/base/ir/ir_cron.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 77269c61550..77c9835d9be 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -229,7 +229,7 @@ class ir_cron(osv.osv): locked_job = lock_cr.fetchone() if not locked_job: - _logger.debug("Job already %s executed by another process/thread. skipping it", job['name']) + _logger.debug("Job `%s` already executed by another process/thread. skipping it", job['name']) continue # Got the lock on the job row, run its code _logger.debug('Starting job `%s`.', job['name']) From 763496be49813da85aecb518116f7ccc7f30eac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 6 Feb 2014 10:48:31 +0100 Subject: [PATCH 032/448] [IMP] mail: added message_last_update field. This field is a datetime field, updated everytime message_post in called. This allows to build filters, search or actions based on the last message date, for reporting for example. bzr revid: tde@openerp.com-20140206094831-nvyds6nqz0v25t59 --- addons/mail/mail_thread.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index ee4dcac5414..c1006182000 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -296,6 +296,9 @@ class mail_thread(osv.AbstractModel): auto_join=True, string='Messages', help="Messages and communication history"), + 'message_last_update': fields.datetime('Last Message Date', + help='Date of the last message posted on the record. Only messages going' + 'through the message_post API are taken into account for better performances.'), 'message_unread': fields.function(_get_message_data, fnct_search=_search_message_unread, multi="_get_message_data", type='boolean', string='Unread Messages', @@ -1522,8 +1525,13 @@ class mail_thread(osv.AbstractModel): for x in ('from', 'to', 'cc'): values.pop(x, None) - # Create and auto subscribe the author + # Post the message msg_id = mail_message.create(cr, uid, values, context=context) + + # Post-process: subscribe author, update message_last_update + if model and model != 'mail.thread' and thread_id: + # done with SUPERUSER_ID, because on some models users can post only with read access, not necessarily write access + self.write(cr, SUPERUSER_ID, [thread_id], {'message_last_update': fields.datetime.now()}, context=context) message = mail_message.browse(cr, uid, msg_id, context=context) if message.author_id and thread_id and type != 'notification' and not context.get('mail_create_nosubscribe'): self.message_subscribe(cr, uid, [thread_id], [message.author_id.id], context=context) From 71be2de1efd510a4f9c2c91e672bbe3e32ca5b2b Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Wed, 12 Feb 2014 18:22:14 +0100 Subject: [PATCH 033/448] [IMP] Add structure of the 'report' module bzr revid: openerp-sle@openerp-sle.home-20140212172214-r0n8wzngr1hkp8q0 --- addons/report/__init__.py | 3 +++ addons/report/__openerp__.py | 14 ++++++++++++++ addons/report/models/report.py | 0 3 files changed, 17 insertions(+) create mode 100644 addons/report/__init__.py create mode 100644 addons/report/__openerp__.py create mode 100644 addons/report/models/report.py diff --git a/addons/report/__init__.py b/addons/report/__init__.py new file mode 100644 index 00000000000..d08c9d29951 --- /dev/null +++ b/addons/report/__init__.py @@ -0,0 +1,3 @@ +import controllers +import models + diff --git a/addons/report/__openerp__.py b/addons/report/__openerp__.py new file mode 100644 index 00000000000..19934972164 --- /dev/null +++ b/addons/report/__openerp__.py @@ -0,0 +1,14 @@ +{ + 'name': 'Report', + 'category': 'Website', + 'summary': 'Report', + 'version': '1.0', + 'description': """ +Report + """, + 'author': 'OpenERP SA', + 'depends': ['base'], + 'data': [ + ], + 'installable': True, +} diff --git a/addons/report/models/report.py b/addons/report/models/report.py new file mode 100644 index 00000000000..e69de29bb2d From c3c43a9ce55dc8e3e1a25c159771296b21555b69 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Wed, 12 Feb 2014 18:27:50 +0100 Subject: [PATCH 034/448] [IMP] Report model: Added a 'render' method that redirects to request.websit.render. Reports rendering therefore needs the website module. bzr revid: openerp-sle@openerp-sle.home-20140212172750-va0dkdavcomwsy87 --- addons/report/models/__init__.py | 2 + addons/report/models/report.py | 152 +++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 addons/report/models/__init__.py diff --git a/addons/report/models/__init__.py b/addons/report/models/__init__.py new file mode 100644 index 00000000000..e6fef4db935 --- /dev/null +++ b/addons/report/models/__init__.py @@ -0,0 +1,2 @@ +import report + diff --git a/addons/report/models/report.py b/addons/report/models/report.py index e69de29bb2d..3794cc8a564 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2014-Today OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.addons.web.http import request +from openerp.osv import osv +from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field +from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT + +import time +from datetime import datetime + + +def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT): + return len((datetime.now()).strftime(date_format)) + + +class report(osv.Model): + _name = "report" + _description = "Report" + + public_user = None + + def get_digits(self, obj=None, f=None, dp=None): + d = DEFAULT_DIGITS = 2 + if dp: + decimal_precision_obj = self.pool['decimal.precision'] + ids = decimal_precision_obj.search(request.cr, request.uid, [('name', '=', dp)]) + if ids: + d = decimal_precision_obj.browse(request.cr, request.uid, ids)[0].digits + elif obj and f: + res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS))) + if isinstance(res_digits, tuple): + d = res_digits[1] + else: + d = res_digits(request.cr)[1] + elif (hasattr(obj, '_field') and + isinstance(obj._field, (float_field, function_field)) and + obj._field.digits): + d = obj._field.digits[1] or DEFAULT_DIGITS + return d + + def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False): + """ + Assuming 'Account' decimal.precision=3: + formatLang(value) -> digits=2 (default) + formatLang(value, digits=4) -> digits=4 + formatLang(value, dp='Account') -> digits=3 + formatLang(value, digits=5, dp='Account') -> digits=5 + """ + if digits is None: + if dp: + digits = self.get_digits(dp=dp) + else: + digits = self.get_digits(value) + + if isinstance(value, (str, unicode)) and not value: + return '' + + if not self.lang_dict_called: + self._get_lang_dict() + self.lang_dict_called = True + + if date or date_time: + if not str(value): + return '' + + date_format = self.lang_dict['date_format'] + parse_format = DEFAULT_SERVER_DATE_FORMAT + if date_time: + value = value.split('.')[0] + date_format = date_format + " " + self.lang_dict['time_format'] + parse_format = DEFAULT_SERVER_DATETIME_FORMAT + if isinstance(value, basestring): + # FIXME: the trimming is probably unreliable if format includes day/month names + # and those would need to be translated anyway. + date = datetime.strptime(value[:get_date_length(parse_format)], parse_format) + elif isinstance(value, time.struct_time): + date = datetime(*value[:6]) + else: + date = datetime(*value.timetuple()[:6]) + if date_time: + # Convert datetime values to the expected client/context timezone + date = datetime_field.context_timestamp(request.cr, request.uid, + timestamp=date, + context=self.localcontext) + return date.strftime(date_format.encode('utf-8')) + + res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary) + if currency_obj: + if currency_obj.position == 'after': + res = '%s %s' % (res, currency_obj.symbol) + elif currency_obj and currency_obj.position == 'before': + res = '%s %s' % (currency_obj.symbol, res) + return res + + def _get_lang_dict(self): + pool_lang = self.pool['res.lang'] + lang = self.localcontext.get('lang', 'en_US') or 'en_US' + lang_ids = pool_lang.search(request.cr, request.uid, [('code', '=', lang)])[0] + lang_obj = pool_lang.browse(request.cr, request.uid, lang_ids) + lang_dict = { + 'lang_obj': lang_obj, + 'date_format': lang_obj.date_format, + 'time_format': lang_obj.time_format + } + self.lang_dict.update(lang_dict) + self.default_lang[lang] = self.lang_dict.copy() + return True + + def render(self, cr, uid, ids, template, values=None, context=None): + if values is None: + values = {} + + if context is None: + context = {} + + self.lang_dict = self.default_lang = {} + self.lang_dict_called = False + self.localcontext = { + 'lang': context.get('lang'), + 'tz': context.get('tz'), + 'uid': context.get('uid'), + } + self._get_lang_dict() + + current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) + values.update({ + 'time': time, + 'user': current_user, + 'formatLang': self.formatLang, + 'get_digits': self.get_digits, + }) + + return request.website.render(template, values) From 7b66c1e35f660443b73c60f149424b1c8b7480d5 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Wed, 12 Feb 2014 18:30:37 +0100 Subject: [PATCH 035/448] [IMP] Report model: Added a 'render_doc' method that renders a template into the associated partner's lang (used in invoices for instance). bzr revid: openerp-sle@openerp-sle.home-20140212173037-889koy9to39xg38c --- addons/report/models/report.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 3794cc8a564..4fd9ff1dc98 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -141,12 +141,37 @@ class report(osv.Model): } self._get_lang_dict() + def render_doc(doc_id, model, template): + """Helper used when a report should be translated into the associated + partner's lang. + + + + + + :param doc_id: id of the record to translate + :param model: model of the record to translate + :param template: name of the template to translate into the partner's lang + """ + ctx = context.copy() + doc = self.pool[model].browse(cr, uid, doc_id, context=ctx) + view_obj = self.pool['ir.ui.view'] + qcontext = values.copy() + # Do not force-translate if we chose to display to report in a specific lang + if ctx.get('translatable') is True: + qcontext['o'] = doc + else: + ctx['lang'] = doc.partner_id.lang + qcontext['o'] = self.pool[model].browse(cr, uid, doc_id, context=ctx) + return view_obj.render(cr, uid, template, qcontext, context=ctx) + current_user = self.pool['res.users'].browse(cr, uid, uid, context=context) values.update({ 'time': time, 'user': current_user, 'formatLang': self.formatLang, 'get_digits': self.get_digits, + 'render_doc': render_doc, }) return request.website.render(template, values) From 0d01d96dd43641dd8608288974f9ad9383cc6145 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Wed, 12 Feb 2014 18:54:12 +0100 Subject: [PATCH 036/448] [IMP] Added a 'paperformat' model that will handle properties of pdf documents that are generated from html reports. A company is linked to a default paper format and a report can be linked to a specific paperformat in order to override the default one. bzr revid: openerp-sle@openerp-sle.home-20140212175412-aeij8e9ifwjwgc1m --- addons/report/__openerp__.py | 4 + addons/report/data/paperformat_defaults.xml | 40 ++++++++ addons/report/models/__init__.py | 1 + addons/report/models/paperformat.py | 107 ++++++++++++++++++++ addons/report/security/ir.model.access.csv | 4 + addons/report/views/paperformat_view.xml | 95 +++++++++++++++++ addons/report/views/res_company_view.xml | 19 ++++ 7 files changed, 270 insertions(+) create mode 100644 addons/report/data/paperformat_defaults.xml create mode 100644 addons/report/models/paperformat.py create mode 100644 addons/report/security/ir.model.access.csv create mode 100644 addons/report/views/paperformat_view.xml create mode 100644 addons/report/views/res_company_view.xml diff --git a/addons/report/__openerp__.py b/addons/report/__openerp__.py index 19934972164..94da7b0d85f 100644 --- a/addons/report/__openerp__.py +++ b/addons/report/__openerp__.py @@ -9,6 +9,10 @@ Report 'author': 'OpenERP SA', 'depends': ['base'], 'data': [ + 'views/paperformat_view.xml', + 'views/res_company_view.xml', + 'data/paperformat_defaults.xml', + 'security/ir.model.access.csv', ], 'installable': True, } diff --git a/addons/report/data/paperformat_defaults.xml b/addons/report/data/paperformat_defaults.xml new file mode 100644 index 00000000000..71077f10ab4 --- /dev/null +++ b/addons/report/data/paperformat_defaults.xml @@ -0,0 +1,40 @@ + + + + + European A4 + + A4 + 0 + 0 + Portrait + 30 + 20 + 7 + 7 + + 25 + 90 + + + + US Letter + + Letter + 0 + 0 + Portrait + 50 + 18 + 10 + 10 + + 5 + 110 + + + + + + + \ No newline at end of file diff --git a/addons/report/models/__init__.py b/addons/report/models/__init__.py index e6fef4db935..9a4a3ad87fc 100644 --- a/addons/report/models/__init__.py +++ b/addons/report/models/__init__.py @@ -1,2 +1,3 @@ import report +import paperformat diff --git a/addons/report/models/paperformat.py b/addons/report/models/paperformat.py new file mode 100644 index 00000000000..ea8cc4e921c --- /dev/null +++ b/addons/report/models/paperformat.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2014-Today OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import osv, fields + + +class actions_report_paperformat(osv.Model): + _name = "ir.actions.report.paperformat" + _description = "Allows customization of a report." + + _columns = {'name': fields.char('Name', required=True), + 'default': fields.boolean('Default paper format ?'), + 'format': fields.selection([('A0', 'A0 5 841 x 1189 mm'), + ('A1', 'A1 6 594 x 841 mm'), + ('A2', 'A2 7 420 x 594 mm'), + ('A3', 'A3 8 297 x 420 mm'), + ('A4', 'A4 0 210 x 297 mm, 8.26 x 11.69 inches'), + ('A5', 'A5 9 148 x 210 mm'), + ('A6', 'A6 10 105 x 148 mm'), + ('A7', 'A7 11 74 x 105 mm'), + ('A8', 'A8 12 52 x 74 mm'), + ('A9', 'A9 13 37 x 52 mm'), + ('B0', 'B0 14 1000 x 1414 mm'), + ('B1', 'B1 15 707 x 1000 mm'), + ('B2', 'B2 17 500 x 707 mm'), + ('B3', 'B3 18 353 x 500 mm'), + ('B4', 'B4 19 250 x 353 mm'), + ('B5', 'B5 1 176 x 250 mm, 6.93 x 9.84 inches'), + ('B6', 'B6 20 125 x 176 mm'), + ('B7', 'B7 21 88 x 125 mm'), + ('B8', 'B8 22 62 x 88 mm'), + ('B9', 'B9 23 33 x 62 mm'), + ('B10', ':B10 16 31 x 44 mm'), + ('C5E', 'C5E 24 163 x 229 mm'), + ('Comm10E', 'Comm10E 25 105 x 241 mm, U.S. ' + 'Common 10 Envelope'), + ('DLE', 'DLE 26 110 x 220 mm'), + ('Executive', 'Executive 4 7.5 x 10 inches, ' + '190.5 x 254 mm'), + ('Folio', 'Folio 27 210 x 330 mm'), + ('Ledger', 'Ledger 28 431.8 x 279.4 mm'), + ('Legal', 'Legal 3 8.5 x 14 inches, ' + '215.9 x 355.6 mm'), + ('Letter', 'Letter 2 8.5 x 11 inches, ' + '215.9 x 279.4 mm'), + ('Tabloid', 'Tabloid 29 279.4 x 431.8 mm'), + ('custom', 'Custom')], + 'Paper size', + help="Select Proper Paper size"), + 'margin_top': fields.integer('Top Margin (mm)'), + 'margin_bottom': fields.integer('Bottom Margin (mm)'), + 'margin_left': fields.integer('Left Margin (mm)'), + 'margin_right': fields.integer('Right Margin (mm)'), + 'page_height': fields.integer('Page height (in)'), + 'page_width': fields.integer('Page width (in)'), + 'orientation': fields.selection([('Landscape', 'Landscape'), + ('Portrait', 'Portrait')], + 'Orientation'), + 'header_line': fields.boolean('Display a header line'), + 'header_spacing': fields.integer('Header spacing'), + 'dpi': fields.integer('Output DPI'), + 'report_ids': fields.one2many('ir.actions.report.xml', + 'paperformat_id', + 'Associated reports', + help="Explicitly associated reports") + } + + def _check_format_or_page(self, cr, uid, ids, context=None): + for paperformat in self.browse(cr, uid, ids, context=context): + if paperformat.format != 'custom' and (paperformat.page_width or paperformat.page_height): + return False + return True + + _constraints = [ + (_check_format_or_page, 'Error ! You cannot select a format AND speficic ' + 'page width/height.', ['format']), + ] + + +class res_company(osv.Model): + _inherit = 'res.company' + + _columns = {'paperformat_id': fields.many2one('ir.actions.report.paperformat', 'Paper format')} + + +class ir_actions_report(osv.Model): + _inherit = 'ir.actions.report.xml' + + _columns = {'paperformat_id': fields.many2one('ir.actions.report.paperformat', 'Paper format')} diff --git a/addons/report/security/ir.model.access.csv b/addons/report/security/ir.model.access.csv new file mode 100644 index 00000000000..2b0cf167b29 --- /dev/null +++ b/addons/report/security/ir.model.access.csv @@ -0,0 +1,4 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"paperformat_access_portal","ir_actions_report_paperformat group_portal","model_ir_actions_report_paperformat",,1,0,0,0 +"paperformat_access_employee","ir_actions_report_paperformat group_hr_user","model_ir_actions_report_paperformat",,1,0,1,0 +"access_report","access_report","model_report",,1,1,1,1 diff --git a/addons/report/views/paperformat_view.xml b/addons/report/views/paperformat_view.xml new file mode 100644 index 00000000000..dba1d433d92 --- /dev/null +++ b/addons/report/views/paperformat_view.xml @@ -0,0 +1,95 @@ + + + + + + + act_report_xml_view_inherit + + ir.actions.report.xml + + + + + + + + + + + paper_format_view_tree + ir.actions.report.paperformat + + + + + + + + + paper_format_view_form + ir.actions.report.paperformat + +
+ + + + + + + + + + + + + + + +
+
+
+ + + + Paper Format General Configuration + ir.actions.report.paperformat + form + tree,form + + + + Reports + ir.actions.report.xml + form + tree,form + + + + + + + + +
+
\ No newline at end of file diff --git a/addons/report/views/res_company_view.xml b/addons/report/views/res_company_view.xml new file mode 100644 index 00000000000..97abd9f5cae --- /dev/null +++ b/addons/report/views/res_company_view.xml @@ -0,0 +1,19 @@ + + + + + view_company_form_inherited + + res.company + + + + + + + + + + + + \ No newline at end of file From fe7d55cbdbd24d142b88441ce46a519bf6dc2902 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Wed, 12 Feb 2014 18:58:19 +0100 Subject: [PATCH 037/448] [IMP] Added some default layouts (internal and external with more or less information). The website RTE is called from these layouts, as well as the language selector. Note: currently, the link of the bootstrap 3.1 css points to the official bootstrap site. bzr revid: openerp-sle@openerp-sle.home-20140212175819-8cr53bzafn2uo8rc --- addons/report/__openerp__.py | 1 + addons/report/views/layouts.xml | 237 ++++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 addons/report/views/layouts.xml diff --git a/addons/report/__openerp__.py b/addons/report/__openerp__.py index 94da7b0d85f..6b5c4516ad5 100644 --- a/addons/report/__openerp__.py +++ b/addons/report/__openerp__.py @@ -9,6 +9,7 @@ Report 'author': 'OpenERP SA', 'depends': ['base'], 'data': [ + 'views/layouts.xml', 'views/paperformat_view.xml', 'views/res_company_view.xml', 'data/paperformat_defaults.xml', diff --git a/addons/report/views/layouts.xml b/addons/report/views/layouts.xml new file mode 100644 index 00000000000..51806bda0c8 --- /dev/null +++ b/addons/report/views/layouts.xml @@ -0,0 +1,237 @@ + + + + + + + + + + + + + + + + + + From 5fa1be13034d1c5869aef37c8c9fcbbd0cff507c Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Wed, 12 Feb 2014 19:00:25 +0100 Subject: [PATCH 038/448] [IMP] Added the default controller for the generic html reports (reports not needing data to be preprocessed) bzr revid: openerp-sle@openerp-sle.home-20140212180025-c3p4xnixd3sty7gh --- addons/report/controllers/__init__.py | 2 + addons/report/controllers/main.py | 83 +++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 addons/report/controllers/__init__.py create mode 100644 addons/report/controllers/main.py diff --git a/addons/report/controllers/__init__.py b/addons/report/controllers/__init__.py new file mode 100644 index 00000000000..cbcfe6fc8a0 --- /dev/null +++ b/addons/report/controllers/__init__.py @@ -0,0 +1,2 @@ +import main + diff --git a/addons/report/controllers/main.py b/addons/report/controllers/main.py new file mode 100644 index 00000000000..5d901037c6a --- /dev/null +++ b/addons/report/controllers/main.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2014-Today OpenERP SA (). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.addons.web import http +from openerp.addons.web.http import request + +import logging + + +_logger = logging.getLogger(__name__) + + +class Report(http.Controller): + + @http.route(['/report//'], type='http', auth='user', website=True, multilang=True) + def report_html(self, reportname, docids, **kwargs): + """This is the generic route for QWeb reports. It is used for reports + which do not need to preprocess the data (i.e. reports that just display + fields of a record). + + It is given a ~fully qualified report name, for instance 'account.report_invoice'. + Based on it, we know the module concerned and the name of the template. With the + name of the template, we will make a search on the ir.actions.reports.xml table and + get the record associated to finally know the model this template refers to. + + There is a way to declare the report (in module_report(s).xml) that you must respect: + id="action_report_model" + model="module.model" # To know which model the report refers to + string="Invoices" + report_type="qweb-pdf" # or qweb-html + name="module.template_name" + file="module.template_name" + + If you don't want your report to be listed under the print button, just add + 'menu=False'. + """ + ids = [int(i) for i in docids.split(',')] + ids = list(set(ids)) + report = self._get_report_from_name(reportname) + report_obj = request.registry[report.model] + docs = report_obj.browse(request.cr, request.uid, ids, context=request.context) + + docargs = { + 'doc_ids': ids, + 'doc_model': report.model, + 'docs': docs, + } + + return request.registry['report'].render(request.cr, request.uid, [], report.report_file, + docargs, context=request.context) + + def _get_report_from_name(self, report_name): + """Get the first record of ir.actions.report.xml having the argument as value for + the field report_name. + """ + report_obj = request.registry['ir.actions.report.xml'] + qwebtypes = ['qweb-pdf', 'qweb-html'] + + idreport = report_obj.search(request.cr, request.uid, + [('report_type', 'in', qwebtypes), + ('report_name', '=', report_name)]) + + report = report_obj.browse(request.cr, request.uid, idreport[0], + context=request.context) + return report From 8dbd892d8b1528313b71be981575dfe2a441482a Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Wed, 12 Feb 2014 19:09:34 +0100 Subject: [PATCH 039/448] [IMP] Added the route converting all reports to pdf thanks to wkhtmltopdf. The html rendered report is downloaded via werkzeug.test.client and then parsed into an lxml.etree in order to extract only the useful data : local css, header, content and footer. We then generate a minimalist html page that is passed to wkhtmltopdf. Save in attachment feature is handled. A method transform a paperformat object into a list of parameters for wkhtmltopdf. Multiple IDS reports are generated in different pdf merged at the end. bzr revid: openerp-sle@openerp-sle.home-20140212180934-dupp8x2ivo52uzib --- addons/report/controllers/main.py | 346 +++++++++++++++++++++ addons/report/static/src/css/reset.min.css | 2 + addons/report/static/src/js/subst.js | 14 + 3 files changed, 362 insertions(+) create mode 100644 addons/report/static/src/css/reset.min.css create mode 100644 addons/report/static/src/js/subst.js diff --git a/addons/report/controllers/main.py b/addons/report/controllers/main.py index 5d901037c6a..bd5b41d9a9d 100644 --- a/addons/report/controllers/main.py +++ b/addons/report/controllers/main.py @@ -19,10 +19,22 @@ # ############################################################################## +from openerp.osv.osv import except_osv from openerp.addons.web import http +from openerp.tools.translate import _ from openerp.addons.web.http import request +import time +import base64 import logging +import tempfile +import lxml.html +import subprocess + +from pyPdf import PdfFileWriter, PdfFileReader +from werkzeug.test import Client +from werkzeug.wrappers import BaseResponse +from werkzeug.datastructures import Headers _logger = logging.getLogger(__name__) @@ -67,6 +79,310 @@ class Report(http.Controller): return request.registry['report'].render(request.cr, request.uid, [], report.report_file, docargs, context=request.context) + @http.route(['/report/pdf/'], type='http', auth="user", website=True) + def report_pdf(self, path=None, landscape=False, **post): + cr, uid, context = request.cr, request.uid, request.context + + # Get the report we are working on. + # Pattern is /report/module.reportname(?a=1) + reportname_in_path = path.split('/')[1].split('?')[0] + report = self._get_report_from_name(reportname_in_path) + + # Check attachment_use field. If set to true and an existing pdf is already saved, return + # this one now. If not, mark save it. + save_in_attachment = {} + + if report.attachment_use is True: + # Get the record ids we are working on. + path_ids = [int(i) for i in path.split('/')[2].split('?')[0].split(',')] + + save_in_attachment['model'] = report.model + save_in_attachment['loaded_documents'] = {} + + for path_id in path_ids: + obj = request.registry[report.model].browse(cr, uid, path_id) + filename = eval(report.attachment, {'object': obj, 'time': time}) + + if filename is False: # May be false if, for instance, the record is in draft state + continue + else: + alreadyindb = [('datas_fname', '=', filename), + ('res_model', '=', report.model), + ('res_id', '=', path_id)] + + attach_ids = request.registry['ir.attachment'].search(cr, uid, alreadyindb) + if attach_ids: + # Add the loaded pdf in the loaded_documents list + pdf = request.registry['ir.attachment'].browse(cr, uid, attach_ids[0]).datas + pdf = base64.decodestring(pdf) + save_in_attachment['loaded_documents'][path_id] = pdf + _logger.info('The PDF document %s was loaded from the database' % filename) + else: + # Mark current document to be saved + save_in_attachment[path_id] = filename + + # Get the paperformat associated to the report, if there is. + if not report.paperformat_id: + user = request.registry['res.users'].browse(cr, uid, uid, context=context) + paperformat = user.company_id.paperformat_id + else: + paperformat = report.paperformat_id + + # Get the html report. + html = self._get_url_content('/' + path, post) + + # Get some css and script in order to build a minimal html page for the report. + # This page will later be sent to wkhtmltopdf. + css = self._get_url_content('/report/static/src/css/reset.min.css') + css += self._get_url_content('/website/static/src/css/website.css') + subst = self._get_url_content('/report/static/src/js/subst.js') + + headerhtml = [] + contenthtml = [] + footerhtml = [] + minimalhtml = """ + + + + + + + + {2} + +""" + + # The retrieved html report must be simplified. We convert it into a xml tree + # via lxml in order to extract header, footer and all reportcontent. + try: + root = lxml.html.fromstring(html) + + for node in root.xpath("//html/head/style"): + css += node.text + + for node in root.xpath("//div[@class='header']"): + body = lxml.html.tostring(node) + header = minimalhtml.format(css, subst, body) + headerhtml.append(header) + + for node in root.xpath("//div[@class='footer']"): + body = lxml.html.tostring(node) + footer = minimalhtml.format(css, subst, body) + footerhtml.append(footer) + + for node in root.xpath("//div[@class='page']"): + # Previously, we marked some reports to be saved in attachment via their ids, so we + # must set a relation between report ids and reportcontent. We use the QWeb + # branding in order to do so: searching after a node having a data-oe-model + # attribute with the value of the current report model and read its oe-id attribute + oemodelnode = node.find(".//*[@data-oe-model='" + report.model + "']") + if oemodelnode is not None: + reportid = oemodelnode.get('data-oe-id', False) + if reportid is not False: + reportid = int(reportid) + else: + reportid = False + + body = lxml.html.tostring(node) + reportcontent = minimalhtml.format(css, '', body) + contenthtml.append(tuple([reportid, reportcontent])) + + except lxml.etree.XMLSyntaxError: + contenthtml = [] + contenthtml.append(html) + save_in_attachment = {} # Don't save this potentially malformed document + + # Get paperformat arguments set in the root html tag. They are prioritized over + # paperformat-record arguments. + specific_paperformat_args = {} + for attribute in root.items(): + if attribute[0].startswith('data-report-'): + specific_paperformat_args[attribute[0]] = attribute[1] + + # Execute wkhtmltopdf process. + pdf = self._generate_wkhtml_pdf(headerhtml, footerhtml, contenthtml, landscape, + paperformat, specific_paperformat_args, save_in_attachment) + + return self._make_pdf_response(pdf) + + def _get_url_content(self, url, post=None): + """Resolve an internal webpage url and return its content with the help of + werkzeug.test.client. + + :param url: string representinf the url to resolve + :param post: a dict representing the query string + :returns: a tuple str(html), int(statuscode) + """ + # Rebuilding the query string. + if post: + url += '?' + url += '&'.join('%s=%s' % (k, v) for (k, v) in post.iteritems()) + + # We have to pass the current headers in order to see the report. + reqheaders = Headers(request.httprequest.headers) + response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders, + follow_redirects=True) + content = response.data + + try: + content = content.decode('utf-8') + except UnicodeDecodeError: + pass + + return content + + def _generate_wkhtml_pdf(self, headers, footers, bodies, landscape, + paperformat, spec_paperformat_args=None, save_in_attachment=None): + """Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf + document. + + :param header: list of string containing the headers + :param footer: list of string containing the footers + :param bodies: list of string containing the reports + :param landscape: boolean to force the pdf to be rendered under a landscape format + :param paperformat: ir.actions.report.paperformat to generate the wkhtmltopf arguments + :param specific_paperformat_args: dict of prioritized paperformat arguments + :param save_in_attachment: dict of reports to save/load in/from the db + :returns: Content of the pdf as a string + """ + command = ['wkhtmltopdf-0.12'] + tmp_dir = tempfile.gettempdir() + + # Display arguments + command_args = [] + + if paperformat: + command_args.extend(self._build_wkhtmltopdf_args(paperformat, spec_paperformat_args)) + + if landscape and '--orientation' in command_args: + command_args_copy = list(command_args) + for index, elem in enumerate(command_args_copy): + if elem == '--orientation': + del command_args[index] + del command_args[index] + command_args.extend(['--orientation', 'landscape']) + elif landscape and not '--orientation' in command_args: + command_args.extend(['--orientation', 'landscape']) + + pdfdocuments = [] + # HTML to PDF thanks to WKhtmltopdf + for index, reporthtml in enumerate(bodies): + command_arg_local = [] + pdfreport = tempfile.NamedTemporaryFile(suffix='.pdf', prefix='report.tmp.', + mode='w+b') + # Directly load the document if we have it + if save_in_attachment and save_in_attachment['loaded_documents'].get(reporthtml[0]): + pdfreport.write(save_in_attachment['loaded_documents'].get(reporthtml[0])) + pdfreport.seek(0) + pdfdocuments.append(pdfreport) + continue + + # Header stuff + if headers: + head_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.header.tmp.', + dir=tmp_dir, mode='w+') + head_file.write(headers[index]) + head_file.seek(0) + command_arg_local.extend(['--header-html', head_file.name]) + + # Footer stuff + if footers: + foot_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.footer.tmp.', + dir=tmp_dir, mode='w+') + foot_file.write(footers[index]) + foot_file.seek(0) + command_arg_local.extend(['--footer-html', foot_file.name]) + + # Body stuff + content_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.body.tmp.', + dir=tmp_dir, mode='w+') + content_file.write(reporthtml[1]) + content_file.seek(0) + + try: + wkhtmltopdf = command + command_args + command_arg_local + wkhtmltopdf += [content_file.name] + [pdfreport.name] + + process = subprocess.Popen(wkhtmltopdf, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + out, err = process.communicate() + + if process.returncode != 0: + raise except_osv(_('Report (PDF)'), + _('wkhtmltopdf-patched failed with error code = %s. ' + 'Message: %s') % (str(process.returncode), err)) + + # Save the pdf in attachment if marked + if reporthtml[0] is not False and save_in_attachment.get(reporthtml[0]): + attachment = { + 'name': save_in_attachment.get(reporthtml[0]), + 'datas': base64.encodestring(pdfreport.read()), + 'datas_fname': save_in_attachment.get(reporthtml[0]), + 'res_model': save_in_attachment.get('model'), + 'res_id': reporthtml[0], + } + request.registry['ir.attachment'].create(request.cr, request.uid, attachment) + _logger.info('The PDF document %s is now saved in the ' + 'database' % attachment['name']) + + pdfreport.seek(0) + pdfdocuments.append(pdfreport) + + if headers: + head_file.close() + if footers: + foot_file.close() + except: + raise + + # Get and return the full pdf + if len(pdfdocuments) == 1: + content = pdfdocuments[0].read() + pdfdocuments[0].close() + else: + content = self._merge_pdf(pdfdocuments) + + return content + + def _build_wkhtmltopdf_args(self, paperformat, specific_paperformat_args=None): + """Build arguments understandable by wkhtmltopdf from an ir.actions.report.paperformat + record. + + :paperformat: ir.actions.report.paperformat record associated to a document + :specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments + :returns: list of string containing the wkhtmltopdf arguments + """ + command_args = [] + if paperformat.format and paperformat.format != 'custom': + command_args.extend(['--page-size', paperformat.format]) + + if paperformat.page_height and paperformat.page_width and paperformat.format == 'custom': + command_args.extend(['--page-width', str(paperformat.page_width) + 'in']) + command_args.extend(['--page-height', str(paperformat.page_height) + 'in']) + + if specific_paperformat_args and specific_paperformat_args['data-report-margin-top']: + command_args.extend(['--margin-top', + str(specific_paperformat_args['data-report-margin-top'])]) + else: + command_args.extend(['--margin-top', str(paperformat.margin_top)]) + + if paperformat.margin_left: + command_args.extend(['--margin-left', str(paperformat.margin_left)]) + if paperformat.margin_bottom: + command_args.extend(['--margin-bottom', str(paperformat.margin_bottom)]) + if paperformat.margin_right: + command_args.extend(['--margin-right', str(paperformat.margin_right)]) + if paperformat.orientation: + command_args.extend(['--orientation', str(paperformat.orientation)]) + if paperformat.header_spacing: + command_args.extend(['--header-spacing', str(paperformat.header_spacing)]) + if paperformat.header_line: + command_args.extend(['--header-line']) + if paperformat.dpi: + command_args.extend(['--dpi', str(paperformat.dpi)]) + + return command_args + def _get_report_from_name(self, report_name): """Get the first record of ir.actions.report.xml having the argument as value for the field report_name. @@ -81,3 +397,33 @@ class Report(http.Controller): report = report_obj.browse(request.cr, request.uid, idreport[0], context=request.context) return report + + def _make_pdf_response(self, pdf): + """Make a request response for a PDF file with correct http headers. + + :param pdf: content of a pdf in a string + :returns: request response for a pdf document + """ + pdfhttpheaders = [('Content-Type', 'application/pdf'), + ('Content-Length', len(pdf))] + return request.make_response(pdf, headers=pdfhttpheaders) + + def _merge_pdf(self, documents): + """Merge PDF files into one. + + :param documents: list of pdf files + :returns: string containing the merged pdf + """ + writer = PdfFileWriter() + for document in documents: + reader = PdfFileReader(file(document.name, "rb")) + for page in range(0, reader.getNumPages()): + writer.addPage(reader.getPage(page)) + document.close() + + merged = tempfile.NamedTemporaryFile(suffix='.pdf', prefix='report.tmp.', mode='w+b') + writer.write(merged) + merged.seek(0) + content = merged.read() + merged.close() + return content diff --git a/addons/report/static/src/css/reset.min.css b/addons/report/static/src/css/reset.min.css new file mode 100644 index 00000000000..cc7842e37ef --- /dev/null +++ b/addons/report/static/src/css/reset.min.css @@ -0,0 +1,2 @@ +/* reset5 2011 opensource.736cs.com MIT - https://code.google.com/p/reset5 */ +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,audio,canvas,details,figcaption,figure,footer,header,hgroup,mark,menu,meter,nav,output,progress,section,summary,time,video{border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent;margin:0;padding:0;}body{line-height:1;}article,aside,dialog,figure,footer,header,hgroup,nav,section,blockquote{display:block;}nav ul{list-style:none;}ol{list-style:decimal;}ul{list-style:disc;}ul ul{list-style:circle;}blockquote,q{quotes:none;}blockquote:before,blockquote:after,q:before,q:after{content:none;}ins{text-decoration:underline;}del{text-decoration:line-through;}mark{background:none;}abbr[title],dfn[title]{border-bottom:1px dotted #000;cursor:help;}table{border-collapse:collapse;border-spacing:0;}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0;}input[type=submit],input[type=button],button{margin:0!important;padding:0!important;}input,select,a img{vertical-align:middle;} diff --git a/addons/report/static/src/js/subst.js b/addons/report/static/src/js/subst.js new file mode 100644 index 00000000000..076a5ff208a --- /dev/null +++ b/addons/report/static/src/js/subst.js @@ -0,0 +1,14 @@ +function subst() { + var vars = {}; + var x = document.location.search.substring(1).split('&'); + for (var i in x) { + var z = x[i].split('=', 2); + vars[z[0]] = unescape(z[1]); + } + var x=['frompage', 'topage', 'page', 'webpage', 'section', 'subsection', 'subsubsection']; + for (var i in x) { + var y = document.getElementsByClassName(x[i]); + for (var j=0; j Date: Wed, 12 Feb 2014 19:12:02 +0100 Subject: [PATCH 040/448] [IMP] Report model: added a method to easily returns a pdf and another to easily generate a report action bzr revid: openerp-sle@openerp-sle.home-20140212181202-8kuc6cap1oymgjn3 --- addons/report/models/report.py | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 4fd9ff1dc98..071e9cb3c5d 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -27,6 +27,10 @@ from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FO import time from datetime import datetime +from werkzeug.datastructures import Headers +from werkzeug.wrappers import BaseResponse +from werkzeug.test import Client + def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT): return len((datetime.now()).strftime(date_format)) @@ -175,3 +179,36 @@ class report(osv.Model): }) return request.website.render(template, values) + + def get_pdf(self, report, record_id, context=None): + """Used to return content of a generated PDF. + + :returns: pdf + """ + url = '/report/pdf/report/' + report.report_file + '/' + str(record_id) + reqheaders = Headers(request.httprequest.headers) + reqheaders.pop('Accept') + reqheaders.add('Accept', 'application/pdf') + reqheaders.pop('Content-Type') + reqheaders.add('Content-Type', 'text/plain') + response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders, + follow_redirects=True) + return response.data + + def get_action(self, cr, uid, ids, report_name, context=None): + """Used to return an action of type ir.actions.report.xml. + + :param report_name: Name of the template to generate an action for + """ + if context is None: + context = {} + + report_obj = self.pool.get('ir.actions.report.xml') + idreport = report_obj.search(cr, uid, [('report_name', '=', report_name)], context=context) + report = report_obj.browse(cr, uid, idreport[0], context=context) + + return { + 'type': 'ir.actions.report.xml', + 'report_name': report.report_name, + 'report_type': report.report_type, + } From 12e9d218b009e832255731d5472f79d33cc57cfd Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Wed, 12 Feb 2014 19:22:47 +0100 Subject: [PATCH 041/448] [IMP] Sale order report converted to QWeb bzr revid: openerp-sle@openerp-sle.home-20140212182247-0ahbbrgob4ssz9bn --- addons/sale/__openerp__.py | 1 + addons/sale/report/__init__.py | 1 - addons/sale/report/sale_order.py | 45 ---- addons/sale/report/sale_order.rml | 322 ------------------------- addons/sale/sale_report.xml | 13 +- addons/sale/views/report_saleorder.xml | 137 +++++++++++ 6 files changed, 146 insertions(+), 373 deletions(-) delete mode 100644 addons/sale/report/sale_order.py delete mode 100644 addons/sale/report/sale_order.rml create mode 100644 addons/sale/views/report_saleorder.xml diff --git a/addons/sale/__openerp__.py b/addons/sale/__openerp__.py index 2bea86b41af..bb5bac89e46 100644 --- a/addons/sale/__openerp__.py +++ b/addons/sale/__openerp__.py @@ -77,6 +77,7 @@ The Dashboard for the Sales Manager will include 'board_sale_view.xml', 'edi/sale_order_action_data.xml', 'res_config_view.xml', + 'views/report_saleorder.xml', ], 'demo': ['sale_demo.xml'], 'test': [ diff --git a/addons/sale/report/__init__.py b/addons/sale/report/__init__.py index 6986639d942..12d484c3e25 100644 --- a/addons/sale/report/__init__.py +++ b/addons/sale/report/__init__.py @@ -19,7 +19,6 @@ # ############################################################################## -import sale_order import sale_report # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/sale/report/sale_order.py b/addons/sale/report/sale_order.py deleted file mode 100644 index 134e87a0c2e..00000000000 --- a/addons/sale/report/sale_order.py +++ /dev/null @@ -1,45 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -import time - -from openerp.report import report_sxw - -class order(report_sxw.rml_parse): - def __init__(self, cr, uid, name, context=None): - super(order, self).__init__(cr, uid, name, context=context) - self.localcontext.update({ - 'time': time, - 'show_discount':self._show_discount, - }) - - def _show_discount(self, uid, context=None): - cr = self.cr - try: - group_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'group_discount_per_so_line')[1] - except: - return False - return group_id in [x.id for x in self.pool.get('res.users').browse(cr, uid, uid, context=context).groups_id] - -report_sxw.report_sxw('report.sale.order', 'sale.order', 'addons/sale/report/sale_order.rml', parser=order, header="external") - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/addons/sale/report/sale_order.rml b/addons/sale/report/sale_order.rml deleted file mode 100644 index 3bddf81ade8..00000000000 --- a/addons/sale/report/sale_order.rml +++ /dev/null @@ -1,322 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - [[repeatIn(objects,'o')]] - [[ setLang(o.partner_id.lang) ]] - - - - - Description - - - Tax - - - Quantity - - - Unit Price - - - Disc.(%) - - - Price - - - - - - - - - - - - - - Shipping address : - [[ (o.partner_shipping_id and o.partner_id.title and o.partner_shipping_id.title.name) or '' ]] [[ (o.partner_shipping_id and o.partner_shipping_id.name) or '' ]] - [[ o.partner_shipping_id and display_address(o.partner_shipping_id) ]] - - - - Invoice address : - [[ (o.partner_invoice_id and o.partner_invoice_id.title and o.partner_invoice_id.title.name) or '' ]] [[ (o.partner_invoice_id and o.partner_invoice_id.name) or '' ]] - [[ o.partner_invoice_id and display_address(o.partner_invoice_id) ]] - - - - - - - - [[ (o.partner_id and o.partner_id.title and o.partner_id.title.name) or '' ]] [[ (o.partner_id and o.partner_id.name) or '' ]] - [[ o.partner_id and display_address(o.partner_id) ]] - - - - Tel. : [[ (o.partner_id.phone) or removeParentNode('para') ]] - Fax : [[ (o.partner_id.fax) or removeParentNode('para') ]] - TVA : [[ (o.partner_id.vat) or removeParentNode('para') ]] - - - - - - - - - - [[ o.state not in ['draft','sent'] and removeParentNode('para') ]] Quotation N° [[ o.name ]] - [[ o.state in ['draft','sent'] and removeParentNode('para') ]] Order N° [[ o.name ]] - - - - - - - Your Reference - - - [[ o.state in ['draft','sent'] and removeParentNode('para') ]] Date Ordered - [[ o.state not in ['draft','sent'] and removeParentNode('para') ]] Quotation Date - - - Salesperson - - - Payment Term - - - - - - - [[ o.client_order_ref ]] - - - [[ formatLang(o.date_order,date = True) ]] - - - [[ (o.user_id and o.user_id.name) or '' ]] - - - [[ (o.payment_term and o.payment_term.name) or '' ]] - - - - - - - - - - Description - - - Tax - - - Quantity - - - Unit Price - - - [[not show_discount(user.id) and removeParentNode('para') ]]Disc.(%) - - - Price - - - -
- [[repeatIn(o.order_line,'line')]] - - - - [[ format(line.name) ]] - - - [[ ', '.join(map(lambda x: x.name, line.tax_id)) ]] - - - [[ formatLang(line.product_uos and line.product_uos_qty or line.product_uom_qty) ]] [[ line.product_uos and line.product_uos.name or line.product_uom.name ]] - - - [[ formatLang(line.price_unit , digits=get_digits(dp='Product Price'))]] - - - [[show_discount(user.id) and formatLang(line.discount, digits=get_digits(dp='Discount')) or '']] - - - [[ formatLang(line.price_subtotal, digits=get_digits(dp='Account'), currency_obj=o.pricelist_id.currency_id) ]] - - - -
- - - - - - - - - Net Total : - - - [[ formatLang(o.amount_untaxed, dp='Account', currency_obj=o.pricelist_id.currency_id) ]] - - - - - - - - - - Taxes : - - - [[ formatLang(o.amount_tax, dp='Account', currency_obj=o.pricelist_id.currency_id) ]] - - - - - - - - - - Total : - - - [[ formatLang(o.amount_total, dp='Account', currency_obj=o.pricelist_id.currency_id) ]] - - - - - - - [[ format(o.note or '') ]] - - - - [[ format(o.payment_term and o.payment_term.note or (o.partner_id.property_payment_term and o.partner_id.property_payment_term.note or '')) ]] - - - - - - -
-
-
diff --git a/addons/sale/sale_report.xml b/addons/sale/sale_report.xml index 0f77913a3c9..7a8614b139b 100644 --- a/addons/sale/sale_report.xml +++ b/addons/sale/sale_report.xml @@ -1,10 +1,13 @@ - - - + diff --git a/addons/sale/views/report_saleorder.xml b/addons/sale/views/report_saleorder.xml new file mode 100644 index 00000000000..2249a7810a1 --- /dev/null +++ b/addons/sale/views/report_saleorder.xml @@ -0,0 +1,137 @@ + + + + + + From 38a9bf090186229b2f6710b86688cc79e8fcca67 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Wed, 12 Feb 2014 22:20:26 +0100 Subject: [PATCH 042/448] [FIX] Report module dependancies bzr revid: openerp-sle@openerp-sle.home-20140212212026-e9jj8tm7esdywsfl --- addons/report/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/report/__openerp__.py b/addons/report/__openerp__.py index 6b5c4516ad5..2ffa758d482 100644 --- a/addons/report/__openerp__.py +++ b/addons/report/__openerp__.py @@ -7,7 +7,7 @@ Report """, 'author': 'OpenERP SA', - 'depends': ['base'], + 'depends': ['base', 'website'], 'data': [ 'views/layouts.xml', 'views/paperformat_view.xml', From 999050a9df70fe82cb17cdaee1c09eb412c92fbf Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Wed, 12 Feb 2014 22:22:59 +0100 Subject: [PATCH 043/448] [IMP] Email_template module now handles qweb-pdf report in mail attachment bzr revid: openerp-sle@openerp-sle.home-20140212212259-sq7mpkt09a0t6v6d --- addons/email_template/email_template.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index fcfe869da90..b6899a7da2b 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -381,12 +381,18 @@ class email_template(osv.osv): for res_id in template_res_ids: attachments = [] report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=context) - report_service = report_xml_pool.browse(cr, uid, template.report_template.id, context).report_name + report = report_xml_pool.browse(cr, uid, template.report_template.id, context) + report_service = report.report_name # Ensure report is rendered using template's language ctx = context.copy() if template.lang: ctx['lang'] = self.render_template_batch(cr, uid, template.lang, template.model, [res_id], context)[res_id] # take 0 ? - result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx) + + if report.report_type in ['qweb-html', 'qweb-pdf']: + result, format = self.pool['report'].get_pdf(report, res_id, context=ctx), 'pdf' + else: + result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx) + result = base64.b64encode(result) if not report_name: report_name = 'report.' + report_service From eb4bdb1a494627d5b245038cc959b4b927a8b0c9 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Wed, 12 Feb 2014 23:08:01 +0100 Subject: [PATCH 044/448] [IMP] Website: contact widget has now an option to disable fontawesome markers bzr revid: openerp-sle@openerp-sle.home-20140212220801-66kuwbz8d3piy2sa --- addons/website/views/website_templates.xml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 79145ef3cda..44c3aee763a 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -762,15 +762,15 @@ Sitemap: sitemap.xml
- +
- , + ,
-
-
-
-
+
+
+
+
From 271f350268cf8bb98d50ab4050215cf454ecdc43 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Wed, 12 Feb 2014 23:12:54 +0100 Subject: [PATCH 045/448] [FIX] Sale Stock: remove print sale order test bzr revid: openerp-sle@openerp-sle.home-20140212221254-whmb4uw3ztl6g34w --- addons/sale_stock/test/picking_order_policy.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/addons/sale_stock/test/picking_order_policy.yml b/addons/sale_stock/test/picking_order_policy.yml index 83610bdd043..b192c750330 100644 --- a/addons/sale_stock/test/picking_order_policy.yml +++ b/addons/sale_stock/test/picking_order_policy.yml @@ -198,14 +198,3 @@ assert order.invoiced == True, "Sale order is not invoiced." assert order.invoiced_rate == 100, "Invoiced progress is not 100%." assert order.state == 'done', 'Order should be in closed.' -- - I print a sale order report. -- - !python {model: sale.order}: | - import os - import openerp.report - from openerp import tools - data, format = openerp.report.render_report(cr, uid, [ref('sale.sale_order_6')], 'sale.order', {}, {}) - if tools.config['test_report_directory']: - file(os.path.join(tools.config['test_report_directory'], 'sale-sale_order.'+format), 'wb+').write(data) - From a2805ee6d7f13af0348ab4b23358650a86de6952 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Thu, 13 Feb 2014 00:00:02 +0100 Subject: [PATCH 046/448] [FIX] Sale: QWeb action returned when clicking on print button on form view bzr revid: openerp-sle@openerp-sle.home-20140212230002-4e1rsmeutg45h237 --- addons/sale/sale.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/addons/sale/sale.py b/addons/sale/sale.py index 8848ba9df2b..eb3a5dba830 100644 --- a/addons/sale/sale.py +++ b/addons/sale/sale.py @@ -409,12 +409,7 @@ class sale_order(osv.osv): ''' assert len(ids) == 1, 'This option should only be used for a single id at a time' self.signal_quotation_sent(cr, uid, ids) - datas = { - 'model': 'sale.order', - 'ids': ids, - 'form': self.read(cr, uid, ids[0], context=context), - } - return {'type': 'ir.actions.report.xml', 'report_name': 'sale.order', 'datas': datas, 'nodestroy': True} + return self.pool['report'].get_action(cr, uid, ids, 'sale.report_saleorder', context=context) def manual_invoice(self, cr, uid, ids, context=None): """ create invoices for the given sales orders (ids), and open the form From cf9548f1a3fb8cad7c0640f967eec2e77ddecdf4 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Thu, 13 Feb 2014 11:25:47 +0100 Subject: [PATCH 047/448] [FIX] Fix bug in report layouts due to 't-att-src' being treated before 't-if' on the same tag bzr revid: openerp-sle@openerp-sle.home-20140213102547-sabhj66c98dxa8x0 --- addons/report/views/layouts.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/report/views/layouts.xml b/addons/report/views/layouts.xml index 51806bda0c8..bff67a9051a 100644 --- a/addons/report/views/layouts.xml +++ b/addons/report/views/layouts.xml @@ -95,7 +95,7 @@
- +
From 0ae6d1f4e7392b01e569dee64602659442c6a70a Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Thu, 13 Feb 2014 13:07:39 +0100 Subject: [PATCH 048/448] [FIX] More accurate values for the demo paper format and website contact widget enhanced bzr revid: openerp-sle@openerp-sle.home-20140213120739-4jh30s94scfr584e --- addons/report/data/paperformat_defaults.xml | 16 ++++++++-------- addons/website/views/website_templates.xml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/addons/report/data/paperformat_defaults.xml b/addons/report/data/paperformat_defaults.xml index 71077f10ab4..15d80ddc3fe 100644 --- a/addons/report/data/paperformat_defaults.xml +++ b/addons/report/data/paperformat_defaults.xml @@ -8,12 +8,12 @@ 0 0 Portrait - 30 + 40 20 7 7 - 25 + 35 90 @@ -24,13 +24,13 @@ 0 0 Portrait - 50 - 18 - 10 - 10 + 40 + 20 + 7 + 7 - 5 - 110 + 35 + 90 diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index 44c3aee763a..99fdc54c4fa 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -762,7 +762,7 @@ Sitemap: sitemap.xml
- +
, From 7d656e961b69939a1b68abae10741a4f156c60f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me?= Date: Thu, 13 Feb 2014 15:30:44 +0100 Subject: [PATCH 049/448] [ADD] crm_project_issue: link module to create lead from issues and issues from lead bzr revid: jem@openerp.com-20140213143044-p06ef9scr32ihef2 --- addons/crm_project_issue/__init__.py | 3 +++ addons/crm_project_issue/__openerp__.py | 21 +++++++++++++++ addons/crm_project_issue/crm_lead.py | 10 +++++++ addons/crm_project_issue/crm_lead_view.xml | 27 +++++++++++++++++++ addons/crm_project_issue/project_issue.py | 14 ++++++++++ .../crm_project_issue/project_issue_view.xml | 17 ++++++++++++ 6 files changed, 92 insertions(+) create mode 100644 addons/crm_project_issue/__init__.py create mode 100644 addons/crm_project_issue/__openerp__.py create mode 100644 addons/crm_project_issue/crm_lead.py create mode 100644 addons/crm_project_issue/crm_lead_view.xml create mode 100644 addons/crm_project_issue/project_issue.py create mode 100644 addons/crm_project_issue/project_issue_view.xml diff --git a/addons/crm_project_issue/__init__.py b/addons/crm_project_issue/__init__.py new file mode 100644 index 00000000000..40302ae183c --- /dev/null +++ b/addons/crm_project_issue/__init__.py @@ -0,0 +1,3 @@ + +import project_issue +import crm_lead \ No newline at end of file diff --git a/addons/crm_project_issue/__openerp__.py b/addons/crm_project_issue/__openerp__.py new file mode 100644 index 00000000000..89ef20a655e --- /dev/null +++ b/addons/crm_project_issue/__openerp__.py @@ -0,0 +1,21 @@ +{ + 'name' : 'CRM Project Issues', + 'version': '1.0', + 'summary': 'Module linker between Leads and Issues', + 'sequence': '19', + 'category': 'Project Management', + 'complexity': 'easy', + 'description': + """ +CRM Project Issues +================== + +Link module to map leads and issues + """, + 'data': [ + 'project_issue_view.xml', + 'crm_lead_view.xml', + ], + 'depends' : ['crm', 'project_issue'], + 'installable': True, +} diff --git a/addons/crm_project_issue/crm_lead.py b/addons/crm_project_issue/crm_lead.py new file mode 100644 index 00000000000..ec83af18a39 --- /dev/null +++ b/addons/crm_project_issue/crm_lead.py @@ -0,0 +1,10 @@ + +from openerp.osv import osv, fields + + +class crm_lead(osv.Model): + _inherit = 'crm.lead' + + _columns = { + 'project_issue_ids': fields.one2many('project.issue', 'lead_id', "Project Issues"), + } diff --git a/addons/crm_project_issue/crm_lead_view.xml b/addons/crm_project_issue/crm_lead_view.xml new file mode 100644 index 00000000000..0e81f16fa9f --- /dev/null +++ b/addons/crm_project_issue/crm_lead_view.xml @@ -0,0 +1,27 @@ + + + + + + lead.form + crm.lead + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/addons/crm_project_issue/project_issue.py b/addons/crm_project_issue/project_issue.py new file mode 100644 index 00000000000..9409aeb51ae --- /dev/null +++ b/addons/crm_project_issue/project_issue.py @@ -0,0 +1,14 @@ + +from openerp.osv import osv, fields + + +class project_issue(osv.Model): + _inherit = 'project.issue' + + _columns = { + 'lead_id': fields.many2one('crm.lead', ondelete='set null', string="Related lead"), + } + + _defaults = { + + } \ No newline at end of file diff --git a/addons/crm_project_issue/project_issue_view.xml b/addons/crm_project_issue/project_issue_view.xml new file mode 100644 index 00000000000..18a03b5a2c1 --- /dev/null +++ b/addons/crm_project_issue/project_issue_view.xml @@ -0,0 +1,17 @@ + + + + + + project.issue.form + project.issue + + + + + + + + + + \ No newline at end of file From 305896b598922dd4d3c5dfb84d067d9199599d21 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Thu, 13 Feb 2014 16:12:48 +0100 Subject: [PATCH 050/448] [IMP] Report model: added support for datas in 'get_action' method bzr revid: openerp-sle@openerp-sle.home-20140213151248-rwlaebo32edot4a7 --- addons/report/models/report.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 071e9cb3c5d..85be497e69e 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -195,7 +195,7 @@ class report(osv.Model): follow_redirects=True) return response.data - def get_action(self, cr, uid, ids, report_name, context=None): + def get_action(self, cr, uid, ids, report_name, datas=None, context=None): """Used to return an action of type ir.actions.report.xml. :param report_name: Name of the template to generate an action for @@ -203,12 +203,20 @@ class report(osv.Model): if context is None: context = {} + if datas is None: + context = {} + report_obj = self.pool.get('ir.actions.report.xml') idreport = report_obj.search(cr, uid, [('report_name', '=', report_name)], context=context) report = report_obj.browse(cr, uid, idreport[0], context=context) - return { + action = { 'type': 'ir.actions.report.xml', 'report_name': report.report_name, 'report_type': report.report_type, } + + if datas.get('datas'): + action['datas'] = datas + + return action From 8bbb20650464b5784fd3afa0eb219ed1802e2a4f Mon Sep 17 00:00:00 2001 From: "ddm@openerp.com" <> Date: Thu, 13 Feb 2014 16:24:49 +0100 Subject: [PATCH 051/448] [IMP] save new orders only bzr revid: ddm@openerp.com-20140213152449-tarl15agw9r3teqg --- addons/point_of_sale/point_of_sale.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 30fcff2e5e6..dec4431a15c 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -513,13 +513,18 @@ class pos_order(osv.osv): _order = "id desc" def create_from_ui(self, cr, uid, orders, context=None): - #_logger.info("orders: %r", orders) + + # Keep only new orders + submitted_references = [o['data']['name'] for o in orders] + existing_orders = self.search_read(cr, uid, [('pos_reference', 'in', submitted_references)], context) + existing_references = set([o['pos_reference'] for o in existing_orders]) + orders_to_save = [o for o in orders if o['data']['name'] not in existing_references] + order_ids = [] - for tmp_order in orders: + for tmp_order in orders_to_save: to_invoice = tmp_order['to_invoice'] order = tmp_order['data'] - order_id = self.create(cr, uid, { 'name': order['name'], 'user_id': order['user_id'] or False, From e7f238615ca1c79700a0deacb1d1799b797a0472 Mon Sep 17 00:00:00 2001 From: "ddm@openerp.com" <> Date: Thu, 13 Feb 2014 17:38:49 +0100 Subject: [PATCH 052/448] [IMP] fetch only relevant fields when checking for dupes bzr revid: ddm@openerp.com-20140213163849-8v485vq5v4xmeocg --- addons/point_of_sale/point_of_sale.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index 9e78a29cfec..01e7a49ec8c 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -517,7 +517,7 @@ class pos_order(osv.osv): # Keep only new orders submitted_references = [o['data']['name'] for o in orders] - existing_orders = self.search_read(cr, uid, [('pos_reference', 'in', submitted_references)], context) + existing_orders = self.search_read(cr, uid, domain=[('pos_reference', 'in', submitted_references)], fields=['pos_reference'], context=context) existing_references = set([o['pos_reference'] for o in existing_orders]) orders_to_save = [o for o in orders if o['data']['name'] not in existing_references] From 488e8390c63d2e993c69d3da47385a5f10849cd0 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Thu, 13 Feb 2014 18:37:09 +0100 Subject: [PATCH 053/448] [IMP] Report model: added an 'eval_params' method that will convert a dict originated from jquery.param into a python dict bzr revid: openerp-sle@openerp-sle.home-20140213173709-c8pzw0htrj0il53z --- addons/report/models/report.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 85be497e69e..8bf6e779199 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -23,6 +23,7 @@ from openerp.addons.web.http import request from openerp.osv import osv from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT +from openerp.tools.translate import _ import time from datetime import datetime @@ -204,11 +205,16 @@ class report(osv.Model): context = {} if datas is None: - context = {} + datas = {} report_obj = self.pool.get('ir.actions.report.xml') idreport = report_obj.search(cr, uid, [('report_name', '=', report_name)], context=context) - report = report_obj.browse(cr, uid, idreport[0], context=context) + + try: + report = report_obj.browse(cr, uid, idreport[0], context=context) + except IndexError: + raise osv.except_osv(_('Bad Report'), + _('This report is not loaded into the database.')) action = { 'type': 'ir.actions.report.xml', @@ -216,7 +222,29 @@ class report(osv.Model): 'report_type': report.report_type, } - if datas.get('datas'): + if datas: action['datas'] = datas return action + + def eval_params(self, dict_param): + """Parse a dictionary generated from the webclient into a dictionary understandable by a + wizard controller. + """ + for key, value in dict_param.iteritems(): + if value.lower() == 'false': + dict_param[key] = False + elif value.lower() == 'true': + dict_param[key] = True + elif ',' in value or '%2C' in value: + dict_param[key] = [int(i) for i in value.split(',')] + else: + try: + i = int(value) + dict_param[key] = i + except (ValueError, TypeError): + pass + + data = {} + data['form'] = dict_param + return data From dd3122c050bcfb6230247373f1ea8fbe3fa29da6 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Thu, 13 Feb 2014 18:39:10 +0100 Subject: [PATCH 054/448] [IMP] Taxes report converted to QWeb bzr revid: openerp-sle@openerp-sle.home-20140213173910-w1srnhcyrwhrox1h --- addons/account/__openerp__.py | 4 +- addons/account/account_report.xml | 14 +- addons/account/report/__init__.py | 2 +- addons/account/report/account_tax_report.rml | 160 ------------------ .../{account_tax_report.py => report_vat.py} | 53 +++--- addons/account/views/report_vat.xml | 68 ++++++++ addons/account/wizard/account_vat.py | 14 +- 7 files changed, 108 insertions(+), 207 deletions(-) delete mode 100644 addons/account/report/account_tax_report.rml rename addons/account/report/{account_tax_report.py => report_vat.py} (85%) create mode 100644 addons/account/views/report_vat.xml diff --git a/addons/account/__openerp__.py b/addons/account/__openerp__.py index 343512dc271..95ecdc8c35a 100644 --- a/addons/account/__openerp__.py +++ b/addons/account/__openerp__.py @@ -123,7 +123,9 @@ for a particular financial year and for preparation of vouchers there is a modul 'edi/invoice_action_data.xml', 'account_bank_view.xml', 'res_config_view.xml', - 'account_pre_install.yml' + 'account_pre_install.yml', + + 'views/report_vat.xml', ], 'js': [ 'static/src/js/account_move_reconciliation.js', diff --git a/addons/account/account_report.xml b/addons/account/account_report.xml index 6e121c9f5a5..4f9ad1b5c56 100644 --- a/addons/account/account_report.xml +++ b/addons/account/account_report.xml @@ -27,13 +27,13 @@ + id="action_report_vat" + model="account.tax.code" + report_type="qweb-pdf" + string="Account tax" + name="account.report_vat" + file="account.report_vat" + /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Tax Statement - - - - Chart of Tax - Fiscal Year - Periods - Based On - - - [[ get_account(data) or removeParentNode('para') ]] - [[ get_fiscalyear(data) or '' ]] - - - - Start Period - End Period - - - [[ get_start_period(data) or '' ]] - [[ get_end_period(data) or '' ]] - - - - [[ get_basedon(data) or '' ]] - - - - - - Tax Name - Debit - Credit - Tax Amount - - - [[ repeatIn(get_lines(data['form']['based_on'], data['form']['company_id']), 'o') ]][[ (o['level']) ]] [[ (len(o['level'])<5 and setTag('para','para',{'fontName':'Helvetica-Bold'})) or removeParentNode('font') ]][[ o['code'] ]] [[ o['name'] ]] - [[ len(o['level'])<5 and setTag('para','para',{'fontName':"Helvetica-Bold"}) or removeParentNode('font')]][[ o['type']=='view' and removeParentNode('font') ]][[ formatLang(o['debit']) ]][[ o['type']<>'view' and removeParentNode('font') ]][[ formatLang(o['debit']) ]] - [[ len(o['level'])<5 and setTag('para','para',{'fontName':"Helvetica-Bold"}) or removeParentNode('font')]][[ o['type']=='view' and removeParentNode('font') ]][[ formatLang(o['credit']) ]][[ o['type']<>'view' and removeParentNode('font') ]][[ formatLang(o['credit'])]] - [[ len(o['level'])<5 and setTag('para','para',{'fontName':"Helvetica-Bold"}) or removeParentNode('font')]][[ o['type']=='view' and removeParentNode('font') ]][[ formatLang(o['tax_amount'], currency_obj=company.currency_id) ]][[ o['type']<>'view' and removeParentNode('font') ]][[ formatLang(o['tax_amount'], currency_obj=company.currency_id) ]] - - - - diff --git a/addons/account/report/account_tax_report.py b/addons/account/report/report_vat.py similarity index 85% rename from addons/account/report/account_tax_report.py rename to addons/account/report/report_vat.py index b3cbcb441bf..79eeccac66c 100644 --- a/addons/account/report/account_tax_report.py +++ b/addons/account/report/report_vat.py @@ -19,16 +19,20 @@ # ############################################################################## -import time - +from openerp.addons.web import http +from openerp.addons.web.http import request from common_report_header import common_report_header -from openerp.report import report_sxw -class tax_report(report_sxw.rml_parse, common_report_header): - _name = 'report.account.vat.declaration' - def set_context(self, objects, data, ids, report_type=None): - new_ids = ids +class tax_report(http.Controller, common_report_header): + + @http.route(['/report/account.report_vat'], type='http', auth='user', website=True, multilang=True) + def report_account_tax(self, **data): + report_obj = request.registry['report'] + self.cr, self.uid, self.pool = request.cr, request.uid, request.registry + + data = report_obj.eval_params(data) + res = {} self.period_ids = [] period_obj = self.pool.get('account.period') @@ -38,28 +42,17 @@ class tax_report(report_sxw.rml_parse, common_report_header): if data['form'].get('period_from', False) and data['form'].get('period_to', False): self.period_ids = period_obj.build_ctx_periods(self.cr, self.uid, data['form']['period_from'], data['form']['period_to']) - periods_l = period_obj.read(self.cr, self.uid, self.period_ids, ['name']) - for period in periods_l: - if res['periods'] == '': - res['periods'] = period['name'] - else: - res['periods'] += ", "+ period['name'] - return super(tax_report, self).set_context(objects, data, new_ids, report_type=report_type) - def __init__(self, cr, uid, name, context=None): - super(tax_report, self).__init__(cr, uid, name, context=context) - self.localcontext.update({ - 'time': time, - 'get_codes': self._get_codes, - 'get_general': self._get_general, - 'get_currency': self._get_currency, - 'get_lines': self._get_lines, - 'get_fiscalyear': self._get_fiscalyear, - 'get_account': self._get_account, - 'get_start_period': self.get_start_period, - 'get_end_period': self.get_end_period, - 'get_basedon': self._get_basedon, - }) + docargs = { + 'fiscalyear': self._get_fiscalyear(data), + 'account': self._get_account(data), + 'based_on': self._get_basedon(data), + 'period_from': self.get_start_period(data), + 'period_to': self.get_end_period(data), + 'taxlines': self._get_lines(self._get_basedon(data), company_id=data['form']['company_id']), + } + return request.registry['report'].render(self.cr, self.uid, [], 'account.report_vat', docargs) + def _get_basedon(self, form): return form['form']['based_on'] @@ -194,7 +187,6 @@ class tax_report(report_sxw.rml_parse, common_report_header): return self.pool.get('res.company').browse(self.cr, self.uid, form['company_id'], context=context).currency_id.name def sort_result(self, accounts, context=None): - # On boucle sur notre rapport result_accounts = [] ind=0 old_level=0 @@ -233,7 +225,4 @@ class tax_report(report_sxw.rml_parse, common_report_header): return result_accounts -report_sxw.report_sxw('report.account.vat.declaration', 'account.tax.code', - 'addons/account/report/account_tax_report.rml', parser=tax_report, header="internal") - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/account/views/report_vat.xml b/addons/account/views/report_vat.xml new file mode 100644 index 00000000000..3dd1d0c668e --- /dev/null +++ b/addons/account/views/report_vat.xml @@ -0,0 +1,68 @@ + + + + + + diff --git a/addons/account/wizard/account_vat.py b/addons/account/wizard/account_vat.py index 37bf4b029a6..6fbfcc9ddf5 100644 --- a/addons/account/wizard/account_vat.py +++ b/addons/account/wizard/account_vat.py @@ -46,18 +46,20 @@ class account_vat_declaration(osv.osv_memory): def create_vat(self, cr, uid, ids, context=None): if context is None: context = {} + datas = {'ids': context.get('active_ids', [])} datas['model'] = 'account.tax.code' datas['form'] = self.read(cr, uid, ids, context=context)[0] + for field in datas['form'].keys(): if isinstance(datas['form'][field], tuple): datas['form'][field] = datas['form'][field][0] - datas['form']['company_id'] = self.pool.get('account.tax.code').browse(cr, uid, [datas['form']['chart_tax_id']], context=context)[0].company_id.id - return { - 'type': 'ir.actions.report.xml', - 'report_name': 'account.vat.declaration', - 'datas': datas, - } + taxcode_obj = self.pool.get('account.tax.code') + taxcode_id = datas['form']['chart_tax_id'] + taxcode = taxcode_obj.browse(cr, uid, [taxcode_id], context=context)[0] + datas['form']['company_id'] = taxcode.company_id.id + + return self.pool['report'].get_action(cr, uid, ids, 'account.report_vat', datas=datas, context=context) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From f51ffe622793de3b3dd237fcfde7f990b5555cbd Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Fri, 14 Feb 2014 10:48:34 +0100 Subject: [PATCH 055/448] [FIX] Minor fixes in sale and tax views bzr revid: openerp-sle@openerp-sle.home-20140214094834-x3trvtnbmffo1dtb --- addons/account/views/report_vat.xml | 2 +- addons/sale/views/report_saleorder.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/account/views/report_vat.xml b/addons/account/views/report_vat.xml index 3dd1d0c668e..d3a3d28bf58 100644 --- a/addons/account/views/report_vat.xml +++ b/addons/account/views/report_vat.xml @@ -12,7 +12,7 @@ Chart of Tax:

-
+
Fiscal Year:

diff --git a/addons/sale/views/report_saleorder.xml b/addons/sale/views/report_saleorder.xml index 2249a7810a1..2e7a47a98ab 100644 --- a/addons/sale/views/report_saleorder.xml +++ b/addons/sale/views/report_saleorder.xml @@ -2,8 +2,8 @@ - \ No newline at end of file + diff --git a/addons/purchase_requisition/__init__.py b/addons/purchase_requisition/__init__.py index 0c1aab5b6e7..587ce369afb 100644 --- a/addons/purchase_requisition/__init__.py +++ b/addons/purchase_requisition/__init__.py @@ -20,6 +20,4 @@ import purchase_requisition import wizard -import report # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/addons/purchase_requisition/__openerp__.py b/addons/purchase_requisition/__openerp__.py index 3f5fce3e8cc..483ea4a0d56 100644 --- a/addons/purchase_requisition/__openerp__.py +++ b/addons/purchase_requisition/__openerp__.py @@ -39,7 +39,9 @@ keep track and order all your purchase orders. 'purchase_requisition_data.xml', 'purchase_requisition_view.xml', 'purchase_requisition_report.xml', - 'security/ir.model.access.csv','purchase_requisition_sequence.xml' + 'security/ir.model.access.csv','purchase_requisition_sequence.xml', + + 'views/report_purchaserequisition.xml', ], 'auto_install': False, 'test': [ diff --git a/addons/purchase_requisition/purchase_requisition_report.xml b/addons/purchase_requisition/purchase_requisition_report.xml index 97d510fe752..64cbc1e7ea1 100644 --- a/addons/purchase_requisition/purchase_requisition_report.xml +++ b/addons/purchase_requisition/purchase_requisition_report.xml @@ -1,6 +1,13 @@ - + diff --git a/addons/purchase_requisition/report/__init__.py b/addons/purchase_requisition/report/__init__.py deleted file mode 100644 index a2d5684cd1d..00000000000 --- a/addons/purchase_requisition/report/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## -import requisition - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/addons/purchase_requisition/report/purchase_requisition.rml b/addons/purchase_requisition/report/purchase_requisition.rml deleted file mode 100644 index 95c5d19ba31..00000000000 --- a/addons/purchase_requisition/report/purchase_requisition.rml +++ /dev/null @@ -1,239 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Description [[ requisition.line_ids==[] and removeParentNode('blockTable') ]] - - - Qty - - - Product UoM - - - - - [[ repeatIn(objects,'requisition') ]] - - - - Purchase for Requisitions [[ requisition.name ]] - - - - - - - Requisition Reference - - - Requisition Date - - - Type - - - Source - - - - - - - [[ requisition.name ]] - - - [[ formatLang(requisition.date_start,date_time='True') ]] - - - [[ requisition.exclusive=='multiple' and 'Multiple Requisitions' or requisition.exclusive=='exclusive' and 'Purchase Requisitions (exclusive)' ]] - - - [[ requisition.origin ]] - - - - - - - Product Detail [[ requisition.line_ids==[] and removeParentNode('para') ]] - - - - Description [[ requisition.line_ids==[] and removeParentNode('blockTable') ]] - - - Qty - - - Product UoM - - - -
- [[ requisition.line_ids==[] and removeParentNode('section') ]] [[ repeatIn(requisition.line_ids,'line_ids') ]] - - - - [ [[ (line_ids.product_id and line_ids.product_id.code) or '' ]] ] [[ (line_ids.product_id and line_ids.product_id.name) or '' ]] - - - [[ formatLang(line_ids.product_qty) ]] - - - [[ (line_ids.product_uom_id and line_ids.product_uom_id.category_id and line_ids.product_uom_id.category_id.name) or '' ]] - - - -
- - - - Quotation Detail [[ requisition.purchase_ids ==[] and removeParentNode('para') ]] - - - - Supplier [[ requisition.purchase_ids ==[] and removeParentNode('blockTable') ]] - - - Date Ordered - - - Order Reference - - - -
- [[ requisition.purchase_ids ==[] and removeParentNode('section') ]] [[ repeatIn(requisition.purchase_ids,'purchase_ids') ]] - - - - [[ (purchase_ids.partner_id and purchase_ids.partner_id.name) or '' ]] - - - [[ formatLang(purchase_ids.date_order,date='True') ]] - - - [[ purchase_ids.name ]] - - - -
-
-
-
\ No newline at end of file diff --git a/addons/purchase_requisition/report/requisition.py b/addons/purchase_requisition/report/requisition.py deleted file mode 100644 index d7d38f2114e..00000000000 --- a/addons/purchase_requisition/report/requisition.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -import time -from openerp.report import report_sxw - -class requisition(report_sxw.rml_parse): - def __init__(self, cr, uid, name, context): - super(requisition, self).__init__(cr, uid, name, context=context) - self.localcontext.update({ - 'time': time, - }) - -report_sxw.report_sxw('report.purchase.requisition','purchase.requisition','addons/purchase_requisition/report/purchase_requisition.rml',parser=requisition) - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/addons/purchase_requisition/test/purchase_requisition.yml b/addons/purchase_requisition/test/purchase_requisition.yml index ffea3c2a8ce..579febe64ea 100644 --- a/addons/purchase_requisition/test/purchase_requisition.yml +++ b/addons/purchase_requisition/test/purchase_requisition.yml @@ -84,13 +84,3 @@ !python {model: purchase.requisition}: | requisition = self.browse(cr, uid, ref('requisition1'), context=context) requisition.state == 'done', "Requisition should be closed." -- - I print a Requisition report -- - !python {model: purchase.requisition}: | - import os - import openerp.report - from openerp import tools - data, format = openerp.report.render_report(cr, uid, [ref('purchase_requisition.requisition1')], 'purchase.requisition', {}, {}) - if tools.config['test_report_directory']: - file(os.path.join(tools.config['test_report_directory'], 'purchase_requisition-purchase_requisition_report.'+format), 'wb+').write(data) diff --git a/addons/purchase_requisition/views/report_purchaserequisition.xml b/addons/purchase_requisition/views/report_purchaserequisition.xml new file mode 100644 index 00000000000..077772c8543 --- /dev/null +++ b/addons/purchase_requisition/views/report_purchaserequisition.xml @@ -0,0 +1,97 @@ + + + + + + From bccafbdae0e573c203d23e724aa042d3824a00e6 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Fri, 14 Feb 2014 19:00:52 +0100 Subject: [PATCH 060/448] [IMP] Converted the mrp production production order to QWeb bzr revid: openerp-sle@openerp-sle.home-20140214180052-pbluhvtg0ptl7m2c --- addons/mrp/__openerp__.py | 2 + addons/mrp/mrp_report.xml | 16 +- addons/mrp/report/__init__.py | 1 - addons/mrp/report/order.py | 34 -- addons/mrp/report/order.rml | 373 ------------------ addons/mrp/test/order_process.yml | 10 - addons/mrp/views/report_mrporder.xml | 135 +++++++ .../views/report_purchaserequisition.xml | 8 +- 8 files changed, 149 insertions(+), 430 deletions(-) delete mode 100644 addons/mrp/report/order.py delete mode 100644 addons/mrp/report/order.rml create mode 100644 addons/mrp/views/report_mrporder.xml diff --git a/addons/mrp/__openerp__.py b/addons/mrp/__openerp__.py index 99afd01707a..e5bd8171c60 100644 --- a/addons/mrp/__openerp__.py +++ b/addons/mrp/__openerp__.py @@ -72,6 +72,8 @@ Dashboard / Reports for MRP will include: 'report/mrp_production_order_view.xml', 'board_manufacturing_view.xml', 'res_config_view.xml', + + 'views/report_mrporder.xml', ], 'demo': ['mrp_demo.xml'], #TODO: This yml tests are needed to be completely reviewed again because the product wood panel is removed in product demo as it does not suit for new demo context of computer and consultant company diff --git a/addons/mrp/mrp_report.xml b/addons/mrp/mrp_report.xml index c37343f50db..781615588a7 100644 --- a/addons/mrp/mrp_report.xml +++ b/addons/mrp/mrp_report.xml @@ -4,14 +4,14 @@ - + ). -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -import time -from openerp.report import report_sxw - -class order(report_sxw.rml_parse): - def __init__(self, cr, uid, name, context): - super(order, self).__init__(cr, uid, name, context=context) - self.localcontext.update({ - 'time': time, - }) - -report_sxw.report_sxw('report.mrp.production.order','mrp.production','addons/mrp/report/order.rml',parser=order,header='internal') - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mrp/report/order.rml b/addons/mrp/report/order.rml deleted file mode 100644 index f413a6cfc70..00000000000 --- a/addons/mrp/report/order.rml +++ /dev/null @@ -1,373 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Product - - - Quantity - - - Source Location - - - Destination Location - - - - - [[repeatIn(objects,'o')]] - - - - Production Order N° : [[ o.name ]] - - - - - - - Source Document - - - Product - - - Quantity - - - - - - - [[ o.origin ]] - - - [[ (o.product_id and o.product_id.code) or '' ]] [[ (o.product_id and o.product_id.name) or '' ]] - - - [[formatLang(o.product_qty) ]] [[ (o.product_id and o.product_uom and o.product_uom.name) or '']] - - - - - - - - - - Scheduled Date - - - Printing date - - - Partner Ref - - - SO Number - - - -
- - - - [[ formatLang(o.date_planned, date_time = True) ]] - - - [[ formatLang(time.strftime('%Y-%m-%d'),date = True) ]] - - - [[ ('sale_ref' in o._columns.keys() and o.sale_ref) or '' ]] - - - [[ ('sale_name' in o._columns.keys() and o.sale_name) or '' ]] - - - -
- - - - Work Orders [[ o.workcenter_lines ==[] and removeParentNode('para')]] - - - - - - - Sequence - - - Name [[ o.workcenter_lines ==[] and removeParentNode('blockTable')]] - - - WorkCenter - - - No. Of Cycles - - - No. Of Hours - - - -
- [[ repeatIn(o.workcenter_lines,'line2') ]] - - - - [[ str(line2.sequence) ]] - - - [[ line2.name ]] - - - [[ (line2.workcenter_id and line2.workcenter_id.name) or '' ]] - - - [[ formatLang(line2.cycle) ]] - - - [[ formatLang(line2.hour) ]] - - - - - - -
- - - - Bill Of Material - - - - - - - Product - - - Quantity - - - Source Location - - - Destination Location - - - -
- Products to Consume [[ o.move_lines ==[] and removeParentNode('section')]] -
- [[ repeatIn(o.move_lines,'line') ]] - - - - [[ (line.product_id and line.product_id.code) or '' ]] [[ (line.product_id and line.product_id.name) or '' ]] - - - [[ formatLang( line.product_qty) ]] [[ (line.product_uom and line.product_uom.name) or '']] - - - [[ (line.location_id and line.location_id.name) or '' ]] - - - [[ (line.location_dest_id and line.location_dest_id.name) or '' ]] - - - -
-
-
- - - - Consumed Products [[ o.move_lines2 ==[] and removeParentNode('section')]] -
- [[ repeatIn(o.move_lines2,'line2') ]] - - - - [[ (line2.product_id and line2.product_id.code) or '' ]] [[ (line2.product_id and line2.product_id.name) or '' ]] - - - [[ formatLang( line2.product_qty) ]] [[ (line2.product_uom and line2.product_uom.name) or '']] - - - [[ (line2.location_id and line2.location_id.name) or '' ]] - - - [[ (line2.location_dest_id and line2.location_dest_id.name) or '' ]] - - - -
- - - -
-
-
-
\ No newline at end of file diff --git a/addons/mrp/test/order_process.yml b/addons/mrp/test/order_process.yml index c4050208570..088cdf4311a 100644 --- a/addons/mrp/test/order_process.yml +++ b/addons/mrp/test/order_process.yml @@ -238,16 +238,6 @@ data, format = openerp.report.render_report(cr, uid, [order.bom_id.id], 'bom.structure', {}, {}) if tools.config['test_report_directory']: file(os.path.join(tools.config['test_report_directory'], 'mrp-bom_structure_report.'+format), 'wb+').write(data) -- - I print "Production Order". -- - !python {model: mrp.production}: | - import os - import openerp.report - from openerp import tools - data, format = openerp.report.render_report(cr, uid, [ref("mrp_production_test1")], 'mrp.production.order', {}, {}) - if tools.config['test_report_directory']: - file(os.path.join(tools.config['test_report_directory'], 'mrp-production_order_report.'+format), 'wb+').write(data) - I print "Work Center Load Report". - diff --git a/addons/mrp/views/report_mrporder.xml b/addons/mrp/views/report_mrporder.xml new file mode 100644 index 00000000000..52390f095d3 --- /dev/null +++ b/addons/mrp/views/report_mrporder.xml @@ -0,0 +1,135 @@ + + + + + + diff --git a/addons/purchase_requisition/views/report_purchaserequisition.xml b/addons/purchase_requisition/views/report_purchaserequisition.xml index 077772c8543..9568762b589 100644 --- a/addons/purchase_requisition/views/report_purchaserequisition.xml +++ b/addons/purchase_requisition/views/report_purchaserequisition.xml @@ -11,20 +11,20 @@

Purchase for Requisitions

-
+
Requisition Reference:
-
+
Requisition Date:
-
+
Type:
Purchase Requisitions (exclusive) Multiple Requisitions
-
+
Source:
From f43f978427d96b6edac74de7af2e1d320d64cb17 Mon Sep 17 00:00:00 2001 From: openerp-sle Date: Sun, 16 Feb 2014 12:04:30 +0100 Subject: [PATCH 061/448] [FIX] Use the local bootstrap bzr revid: openerp-sle@openerp-sle.home-20140216110430-sbzd306ufay3ukxn --- addons/report/controllers/main.py | 2 +- addons/report/views/layouts.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/report/controllers/main.py b/addons/report/controllers/main.py index bd5b41d9a9d..52a949c0bd2 100644 --- a/addons/report/controllers/main.py +++ b/addons/report/controllers/main.py @@ -134,6 +134,7 @@ class Report(http.Controller): # Get some css and script in order to build a minimal html page for the report. # This page will later be sent to wkhtmltopdf. css = self._get_url_content('/report/static/src/css/reset.min.css') + css += self._get_url_content('/web/static/lib/bootstrap/css/bootstrap.css') css += self._get_url_content('/website/static/src/css/website.css') subst = self._get_url_content('/report/static/src/js/subst.js') @@ -144,7 +145,6 @@ class Report(http.Controller): - diff --git a/addons/report/views/layouts.xml b/addons/report/views/layouts.xml index bff67a9051a..45e6526cf94 100644 --- a/addons/report/views/layouts.xml +++ b/addons/report/views/layouts.xml @@ -24,7 +24,7 @@ - + @@ -169,12 +173,12 @@ class Report(http.Controller): for node in root.xpath("//div[@class='header']"): body = lxml.html.tostring(node) - header = minimalhtml.format(css, subst, body) + header = minimalhtml.format(css, subst, body, base_url) headerhtml.append(header) for node in root.xpath("//div[@class='footer']"): body = lxml.html.tostring(node) - footer = minimalhtml.format(css, subst, body) + footer = minimalhtml.format(css, subst, body, base_url) footerhtml.append(footer) for node in root.xpath("//div[@class='page']"): @@ -191,7 +195,7 @@ class Report(http.Controller): reportid = False body = lxml.html.tostring(node) - reportcontent = minimalhtml.format(css, '', body) + reportcontent = minimalhtml.format(css, '', body, base_url) contenthtml.append(tuple([reportid, reportcontent])) except lxml.etree.XMLSyntaxError: @@ -255,9 +259,11 @@ class Report(http.Controller): command = ['wkhtmltopdf-0.12'] tmp_dir = tempfile.gettempdir() - # Display arguments command_args = [] + # Passing the cookie in order to resolve URL. + command_args.extend(['--cookie', 'session_id', request.httprequest.cookies['session_id']]) + # Display arguments if paperformat: command_args.extend(self._build_wkhtmltopdf_args(paperformat, spec_paperformat_args)) @@ -316,7 +322,7 @@ class Report(http.Controller): if process.returncode != 0: raise except_osv(_('Report (PDF)'), - _('wkhtmltopdf-patched failed with error code = %s. ' + _('wkhtmltopdf-0.12 failed with error code = %s. ' 'Message: %s') % (str(process.returncode), err)) # Save the pdf in attachment if marked @@ -354,6 +360,7 @@ class Report(http.Controller): def _build_wkhtmltopdf_args(self, paperformat, specific_paperformat_args=None): """Build arguments understandable by wkhtmltopdf from an ir.actions.report.paperformat record. + Sample: :paperformat: ir.actions.report.paperformat record associated to a document :specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments @@ -370,7 +377,7 @@ class Report(http.Controller): if specific_paperformat_args and specific_paperformat_args['data-report-margin-top']: command_args.extend(['--margin-top', str(specific_paperformat_args['data-report-margin-top'])]) - else: + elif paperformat.margin_top: command_args.extend(['--margin-top', str(paperformat.margin_top)]) if paperformat.margin_left: @@ -449,15 +456,22 @@ class Report(http.Controller): response.headers.add('Content-Disposition', 'attachment; filename=report.pdf;') return response - @http.route('/report/getbarcode//', type='http', auth="user") - def barcode(self, type, value): - """Accepted types: 'Codabar', 'Code11', 'Code128', 'EAN13', 'EAN8', 'Extended39', + @http.route([ + '/report/getbarcode//', + '/report/getbarcode////', + ], type='http', auth="user") + def barcode(self, type, value, width=300, height=50): + """Contoller able to render barcode images thanks to reportlab. + + :param type: Accepted types: 'Codabar', 'Code11', 'Code128', 'EAN13', 'EAN8', 'Extended39', 'Extended93', 'FIM', 'I2of5', 'MSI', 'POSTNET', 'QR', 'Standard39', 'Standard93', 'UPCA', 'USPS_4State' """ try: - barcode = createBarcodeImageInMemory(type, value=value) + barcode = createBarcodeImageInMemory( + type, value=value, format='jpg', width=width, height=height + ) except (ValueError, AttributeError): - raise exceptions.HTTPException(description="Cannot convert into barcode.") + raise exceptions.HTTPException(description='Cannot convert into barcode.') return request.make_response(barcode, headers=[('Content-Type', 'image/jpg')]) diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 92bec703137..86b4c69c2a1 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -196,7 +196,6 @@ class report(osv.Model): 'formatLang': self.formatLang, 'get_digits': self.get_digits, 'render_doc': render_doc, - 'website': website, 'res_company': res_company, }) From 6305ac409444ca01388ce1ad66ef2af3783360bf Mon Sep 17 00:00:00 2001 From: Kersten Jeremy Date: Thu, 20 Feb 2014 16:08:17 +0100 Subject: [PATCH 079/448] [FIX] Disable traceback popup when rpc /calendar/notify fail bzr revid: jke@openerp.com-20140220150817-3rvvm8neeqviba6o --- addons/calendar/static/src/js/base_calendar.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js index 18fd4dfa20e..4384964575c 100644 --- a/addons/calendar/static/src/js/base_calendar.js +++ b/addons/calendar/static/src/js/base_calendar.js @@ -11,7 +11,7 @@ openerp.calendar = function(instance) { get_next_notif: function() { var self= this; this.rpc("/calendar/notify") - .then( + .done( function(result) { _.each(result, function(res) { setTimeout(function() { @@ -44,6 +44,12 @@ openerp.calendar = function(instance) { },res.timer * 1000); }); } + ) + .fail( + // To override error from framework.js in RPC function + function(error, event) { + event.preventDefault(); + } ); }, check_notifications: function() { @@ -51,14 +57,14 @@ openerp.calendar = function(instance) { self.get_next_notif(); setInterval(function(){ self.get_next_notif(); - }, 5 * 60 * 1000 ); + }, 5 * 60 * 1000 ); }, //Override the show_application of addons/web/static/src/js/chrome.js show_application: function() { this._super(); this.check_notifications(); - }, + }, }); From 8d8ede7f7d075c29f903b2714028c009b7a337ec Mon Sep 17 00:00:00 2001 From: Kersten Jeremy Date: Thu, 20 Feb 2014 16:08:17 +0100 Subject: [PATCH 080/448] [FIX] Disable traceback popup when rpc /calendar/notify fail bzr revid: jke@openerp.com-20140220150817-0yyovkr4ooqu8ejc --- addons/calendar/static/src/js/base_calendar.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/addons/calendar/static/src/js/base_calendar.js b/addons/calendar/static/src/js/base_calendar.js index 18fd4dfa20e..4384964575c 100644 --- a/addons/calendar/static/src/js/base_calendar.js +++ b/addons/calendar/static/src/js/base_calendar.js @@ -11,7 +11,7 @@ openerp.calendar = function(instance) { get_next_notif: function() { var self= this; this.rpc("/calendar/notify") - .then( + .done( function(result) { _.each(result, function(res) { setTimeout(function() { @@ -44,6 +44,12 @@ openerp.calendar = function(instance) { },res.timer * 1000); }); } + ) + .fail( + // To override error from framework.js in RPC function + function(error, event) { + event.preventDefault(); + } ); }, check_notifications: function() { @@ -51,14 +57,14 @@ openerp.calendar = function(instance) { self.get_next_notif(); setInterval(function(){ self.get_next_notif(); - }, 5 * 60 * 1000 ); + }, 5 * 60 * 1000 ); }, //Override the show_application of addons/web/static/src/js/chrome.js show_application: function() { this._super(); this.check_notifications(); - }, + }, }); From 2e49e46698c52edf29c7a42b1a67f8f2cb110e34 Mon Sep 17 00:00:00 2001 From: Paramjit Singh Sahota Date: Fri, 21 Feb 2014 11:42:36 +0530 Subject: [PATCH 081/448] [FIX] Fixed the sliding issue of carousel library in IE9. The transition was not working in any snippet bzr revid: psa@tinyerp.com-20140221061236-7jcbgae7m081pjv8 --- addons/website/static/lib/bootstrap/js/bootstrap.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/addons/website/static/lib/bootstrap/js/bootstrap.js b/addons/website/static/lib/bootstrap/js/bootstrap.js index b22be6fce1d..2d31d0eb3c3 100644 --- a/addons/website/static/lib/bootstrap/js/bootstrap.js +++ b/addons/website/static/lib/bootstrap/js/bootstrap.js @@ -418,6 +418,17 @@ if (typeof jQuery === "undefined") { throw new Error("Bootstrap requires jQuery" setTimeout(function () { that.$element.trigger('slid') }, 0) }) .emulateTransitionEnd(600) + } else if(this.$element.hasClass('slide')) { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $active.animate({left: (direction == 'right' ? '100%' : '-100%')}, 600, function(){ + $active.removeClass('active') + that.sliding = false + setTimeout(function() { that.$element.trigger('slid')}, 0) + }) + $next.addClass(type).css({left: (direction == 'right' ? '-100%' : '100%')}).animate({left: 0}, 600, function() { + $next.removeClass(type).addClass('active') + }) } else { this.$element.trigger(e) if (e.isDefaultPrevented()) return From 777506617af86dad56743ae014866b5bb6224c59 Mon Sep 17 00:00:00 2001 From: Paramjit Singh Sahota Date: Fri, 21 Feb 2014 12:45:12 +0530 Subject: [PATCH 082/448] [FIX] 'window.location.origin' is not found in IE browser thats why generated manually. bzr revid: psa@tinyerp.com-20140221071512-4uky4k2wiu6miysi --- addons/website/static/src/js/website.mobile.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/addons/website/static/src/js/website.mobile.js b/addons/website/static/src/js/website.mobile.js index 4acf2d78db8..ac3b17ddad7 100644 --- a/addons/website/static/src/js/website.mobile.js +++ b/addons/website/static/src/js/website.mobile.js @@ -17,6 +17,9 @@ 'hidden.bs.modal': 'destroy' }, start: function () { + if (!window.location.origin) { + window.location.origin = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port: ''); + } document.getElementById("mobile-viewport").src = window.location.origin + window.location.pathname + "#mobile-preview"; this.$el.modal(); }, From 0304f71112d85b306f752f45869bdc4baf4034e5 Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Fri, 21 Feb 2014 11:15:20 +0100 Subject: [PATCH 083/448] [FIX] Small changes in barcode controller and set default values when creating paper format bzr revid: sle@openerp.com-20140221101520-tg522m1w38jdp55p --- addons/account/report/report_vat.py | 1 - addons/report/controllers/main.py | 13 ++++++------- addons/report/models/report_paperformat.py | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/addons/account/report/report_vat.py b/addons/account/report/report_vat.py index 79eeccac66c..2d31a5c7a70 100644 --- a/addons/account/report/report_vat.py +++ b/addons/account/report/report_vat.py @@ -53,7 +53,6 @@ class tax_report(http.Controller, common_report_header): } return request.registry['report'].render(self.cr, self.uid, [], 'account.report_vat', docargs) - def _get_basedon(self, form): return form['form']['based_on'] diff --git a/addons/report/controllers/main.py b/addons/report/controllers/main.py index af04e5749eb..724d3040e36 100644 --- a/addons/report/controllers/main.py +++ b/addons/report/controllers/main.py @@ -360,7 +360,6 @@ class Report(http.Controller): def _build_wkhtmltopdf_args(self, paperformat, specific_paperformat_args=None): """Build arguments understandable by wkhtmltopdf from an ir.actions.report.paperformat record. - Sample: :paperformat: ir.actions.report.paperformat record associated to a document :specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments @@ -456,12 +455,10 @@ class Report(http.Controller): response.headers.add('Content-Disposition', 'attachment; filename=report.pdf;') return response - @http.route([ - '/report/getbarcode//', - '/report/getbarcode////', - ], type='http', auth="user") + @http.route(['/report/barcode', '/report/barcode//'], type='http', auth="user") def barcode(self, type, value, width=300, height=50): """Contoller able to render barcode images thanks to reportlab. + Sample: :param type: Accepted types: 'Codabar', 'Code11', 'Code128', 'EAN13', 'EAN8', 'Extended39', 'Extended93', 'FIM', 'I2of5', 'MSI', 'POSTNET', 'QR', 'Standard39', 'Standard93', @@ -469,9 +466,11 @@ class Report(http.Controller): """ try: barcode = createBarcodeImageInMemory( - type, value=value, format='jpg', width=width, height=height + type, value=value, format='png', width=width, height=height ) except (ValueError, AttributeError): raise exceptions.HTTPException(description='Cannot convert into barcode.') + except AssertionError: + raise exceptions.HTTPException(description='Please upgrade reportlab to at least 3.0.') - return request.make_response(barcode, headers=[('Content-Type', 'image/jpg')]) + return request.make_response(barcode, headers=[('Content-Type', 'image/png')]) diff --git a/addons/report/models/report_paperformat.py b/addons/report/models/report_paperformat.py index 188de726f48..975ed779d97 100644 --- a/addons/report/models/report_paperformat.py +++ b/addons/report/models/report_paperformat.py @@ -76,7 +76,7 @@ class report_paperformat(osv.Model): 'Orientation'), 'header_line': fields.boolean('Display a header line'), 'header_spacing': fields.integer('Header spacing'), - 'dpi': fields.integer('Output DPI'), + 'dpi': fields.integer('Output DPI', required=True), 'report_ids': fields.one2many('ir.actions.report.xml', 'paperformat_id', 'Associated reports', @@ -94,6 +94,20 @@ class report_paperformat(osv.Model): 'page width/height.', ['format']), ] + _defaults = { + 'format': 'A4', + 'margin_top': 40, + 'margin_bottom': 20, + 'margin_left': 7, + 'margin_right': 7, + 'page_height': False, + 'page_width': False, + 'orientation': 'Landscape', + 'header_line': False, + 'header_spacing': 35, + 'dpi': 90, + } + class res_company(osv.Model): _inherit = 'res.company' From 467ec6a91953378904f2462631cc4d169a2773ba Mon Sep 17 00:00:00 2001 From: Pariket Trivedi Date: Fri, 21 Feb 2014 18:42:15 +0530 Subject: [PATCH 084/448] [FIX] Fixed a small problem which falls to load the template in mobile bzr revid: psa@tinyerp.com-20140221131215-nf1y2t3qbeiosujb --- addons/website/static/src/js/website.mobile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/website/static/src/js/website.mobile.js b/addons/website/static/src/js/website.mobile.js index ac3b17ddad7..35b3e7c5acd 100644 --- a/addons/website/static/src/js/website.mobile.js +++ b/addons/website/static/src/js/website.mobile.js @@ -18,7 +18,7 @@ }, start: function () { if (!window.location.origin) { - window.location.origin = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port: ''); + window.location.origin = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port: '') + '/'; } document.getElementById("mobile-viewport").src = window.location.origin + window.location.pathname + "#mobile-preview"; this.$el.modal(); From ffa475afcd2b0b2504a0a0947b2368c130d73815 Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Fri, 21 Feb 2014 17:33:20 +0100 Subject: [PATCH 085/448] [IMP] Added the possibility to define ultra-specific reports outputing any king of file. These reports are of type 'controller' because they redirects to a controller that must returns a response containing the http headers 'content-type' and 'content-disposition'. The route for this controller is specified in the report_file field. Added an XLS version of the tax report for the POC (testable by setting 'report_vat' type to controller and setting '/report/account.report_vat_xls' in the 'report_file' field). bzr revid: sle@openerp.com-20140221163320-g5ouoywsuduoi0qe --- addons/account/report/report_vat.py | 44 ++++++++++++ addons/account/views/report_vat.xml | 2 +- addons/report/controllers/main.py | 67 ++++++++++++------- addons/report/models/report.py | 1 + .../report/static/src/js/qwebactionmanager.js | 35 +++++++--- 5 files changed, 114 insertions(+), 35 deletions(-) diff --git a/addons/account/report/report_vat.py b/addons/account/report/report_vat.py index 2d31a5c7a70..236e2ffaac2 100644 --- a/addons/account/report/report_vat.py +++ b/addons/account/report/report_vat.py @@ -22,6 +22,11 @@ from openerp.addons.web import http from openerp.addons.web.http import request from common_report_header import common_report_header +try: + import cStringIO as StringIO +except ImportError: + import StringIO +import xlwt class tax_report(http.Controller, common_report_header): @@ -224,4 +229,43 @@ class tax_report(http.Controller, common_report_header): return result_accounts + @http.route(['/report/account.report_vat_xls'], type='http', auth='user', website=True, multilang=True) + def report_account_tax_xls(self, **data): + report_obj = request.registry['report'] + self.cr, self.uid, self.pool = request.cr, request.uid, request.registry + + data = report_obj.eval_params(data) + + res = {} + self.period_ids = [] + period_obj = self.pool.get('account.period') + self.display_detail = data['form']['display_detail'] + res['periods'] = '' + res['fiscalyear'] = data['form'].get('fiscalyear_id', False) + + if data['form'].get('period_from', False) and data['form'].get('period_to', False): + self.period_ids = period_obj.build_ctx_periods(self.cr, self.uid, data['form']['period_from'], data['form']['period_to']) + + content = '' + lines = self._get_lines(self._get_basedon(data), company_id=data['form']['company_id']) + + if lines: + xls = StringIO.StringIO() + xls_workbook = xlwt.Workbook() + vat_sheet = xls_workbook.add_sheet('report_vat') + + for x in range(0, len(lines)): + for y in range(0, len(lines[0])): + vat_sheet.write(x, y, lines[x].values()[y]) + + xls_workbook.save(xls) + xls.seek(0) + content = xls.read() + + response = request.make_response(content, headers=[ + ('Content-Type', 'application/vnd.ms-excel'), + ('Content-Disposition', 'attachment; filename=report_vat.xls;') + ]) + return response + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/account/views/report_vat.xml b/addons/account/views/report_vat.xml index 4ef1fb54aec..03664045ef4 100644 --- a/addons/account/views/report_vat.xml +++ b/addons/account/views/report_vat.xml @@ -7,7 +7,7 @@

Tax Statement

-
+
Chart of Tax:

diff --git a/addons/report/controllers/main.py b/addons/report/controllers/main.py index 724d3040e36..df68fce1291 100644 --- a/addons/report/controllers/main.py +++ b/addons/report/controllers/main.py @@ -83,11 +83,18 @@ class Report(http.Controller): 'docs': docs, } - return request.registry['report'].render(request.cr, request.uid, [], report.report_file, + return request.registry['report'].render(request.cr, request.uid, [], report.report_name, docargs, context=request.context) @http.route(['/report/pdf/'], type='http', auth="user", website=True) def report_pdf(self, path=None, landscape=False, **post): + """Route converting any reports to pdf. It will get the html-rendered report, extract + header, page and footer in order to prepare minimal html pages that will be further passed + to wkhtmltopdf. + + :param path: URL of the report (e.g. /report/account.report_invoice/1) + :returns: a response with 'application/pdf' headers and the pdf as content + """ cr, uid, context = request.cr, request.uid, request.context # Get the report we are working on. @@ -136,14 +143,14 @@ class Report(http.Controller): paperformat = report.paperformat_id # Get the html report. - html = self._get_url_content('/' + path, post) + html = self._get_url_content('/' + path, post)[0] # Get some css and script in order to build a minimal html page for the report. # This page will later be sent to wkhtmltopdf. - css = self._get_url_content('/report/static/src/css/reset.min.css') - css += self._get_url_content('/web/static/lib/bootstrap/css/bootstrap.css') - css += self._get_url_content('/website/static/src/css/website.css') - subst = self._get_url_content('/report/static/src/js/subst.js') + css = self._get_url_content('/report/static/src/css/reset.min.css')[0] + css += self._get_url_content('/web/static/lib/bootstrap/css/bootstrap.css')[0] + css += self._get_url_content('/website/static/src/css/website.css')[0] + subst = self._get_url_content('/report/static/src/js/subst.js')[0] headerhtml = [] contenthtml = [] @@ -240,7 +247,7 @@ class Report(http.Controller): except UnicodeDecodeError: pass - return content + return tuple([content, response.headers]) def _generate_wkhtml_pdf(self, headers, footers, bodies, landscape, paperformat, spec_paperformat_args=None, save_in_attachment=None): @@ -440,31 +447,19 @@ class Report(http.Controller): merged.close() return content - @http.route('/report/downloadpdf/', type='http', auth="user") - def report_pdf_attachment(self, data, token): - """This function is only used by 'qwebactionmanager.js' in order to trigger the download of - a pdf report. - - :param data: The JSON.stringified report internal url - :returns: Response with a filetoken cookie and an attachment header - """ - url = simplejson.loads(data) - pdf = self._get_url_content(url) - response = self._make_pdf_response(pdf) - response.set_cookie('fileToken', token) - response.headers.add('Content-Disposition', 'attachment; filename=report.pdf;') - return response - @http.route(['/report/barcode', '/report/barcode//'], type='http', auth="user") def barcode(self, type, value, width=300, height=50): """Contoller able to render barcode images thanks to reportlab. - Sample: + Samples: + + :param type: Accepted types: 'Codabar', 'Code11', 'Code128', 'EAN13', 'EAN8', 'Extended39', 'Extended93', 'FIM', 'I2of5', 'MSI', 'POSTNET', 'QR', 'Standard39', 'Standard93', 'UPCA', 'USPS_4State' """ try: + width, height = int(width), int(height) barcode = createBarcodeImageInMemory( type, value=value, format='png', width=width, height=height ) @@ -474,3 +469,29 @@ class Report(http.Controller): raise exceptions.HTTPException(description='Please upgrade reportlab to at least 3.0.') return request.make_response(barcode, headers=[('Content-Type', 'image/png')]) + + @http.route('/report/download/', type='http', auth="user") + def report_attachment(self, data, token): + """This function is only used by 'qwebactionmanager.js' in order to trigger the download of + a report. + + :param data: The JSON.stringified report internal url + :returns: Response with a filetoken cookie and an attachment header + """ + requestcontent = simplejson.loads(data) + url, type = requestcontent[0], requestcontent[1] + file, fileheaders = self._get_url_content(url) + + if type == 'qweb-pdf': + response = self._make_pdf_response(file) + response.headers.add('Content-Disposition', 'attachment; filename=report.pdf;') + elif type == 'controller': + response = request.make_response(file) + response.headers.add('Content-Disposition', fileheaders['Content-Disposition']) + response.headers.add('Content-Type', fileheaders['Content-Type']) + else: + return + + response.headers.add('Content-Length', len(file)) + response.set_cookie('fileToken', token) + return response diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 86b4c69c2a1..e54e2b9a6a2 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -241,6 +241,7 @@ class report(osv.Model): 'type': 'ir.actions.report.xml', 'report_name': report.report_name, 'report_type': report.report_type, + 'report_file': report.report_file, } if datas: diff --git a/addons/report/static/src/js/qwebactionmanager.js b/addons/report/static/src/js/qwebactionmanager.js index 37f5489ab37..615b3677c30 100644 --- a/addons/report/static/src/js/qwebactionmanager.js +++ b/addons/report/static/src/js/qwebactionmanager.js @@ -9,13 +9,22 @@ openerp.report = function(instance) { action.context = instance.web.pyeval.eval('contexts',eval_contexts); // QWeb reports - if ('report_type' in action && (action.report_type == 'qweb-html' || action.report_type == 'qweb-pdf')) { - var report_url = ''; - - if (action.report_type == 'qweb-html') { - report_url = '/report/' + action.report_name; - } else { - report_url = '/report/pdf/report/' + action.report_name; + if ('report_type' in action && (action.report_type == 'qweb-html' || action.report_type == 'qweb-pdf' || action.report_type == 'controller')) { + + var report_url = '' + switch (action.report_type) { + case 'qweb-html': + report_url = '/report/' + action.report_name; + break; + case 'qweb-pdf': + report_url = '/report/pdf/report/' + action.report_name; + break; + case 'controller': + report_url = action.report_file; + break; + default: + report_url = '/report/' + action.report_name; + break; } // single/multiple id(s): no query string @@ -27,7 +36,7 @@ openerp.report = function(instance) { } else { _.each(action.datas.form, function(value, key) { // will be erased when all wizards are rewritten - if(key.substring(0, 12) === 'used_context') { + if (key.substring(0, 12) === 'used_context') { delete action.datas.form[key]; } @@ -43,11 +52,15 @@ openerp.report = function(instance) { instance.web.unblockUI(); return; } else { - // Trigger the download of the pdf report + // Trigger the download of the pdf/custom controller report var c = openerp.webclient.crashmanager; + var response = new Array() + response[0] = report_url + response[1] = action.report_type + this.session.get_file({ - url: '/report/downloadpdf', - data: {data: JSON.stringify(report_url)}, + url: '/report/download', + data: {data: JSON.stringify(response)}, complete: openerp.web.unblockUI, error: c.rpc_error.bind(c) }); From 1bc89c97383b572ee3c199dbc1524ab7f27c7010 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 24 Feb 2014 10:48:29 +0100 Subject: [PATCH 086/448] [FIX] unbreak website_blog.css bzr revid: xmo@openerp.com-20140224094829-99ayhh5shzwu6n44 --- addons/website_blog/static/src/css/website_blog.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/addons/website_blog/static/src/css/website_blog.css b/addons/website_blog/static/src/css/website_blog.css index 96045adb914..f7355ce783d 100644 --- a/addons/website_blog/static/src/css/website_blog.css +++ b/addons/website_blog/static/src/css/website_blog.css @@ -1,5 +1,4 @@ -@charset "utf-8"; -@import url(compass/css3.css); +@charset "UTF-8"; .css_website_mail .has-error { border-color: red; } From 09d34a8e9651615ca60b84c939fdb960f3c1295b Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Mon, 24 Feb 2014 11:33:29 +0100 Subject: [PATCH 087/448] [IMP] Report controller: put the css/js as relative links in the minimal html page (they were copied inline) bzr revid: sle@openerp.com-20140224103329-pmmmcl56mzmynmx5 --- addons/report/controllers/main.py | 33 +++++++++++++++++-------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/addons/report/controllers/main.py b/addons/report/controllers/main.py index df68fce1291..1390f401bcc 100644 --- a/addons/report/controllers/main.py +++ b/addons/report/controllers/main.py @@ -68,7 +68,7 @@ class Report(http.Controller): name="module.template_name" file="module.template_name" - If you don't want your report to be listed under the print button, just add + If you don't want your report listed under the print button, just add 'menu=False'. """ ids = [int(i) for i in docids.split(',')] @@ -102,7 +102,7 @@ class Report(http.Controller): reportname_in_path = path.split('/')[1].split('?')[0] report = self._get_report_from_name(reportname_in_path) - # Check attachment_use field. If set to true and an existing pdf is already saved, return + # Check attachment_use field. If set to true and an existing pdf is already saved, load # this one now. If not, mark save it. save_in_attachment = {} @@ -135,7 +135,8 @@ class Report(http.Controller): # Mark current document to be saved save_in_attachment[path_id] = filename - # Get the paperformat associated to the report, if there is. + # Get the paperformat associated to the report. If there is not, get the one associated to + # the company. if not report.paperformat_id: user = request.registry['res.users'].browse(cr, uid, uid, context=context) paperformat = user.company_id.paperformat_id @@ -144,13 +145,8 @@ class Report(http.Controller): # Get the html report. html = self._get_url_content('/' + path, post)[0] - - # Get some css and script in order to build a minimal html page for the report. - # This page will later be sent to wkhtmltopdf. - css = self._get_url_content('/report/static/src/css/reset.min.css')[0] - css += self._get_url_content('/web/static/lib/bootstrap/css/bootstrap.css')[0] - css += self._get_url_content('/website/static/src/css/website.css')[0] - subst = self._get_url_content('/report/static/src/js/subst.js')[0] + subst = self._get_url_content('/report/static/src/js/subst.js')[0] # Used in age numbering + css = '' # Local css headerhtml = [] contenthtml = [] @@ -162,7 +158,13 @@ class Report(http.Controller): + + + + + + @@ -171,7 +173,7 @@ class Report(http.Controller): """ # The retrieved html report must be simplified. We convert it into a xml tree - # via lxml in order to extract header, footer and all reportcontent. + # via lxml in order to extract headers, footers and content. try: root = lxml.html.fromstring(html) @@ -190,7 +192,7 @@ class Report(http.Controller): for node in root.xpath("//div[@class='page']"): # Previously, we marked some reports to be saved in attachment via their ids, so we - # must set a relation between report ids and reportcontent. We use the QWeb + # must set a relation between report ids and report's content. We use the QWeb # branding in order to do so: searching after a node having a data-oe-model # attribute with the value of the current report model and read its oe-id attribute oemodelnode = node.find(".//*[@data-oe-model='" + report.model + "']") @@ -472,10 +474,11 @@ class Report(http.Controller): @http.route('/report/download/', type='http', auth="user") def report_attachment(self, data, token): - """This function is only used by 'qwebactionmanager.js' in order to trigger the download of - a report. + """This function is used by 'qwebactionmanager.js' in order to trigger the download of + a report of any type. - :param data: The JSON.stringified report internal url + :param data: a javasscript array JSON.stringified containg report internal url ([0]) and + type [1] :returns: Response with a filetoken cookie and an attachment header """ requestcontent = simplejson.loads(data) From 62538c61e3ef3cce0d71506a894f0fb5bc813ef9 Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Mon, 24 Feb 2014 11:34:53 +0100 Subject: [PATCH 088/448] [IMP] Converted mrp BOM Structure to QWeb bzr revid: sle@openerp.com-20140224103453-0s60blgbk7e3ho5n --- addons/mrp/__openerp__.py | 1 + addons/mrp/mrp_report.xml | 13 +- addons/mrp/report/bom_structure.py | 29 ++-- addons/mrp/report/bom_structure.rml | 154 -------------------- addons/mrp/test/order_process.yml | 14 -- addons/mrp/views/report_mrpbomstructure.xml | 55 +++++++ 6 files changed, 83 insertions(+), 183 deletions(-) delete mode 100644 addons/mrp/report/bom_structure.rml create mode 100644 addons/mrp/views/report_mrpbomstructure.xml diff --git a/addons/mrp/__openerp__.py b/addons/mrp/__openerp__.py index e5bd8171c60..bc7dfc7ec29 100644 --- a/addons/mrp/__openerp__.py +++ b/addons/mrp/__openerp__.py @@ -74,6 +74,7 @@ Dashboard / Reports for MRP will include: 'res_config_view.xml', 'views/report_mrporder.xml', + 'views/report_mrpbomstructure.xml', ], 'demo': ['mrp_demo.xml'], #TODO: This yml tests are needed to be completely reviewed again because the product wood panel is removed in product demo as it does not suit for new demo context of computer and consultant company diff --git a/addons/mrp/mrp_report.xml b/addons/mrp/mrp_report.xml index 781615588a7..29c848acbe4 100644 --- a/addons/mrp/mrp_report.xml +++ b/addons/mrp/mrp_report.xml @@ -1,8 +1,14 @@ - - + + xsl="mrp/report/price.xsl" + /> diff --git a/addons/mrp/report/bom_structure.py b/addons/mrp/report/bom_structure.py index b153ea2e4e9..9af4e91b8ce 100644 --- a/addons/mrp/report/bom_structure.py +++ b/addons/mrp/report/bom_structure.py @@ -19,16 +19,24 @@ # ############################################################################## -import time -from openerp.report import report_sxw +from openerp.addons.web import http +from openerp.addons.web.http import request -class bom_structure(report_sxw.rml_parse): - def __init__(self, cr, uid, name, context): - super(bom_structure, self).__init__(cr, uid, name, context=context) - self.localcontext.update({ - 'time': time, - 'get_children':self.get_children, - }) + +class bom_structure(http.Controller): + + @http.route(['/report/mrp.report_mrpbomstructure/'], type='http', auth='user', website=True, multilang=True) + def report_mrpbomstructure(self, docids): + ids = [int(i) for i in docids.split(',')] + ids = list(set(ids)) + report_obj = request.registry['mrp.bom'] + docs = report_obj.browse(request.cr, request.uid, ids, context=request.context) + + docargs = { + 'docs': docs, + 'get_children': self.get_children, + } + return request.registry['report'].render(request.cr, request.uid, [], 'mrp.report_mrpbomstructure', docargs) def get_children(self, object, level=0): result = [] @@ -56,7 +64,4 @@ class bom_structure(report_sxw.rml_parse): return children -report_sxw.report_sxw('report.bom.structure','mrp.bom','mrp/report/bom_structure.rml',parser=bom_structure,header='internal') - - # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/mrp/report/bom_structure.rml b/addons/mrp/report/bom_structure.rml deleted file mode 100644 index cdc27c08d87..00000000000 --- a/addons/mrp/report/bom_structure.rml +++ /dev/null @@ -1,154 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - BOM Name - - - Product Name - - - Quantity - - - BOM Ref - - - - - - BOM Structure - - - - BOM Name - - - Product Name - - - Quantity - - - BOM Ref - - - -

- [[ repeatIn(objects, 'o') ]] - - - - [[ o.name ]] - - - [ [[ (o.product_id.default_code) or removeParentNode('font') ]] ] [[ o.product_id.name ]] - - - [[ o.product_qty ]] [[ o.product_uom.name ]] - - - [[ o.code ]] - - - -
- [[ repeatIn(get_children(o.bom_lines), 'l') ]] - - - - [[ '... '*(l['level']) ]] - [[ l['name'] ]] - - - [ [[ (l['pcode']) or removeParentNode('font') ]] ] [[ l['pname'] ]] - - - [[ l['pqty'] ]] [[ l['uname'] ]] - - - [[ l['code'] ]] - - - -
-
- - - \ No newline at end of file diff --git a/addons/mrp/test/order_process.yml b/addons/mrp/test/order_process.yml index 088cdf4311a..9fa13bbf33d 100644 --- a/addons/mrp/test/order_process.yml +++ b/addons/mrp/test/order_process.yml @@ -224,20 +224,6 @@ assert line.journal_id.id == wc.costs_journal_id.id, "Account Journal is not correspond." assert line.product_id.id == wc.product_id.id, "Product is not correspond." assert line.product_uom_id.id == wc.product_id.uom_id.id, "UOM is not correspond." -- - I print a "BOM Structure". -- - !context - uid: 'res_users_mrp_user' -- - !python {model: mrp.production}: | - import os - import openerp.report - from openerp import tools - order = self.browse(cr, uid, ref("mrp_production_test1")) - data, format = openerp.report.render_report(cr, uid, [order.bom_id.id], 'bom.structure', {}, {}) - if tools.config['test_report_directory']: - file(os.path.join(tools.config['test_report_directory'], 'mrp-bom_structure_report.'+format), 'wb+').write(data) - I print "Work Center Load Report". - diff --git a/addons/mrp/views/report_mrpbomstructure.xml b/addons/mrp/views/report_mrpbomstructure.xml new file mode 100644 index 00000000000..ae5469dbc17 --- /dev/null +++ b/addons/mrp/views/report_mrpbomstructure.xml @@ -0,0 +1,55 @@ + + + + + + From d1bbeb21a1dfb6001f68a7859801f3d961019c2a Mon Sep 17 00:00:00 2001 From: Fabien Meghazi Date: Mon, 24 Feb 2014 11:43:01 +0100 Subject: [PATCH 089/448] [FIX] website's translation problems on multiple view inheritance bzr revid: fme@openerp.com-20140224104301-n8dqg0lx4oijf66b --- openerp/addons/base/ir/ir_ui_view.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index aafce29b7eb..d4655bf8b9c 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -316,14 +316,14 @@ class view(osv.osv): return node return None - def inherit_branding(self, specs_tree, view_id, source_id): + def inherit_branding(self, specs_tree, view_id, root_id): for node in specs_tree.iterchildren(tag=etree.Element): xpath = node.getroottree().getpath(node) if node.tag == 'data' or node.tag == 'xpath': - self.inherit_branding(node, view_id, source_id) + self.inherit_branding(node, view_id, root_id) else: node.set('data-oe-id', str(view_id)) - node.set('data-oe-source-id', str(source_id)) + node.set('data-oe-source-id', str(root_id)) node.set('data-oe-xpath', xpath) node.set('data-oe-model', 'ir.ui.view') node.set('data-oe-field', 'arch') @@ -397,7 +397,7 @@ class view(osv.osv): return source - def apply_view_inheritance(self, cr, uid, source, source_id, model, context=None): + def apply_view_inheritance(self, cr, uid, source, source_id, model, root_id=None, context=None): """ Apply all the (directly and indirectly) inheriting views. :param source: a parent architecture to modify (with parent modifications already applied) @@ -408,13 +408,15 @@ class view(osv.osv): :return: a modified source where all the modifying architecture are applied """ if context is None: context = {} + if root_id is None: + root_id = source_id sql_inherit = self.pool.get('ir.ui.view').get_inheriting_views_arch(cr, uid, source_id, model, context=context) for (specs, view_id) in sql_inherit: specs_tree = etree.fromstring(specs.encode('utf-8')) if context.get('inherit_branding'): - self.inherit_branding(specs_tree, view_id, source_id) + self.inherit_branding(specs_tree, view_id, root_id) source = self.apply_inheritance_specs(cr, uid, source, specs_tree, view_id, context=context) - source = self.apply_view_inheritance(cr, uid, source, view_id, model, context=context) + source = self.apply_view_inheritance(cr, uid, source, view_id, model, root_id=root_id, context=context) return source def read_combined(self, cr, uid, view_id, fields=None, context=None): From ad86d1afdc168562637dfa19e309745f8b5b0dc2 Mon Sep 17 00:00:00 2001 From: Kersten Jeremy Date: Mon, 24 Feb 2014 11:47:45 +0100 Subject: [PATCH 090/448] [FIX] Remove unused field from crm for calendar. By consequence, bug with email attendee is removed. bzr revid: jke@openerp.com-20140224104745-egf6hutwzk9gcweh --- addons/crm/calendar_event.py | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/addons/crm/calendar_event.py b/addons/crm/calendar_event.py index df50d99b72b..4f57e364282 100644 --- a/addons/crm/calendar_event.py +++ b/addons/crm/calendar_event.py @@ -48,31 +48,11 @@ class calendar_attendee(osv.osv): _inherit = 'calendar.attendee' _description = 'Calendar Attendee' - def _compute_data(self, cr, uid, ids, name, arg, context=None): - """ - @param self: The object pointer - @param cr: the current row, from the database cursor, - @param uid: the current user’s ID for security checks, - @param ids: List of compute data’s IDs - @param context: A standard dictionary for contextual values - """ - name = name[0] - result = super(calendar_attendee, self)._compute_data(cr, uid, ids, name, arg, context=context) - - for attdata in self.browse(cr, uid, ids, context=context): - id = attdata.id - result[id] = {} - if name == 'categ_id': - if attdata.ref and 'categ_id' in attdata.ref._columns: - result[id][name] = (attdata.ref.categ_id.id, attdata.ref.categ_id.name,) - else: - result[id][name] = False - return result + def _noop(self, cr, uid, ids, name, arg, context=None): + return dict.fromkeys(ids,False) _columns = { - 'categ_id': fields.function(_compute_data, \ - string='Event Type', type="many2one", \ - relation="crm.case.categ", multi='categ_id'), + 'categ_id': fields.function(_noop, string='Event Type', deprecated="Unused Field - TODO : Remove it in trunk", type="many2one", relation="crm.case.categ"), } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From bfaa40266d9ea0dec15daf1c383bc4ade262bd28 Mon Sep 17 00:00:00 2001 From: Kersten Jeremy Date: Mon, 24 Feb 2014 11:48:50 +0100 Subject: [PATCH 091/448] [IMP] Calendar : Check that user are not disconnected before to check next_potential_notif, that avoid to see an error in console every 5 minutes bzr revid: jke@openerp.com-20140224104850-wty7kk230s72tdcy --- addons/calendar/calendar.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addons/calendar/calendar.py b/addons/calendar/calendar.py index ca4d00eb85e..96c1e2b68e9 100644 --- a/addons/calendar/calendar.py +++ b/addons/calendar/calendar.py @@ -483,8 +483,12 @@ class calendar_alarm_manager(osv.AbstractModel): def get_next_notif(self,cr,uid,context=None): ajax_check_every_seconds = 300 - partner = self.pool.get('res.users').browse(cr,uid,uid,context=context).partner_id; + partner = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id all_notif = [] + + if not partner or not partner.id: # If user is disconnected + return [] + all_events = self.get_next_potential_limit_alarm(cr,uid,ajax_check_every_seconds,partner_id=partner.id,mail=False,context=context) for event in all_events: # .values() From f94f50c7e86a444508b096c8e9693c8d22a7abb4 Mon Sep 17 00:00:00 2001 From: Gery Debongnie Date: Mon, 24 Feb 2014 12:05:54 +0100 Subject: [PATCH 092/448] [FIX] fixes a crash caused by incorrect use of date/datetime fields of the form 'date:interval': the list view in some case tried to format the strings as a date, and it could not parse it. lp bug: https://launchpad.net/bugs/1279382 fixed bzr revid: ged@openerp.com-20140224110554-y007810prx7qg5yi --- addons/web/static/src/js/data.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index cc595c30688..24a4dc3dd2a 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -221,11 +221,12 @@ instance.web.QueryGroup = instance.web.Class.extend({ {__context: {group_by: []}, __domain: []}, read_group_group); + var raw_field = grouping_field && grouping_field.split(':')[0]; var aggregates = {}; _(fixed_group).each(function (value, key) { if (key.indexOf('__') === 0 - || key === grouping_field - || key === grouping_field + '_count') { + || key === raw_field + || key === raw_field + '_count') { return; } aggregates[key] = value || 0; @@ -234,7 +235,6 @@ instance.web.QueryGroup = instance.web.Class.extend({ this.model = new instance.web.Model( model, fixed_group.__context, fixed_group.__domain); - var raw_field = grouping_field && grouping_field.split(':')[0]; var group_size = fixed_group[raw_field + '_count'] || fixed_group.__count || 0; var leaf_group = fixed_group.__context.group_by.length === 0; From 5557a1152c7fb41771ad52e0a013790d5be62f70 Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Mon, 24 Feb 2014 13:07:49 +0100 Subject: [PATCH 093/448] [IMP] Report controller: little changes to get the barcode route working with reportlab 2.5 bzr revid: sle@openerp.com-20140224120749-nlg3n95vgghxv63y --- addons/report/controllers/main.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/addons/report/controllers/main.py b/addons/report/controllers/main.py index 1390f401bcc..99a672a1dd8 100644 --- a/addons/report/controllers/main.py +++ b/addons/report/controllers/main.py @@ -41,7 +41,7 @@ from werkzeug import exceptions from werkzeug.test import Client from werkzeug.wrappers import BaseResponse from werkzeug.datastructures import Headers -from reportlab.graphics.barcode import createBarcodeImageInMemory +from reportlab.graphics.barcode import createBarcodeDrawing _logger = logging.getLogger(__name__) @@ -462,13 +462,12 @@ class Report(http.Controller): """ try: width, height = int(width), int(height) - barcode = createBarcodeImageInMemory( + barcode = createBarcodeDrawing( type, value=value, format='png', width=width, height=height ) + barcode = barcode.asString('png') except (ValueError, AttributeError): raise exceptions.HTTPException(description='Cannot convert into barcode.') - except AssertionError: - raise exceptions.HTTPException(description='Please upgrade reportlab to at least 3.0.') return request.make_response(barcode, headers=[('Content-Type', 'image/png')]) From bff00ec6df967f95b41493afb5c81aef1944e758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 24 Feb 2014 13:10:04 +0100 Subject: [PATCH 094/448] [FIX] res_partner: avoid duplicating users when duplicating partners, because I think your database wil hate you. bzr revid: tde@openerp.com-20140224121004-lgy2rsszsomy7mji --- openerp/addons/base/res/res_partner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index e9573088680..46f0218e21f 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -350,6 +350,7 @@ class res_partner(osv.osv, format_address): def copy(self, cr, uid, id, default=None, context=None): if default is None: default = {} + default['user_ids'] = False name = self.read(cr, uid, [id], ['name'], context)[0]['name'] default.update({'name': _('%s (copy)') % name}) return super(res_partner, self).copy(cr, uid, id, default, context) From 901172a194d0acb158ef0accbfaeafc5e011558c Mon Sep 17 00:00:00 2001 From: Simon Lejeune Date: Mon, 24 Feb 2014 14:47:32 +0100 Subject: [PATCH 095/448] [FIX] Workaround to get printing done with 1 worker bzr revid: sle@openerp.com-20140224134732-imptqwtzyoyml2qq --- addons/report/controllers/main.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/addons/report/controllers/main.py b/addons/report/controllers/main.py index 99a672a1dd8..ecdf5b09647 100644 --- a/addons/report/controllers/main.py +++ b/addons/report/controllers/main.py @@ -23,6 +23,7 @@ from openerp.osv.osv import except_osv from openerp.addons.web import http from openerp.tools.translate import _ from openerp.addons.web.http import request +import openerp.tools.config as config import time import base64 @@ -35,6 +36,10 @@ try: import cStringIO as StringIO except ImportError: import StringIO +import psutil +import signal +import os + from pyPdf import PdfFileWriter, PdfFileReader from werkzeug import exceptions @@ -322,6 +327,12 @@ class Report(http.Controller): content_file.seek(0) try: + # If the server is running with only one worker, increase it to two to be able + # to serve the http request from wkhtmltopdf. + if config['workers'] == 1: + ppid = psutil.Process(os.getpid()).ppid + os.kill(ppid, signal.SIGTTIN) + wkhtmltopdf = command + command_args + command_arg_local wkhtmltopdf += [content_file.name] + [pdfreport.name] @@ -329,6 +340,9 @@ class Report(http.Controller): stderr=subprocess.PIPE) out, err = process.communicate() + if config['workers'] == 1: + os.kill(ppid, signal.SIGTTOU) + if process.returncode != 0: raise except_osv(_('Report (PDF)'), _('wkhtmltopdf-0.12 failed with error code = %s. ' From 60c75d38bfc948ed723cde3a00b1c3871ff89148 Mon Sep 17 00:00:00 2001 From: Christophe Matthieu Date: Mon, 24 Feb 2014 15:27:51 +0100 Subject: [PATCH 096/448] [FIX] website placeholder; website_hr_recruitment required for ie bzr revid: chm@openerp.com-20140224142751-7bolxomy2nc717w0 --- addons/website/static/src/js/website.js | 3 +- .../controllers/main.py | 31 ++++++++++++++----- .../views/templates.xml | 15 ++++----- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index 3be70b9b404..54ee4689814 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -69,7 +69,8 @@ website.is_editable = website.is_editable || $('html').data('editable'); website.is_editable_button= website.is_editable_button || $('html').data('editable'); dom_ready.resolve(); - $('input, textarea').placeholder(); + // fix for ie + if($.fn.placeholder) $('input, textarea').placeholder(); }); website.init_kanban = function ($kanban) { diff --git a/addons/website_hr_recruitment/controllers/main.py b/addons/website_hr_recruitment/controllers/main.py index c50bc6a0428..a3cbbe3cbc4 100644 --- a/addons/website_hr_recruitment/controllers/main.py +++ b/addons/website_hr_recruitment/controllers/main.py @@ -7,7 +7,6 @@ from openerp.tools.translate import _ from openerp.addons.web.http import request class website_hr_recruitment(http.Controller): - @http.route([ '/jobs', '/jobs/department/', @@ -61,27 +60,45 @@ class website_hr_recruitment(http.Controller): @http.route(['/jobs/apply/'], type='http', auth="public", website=True, multilang=True) def jobs_apply(self, job): - return request.website.render("website_hr_recruitment.apply", { 'job': job }) + error = {} + default = {} + if 'website_hr_recruitment_error' in request.session: + error = request.session.pop('website_hr_recruitment_error') + default = request.session.pop('website_hr_recruitment_default') + return request.website.render("website_hr_recruitment.apply", { 'job': job, 'error': error, 'default': default}) @http.route(['/jobs/thankyou'], methods=['POST'], type='http', auth="public", website=True, multilang=True) def jobs_thankyou(self, **post): cr, uid, context = request.cr, request.uid, request.context imd = request.registry['ir.model.data'] + + error = {} + for field_name in ["partner_name", "phone", "email_from"]: + if not post.get(field_name): + error[field_name] = 'missing' + if error: + request.session['website_hr_recruitment_error'] = error + ufile = post.pop('ufile') + if ufile: + error['ufile'] = 'reset' + request.session['website_hr_recruitment_default'] = post + return request.redirect('/jobs/apply/%s' % post.get("job_id")) + value = { - 'name': _('Online Form'), - 'user_id': False, 'source_id' : imd.xmlid_to_res_id(cr, SUPERUSER_ID, 'hr_recruitment.source_website_company'), } - for f in ['phone', 'email_from', 'partner_name', 'description', 'department_id', 'job_id']: + for f in ['phone', 'email_from', 'partner_name', 'description']: value[f] = post.get(f) + for f in ['department_id', 'job_id']: + value[f] = int(post.get(f) or 0) - job_id = request.registry['hr.applicant'].create(cr, SUPERUSER_ID, value, context=context) + applicant_id = request.registry['hr.applicant'].create(cr, SUPERUSER_ID, value, context=context) if post['ufile']: attachment_value = { 'name': post['ufile'].filename, 'res_name': value['partner_name'], 'res_model': 'hr.applicant', - 'res_id': job_id, + 'res_id': applicant_id, 'datas': base64.encodestring(post['ufile'].read()), 'datas_fname': post['ufile'].filename, } diff --git a/addons/website_hr_recruitment/views/templates.xml b/addons/website_hr_recruitment/views/templates.xml index 3e057d249f2..9dab6c6accc 100644 --- a/addons/website_hr_recruitment/views/templates.xml +++ b/addons/website_hr_recruitment/views/templates.xml @@ -161,34 +161,35 @@
-
+
- +
-
+
- +
-
+
- +
-
'); + else if (popupName === "pastetext") { + $popup.html('Paste your content here and click submit.

'); popupTypeClass = PROMPT_CLASS; } @@ -720,12 +734,12 @@ } // Execute the command and check for error - var success = true, description; - if (ie && command.toLowerCase() == "inserthtml") + var success = true, message; + if (ie && command.toLowerCase() === "inserthtml") getRange(editor).pasteHTML(value); else { try { success = editor.doc.execCommand(command, 0, value || null); } - catch (err) { description = err.description; success = false; } + catch (err) { message = err.message; success = false; } if (!success) { if ("cutcopypaste".indexOf(command) > -1) showMessage(editor, "For security reasons, your browser does not support the " + @@ -733,13 +747,14 @@ button); else showMessage(editor, - (description ? description : "Error executing the " + command + " command."), + (message ? message : "Error executing the " + command + " command."), button); } } - // Enable the buttons + // Enable the buttons and update the textarea refreshButtons(editor); + updateTextArea(editor, true); return success; } @@ -765,19 +780,26 @@ return editor.$frame[0].contentWindow.getSelection(); } - // Returns the hex value for the passed in string. - // hex("rgb(255, 0, 0)"); // #FF0000 - // hex("#FF0000"); // #FF0000 - // hex("#F00"); // #FF0000 + // hex - returns the hex value for the passed in color string function hex(s) { - var m = /rgba?\((\d+), (\d+), (\d+)/.exec(s), - c = s.split(""); + + // hex("rgb(255, 0, 0)") returns #FF0000 + var m = /rgba?\((\d+), (\d+), (\d+)/.exec(s); if (m) { - s = ( m[1] << 16 | m[2] << 8 | m[3] ).toString(16); + s = (m[1] << 16 | m[2] << 8 | m[3]).toString(16); while (s.length < 6) s = "0" + s; + return "#" + s; } - return "#" + (s.length == 6 ? s : c[1] + c[1] + c[2] + c[2] + c[3] + c[3]); + + // hex("#F00") returns #FF0000 + var c = s.split(""); + if (s.length === 4) + return "#" + c[1] + c[1] + c[2] + c[2] + c[3] + c[3]; + + // hex("#FF0000") returns #FF0000 + return s; + } // hidePopups - hides all popups @@ -792,9 +814,8 @@ // imagesPath - returns the path to the images folder function imagesPath() { - var cssFile = "jquery.cleditor.css", - href = $("link[href$='" + cssFile +"']").attr("href"); - return href.substr(0, href.length - cssFile.length) + "images/"; + var href = $("link[href*=cleditor]").attr("href"); + return href.replace(/^(.*\/)[^\/]+$/, '$1') + "images/"; } // imageUrl - Returns the css url string for a filemane @@ -813,7 +834,7 @@ editor.$frame.remove(); // Create a new iframe - var $frame = editor.$frame = $('