diff --git a/addons/account/account.py b/addons/account/account.py index 6e96ce73962..caf1d68c45d 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -24,11 +24,12 @@ from datetime import datetime from dateutil.relativedelta import relativedelta from operator import itemgetter -import netsvc +import logging import pooler from osv import fields, osv import decimal_precision as dp from tools.translate import _ +_logger = logging.getLogger(__name__) def check_cycle(self, cr, uid, ids, context=None): """ climbs the ``self._table.parent_id`` chains for 100 levels or @@ -212,7 +213,6 @@ class account_account(osv.osv): _name = "account.account" _description = "Account" _parent_store = True - logger = netsvc.Logger() def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False): @@ -295,8 +295,7 @@ class account_account(osv.osv): if aml_query.strip(): wheres.append(aml_query.strip()) filters = " AND ".join(wheres) - self.logger.notifyChannel('addons.'+self._name, netsvc.LOG_DEBUG, - 'Filters: %s'%filters) + _logger.debug('Filters: %s',(filters)) # IN might not work ideally in case there are too many # children_and_consolidated, in that case join on a # values() e.g.: @@ -312,8 +311,7 @@ class account_account(osv.osv): " GROUP BY l.account_id") params = (tuple(children_and_consolidated),) + query_params cr.execute(request, params) - self.logger.notifyChannel('addons.'+self._name, netsvc.LOG_DEBUG, - 'Status: %s'%cr.statusmessage) + _logger.debug('Status: %s',(cr.statusmessage)) for res in cr.dictfetchall(): accounts[res['id']] = res @@ -2095,9 +2093,7 @@ class account_tax(osv.osv): } def compute(self, cr, uid, taxes, price_unit, quantity, product=None, partner=None): - logger = netsvc.Logger() - logger.notifyChannel("warning", netsvc.LOG_WARNING, - "Deprecated, use compute_all(...)['taxes'] instead of compute(...) to manage prices with tax included") + _logger.warning("Deprecated, use compute_all(...)['taxes'] instead of compute(...) to manage prices with tax included") return self._compute(cr, uid, taxes, price_unit, quantity, product, partner) def _compute(self, cr, uid, taxes, price_unit, quantity, product=None, partner=None): diff --git a/addons/account/account_invoice_view.xml b/addons/account/account_invoice_view.xml index 250e2cd0df6..a5be9239ed3 100644 --- a/addons/account/account_invoice_view.xml +++ b/addons/account/account_invoice_view.xml @@ -39,7 +39,7 @@ - + @@ -61,8 +61,8 @@ on_change="uos_id_change(product_id, uos_id, quantity, name, parent.type, parent.partner_id, parent.fiscal_position, price_unit, parent.currency_id, context, parent.company_id)"/> - - + + @@ -98,7 +98,7 @@ - + @@ -190,7 +190,7 @@ - + @@ -274,11 +274,12 @@
- - + account.invoice.customer.pay + account.invoice + form + + + + + +
diff --git a/addons/crm_claim/crm_claim_menu.xml b/addons/crm_claim/crm_claim_menu.xml index 75bfd9ce211..d4bfd8f21bb 100644 --- a/addons/crm_claim/crm_claim_menu.xml +++ b/addons/crm_claim/crm_claim_menu.xml @@ -51,9 +51,13 @@ parent="base.menu_aftersale" action="crm_case_categ_claim0" sequence="1"/> - - - + + + + + diff --git a/addons/document/content_index.py b/addons/document/content_index.py index 64b480ee736..f24c033a7ec 100644 --- a/addons/document/content_index.py +++ b/addons/document/content_index.py @@ -22,7 +22,7 @@ import logging import os import tempfile from subprocess import Popen, PIPE - +_logger = logging.getLogger(__name__) class NhException(Exception): pass @@ -116,7 +116,7 @@ def mime_match(mime, mdict): return (None, None) class contentIndex(object): - __logger = logging.getLogger('addons.document.content_index') + def __init__(self): self.mimes = {} self.exts = {} @@ -132,7 +132,7 @@ class contentIndex(object): f = True if f: - self.__logger.debug('Register content indexer: %r', obj) + _logger.debug('Register content indexer: %r', obj) if not f: raise Exception("Your indexer should at least suport a mimetype or extension") @@ -169,22 +169,22 @@ class contentIndex(object): (result, _) = pop.communicate() mime2 = result.split(';')[0] - self.__logger.debug('File gave us: %s', mime2) + _logger.debug('File gave us: %s', mime2) # Note that the temporary file still exists now. mime,fobj = mime_match(mime2, self.mimes) if not mime: mime = mime2 except Exception: - self.__logger.exception('Cannot determine mime type') + _logger.exception('Cannot determine mime type') try: if fobj: res = (mime, fobj.indexContent(content,filename,fname or realfname) ) else: - self.__logger.debug("Have no object, return (%s, None)", mime) + _logger.debug("Have no object, return (%s, None)", mime) res = (mime, None ) except Exception: - self.__logger.exception("Could not index file %s (%s)", + _logger.exception("Could not index file %s (%s)", filename, fname or realfname) res = None @@ -193,8 +193,7 @@ class contentIndex(object): try: os.unlink(fname) except Exception: - self.__logger.exception("Could not unlink %s", fname) - + _logger.exception("Could not unlink %s", fname) return res cntIndex = contentIndex() diff --git a/addons/document/document.py b/addons/document/document.py index 92ece063a47..fa65f8d9966 100644 --- a/addons/document/document.py +++ b/addons/document/document.py @@ -30,12 +30,15 @@ from tools.translate import _ import nodes import logging +_logger = logging.getLogger(__name__) + DMS_ROOT_PATH = tools.config.get('document_path', os.path.join(tools.config['root_path'], 'filestore')) class document_file(osv.osv): _inherit = 'ir.attachment' _rec_name = 'datas_fname' - + + def _attach_parent_id(self, cr, uid, ids=None, context=None): """Migrate ir.attachments to the document module. @@ -54,7 +57,7 @@ class document_file(osv.osv): parent_id = self.pool.get('document.directory')._get_root_directory(cr,uid) if not parent_id: - logging.getLogger('document').warning("at _attach_parent_id(), still not able to set the parent!") + _logger.warning("at _attach_parent_id(), still not able to set the parent!") return False if ids is not None: @@ -140,8 +143,8 @@ class document_file(osv.osv): _defaults = { 'user_id': lambda self, cr, uid, ctx:uid, + 'parent_id': __get_def_directory, 'file_size': lambda self, cr, uid, ctx:0, - 'parent_id': __get_def_directory } _sql_constraints = [ # filename_uniq is not possible in pure SQL @@ -336,7 +339,7 @@ class document_file(osv.osv): if r: unres.append(r) else: - logging.getLogger('document').warning("Unlinking attachment #%s %s that has no storage", + self.loggerdoc.warning("Unlinking attachment #%s %s that has no storage", f.id, f.name) res = super(document_file, self).unlink(cr, uid, ids, context) stor.do_unlink(cr, uid, unres) diff --git a/addons/document/document_data.xml b/addons/document/document_data.xml index ac7a174b05f..a7bc98de6f3 100644 --- a/addons/document/document_data.xml +++ b/addons/document/document_data.xml @@ -104,6 +104,5 @@ - diff --git a/addons/document/document_directory.py b/addons/document/document_directory.py index d09d2cc86d5..03b2dcc47bf 100644 --- a/addons/document/document_directory.py +++ b/addons/document/document_directory.py @@ -22,10 +22,10 @@ from osv import osv, fields from osv.orm import except_orm - +import logging import nodes from tools.translate import _ - +_logger = logging.getLogger(__name__) class document_directory(osv.osv): _name = 'document.directory' _description = 'Directory' @@ -78,9 +78,8 @@ class document_directory(osv.osv): root_id = objid.read(cr, uid, mid, ['res_id'])['res_id'] return root_id except Exception, e: - import netsvc - logger = netsvc.Logger() - logger.notifyChannel("document", netsvc.LOG_WARNING, 'Cannot set directory root:'+ str(e)) + + _logger.warning('Cannot set directory root:'+ str(e)) return False return objid.browse(cr, uid, mid, context=context).res_id diff --git a/addons/document/document_storage.py b/addons/document/document_storage.py index fe757e7876d..339b320e9a7 100644 --- a/addons/document/document_storage.py +++ b/addons/document/document_storage.py @@ -29,18 +29,15 @@ import logging import shutil from StringIO import StringIO import psycopg2 - from tools.misc import ustr from tools.translate import _ - from osv.orm import except_orm - import random import string import pooler import nodes from content_index import cntIndex - +_logger = logging.getLogger(__name__) DMS_ROOT_PATH = tools.config.get('document_path', os.path.join(tools.config.get('root_path'), 'filestore')) @@ -130,7 +127,7 @@ class nodefd_file(nodes.node_descriptor): mime, icont = cntIndex.doIndex(None, filename=filename, content_type=None, realfname=fname) except Exception: - logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True) + _logger.debug('Cannot index file:', exc_info=True) pass try: @@ -150,7 +147,7 @@ class nodefd_file(nodes.node_descriptor): cr.commit() cr.close() except Exception: - logging.getLogger('document.storage').warning('Cannot save file indexed content:', exc_info=True) + _logger.warning('Cannot save file indexed content:', exc_info=True) elif self.mode in ('a', 'a+' ): try: @@ -164,7 +161,7 @@ class nodefd_file(nodes.node_descriptor): cr.commit() cr.close() except Exception: - logging.getLogger('document.storage').warning('Cannot save file appended content:', exc_info=True) + _logger.warning('Cannot save file appended content:', exc_info=True) @@ -191,7 +188,7 @@ class nodefd_db(StringIO, nodes.node_descriptor): elif mode == 'a': StringIO.__init__(self, None) else: - logging.getLogger('document.storage').error("Incorrect mode %s specified", mode) + _logger.error("Incorrect mode %s specified", mode) raise IOError(errno.EINVAL, "Invalid file mode") self.mode = mode @@ -217,7 +214,7 @@ class nodefd_db(StringIO, nodes.node_descriptor): mime, icont = cntIndex.doIndex(data, filename=filename, content_type=None, realfname=None) except Exception: - logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True) + _logger.debug('Cannot index file:', exc_info=True) pass try: @@ -241,7 +238,7 @@ class nodefd_db(StringIO, nodes.node_descriptor): (out, len(data), par.file_id)) cr.commit() except Exception: - logging.getLogger('document.storage').exception('Cannot update db file #%d for close:', par.file_id) + _logger.exception('Cannot update db file #%d for close:', par.file_id) raise finally: cr.close() @@ -271,7 +268,7 @@ class nodefd_db64(StringIO, nodes.node_descriptor): elif mode == 'a': StringIO.__init__(self, None) else: - logging.getLogger('document.storage').error("Incorrect mode %s specified", mode) + _logger.error("Incorrect mode %s specified", mode) raise IOError(errno.EINVAL, "Invalid file mode") self.mode = mode @@ -297,7 +294,7 @@ class nodefd_db64(StringIO, nodes.node_descriptor): mime, icont = cntIndex.doIndex(data, filename=filename, content_type=None, realfname=None) except Exception: - logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True) + self.logger.debug('Cannot index file:', exc_info=True) pass try: @@ -320,7 +317,7 @@ class nodefd_db64(StringIO, nodes.node_descriptor): (base64.encodestring(data), len(data), par.file_id)) cr.commit() except Exception: - logging.getLogger('document.storage').exception('Cannot update db file #%d for close:', par.file_id) + _logger.exception('Cannot update db file #%d for close:', par.file_id) raise finally: cr.close() @@ -339,7 +336,6 @@ class document_storage(osv.osv): """ _name = 'document.storage' _description = 'Storage Media' - _doclog = logging.getLogger('document') _columns = { 'name': fields.char('Name', size=64, required=True, select=1), @@ -402,7 +398,7 @@ class document_storage(osv.osv): npath = filter(lambda x: x is not None, npath) # if self._debug: - # self._doclog.debug('Npath: %s', npath) + # self._logger.debug('Npath: %s', npath) for n in npath: if n == '..': raise ValueError("Invalid '..' element in path") @@ -413,7 +409,7 @@ class document_storage(osv.osv): dpath += npath[:-1] path = os.path.join(*dpath) if not os.path.isdir(path): - self._doclog.debug("Create dirs: %s", path) + _logger.debug("Create dirs: %s", path) os.makedirs(path) return path, npath @@ -451,7 +447,7 @@ class document_storage(osv.osv): # try to fix their directory. if mode in ('r','r+'): if ira.file_size: - self._doclog.warning( "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id) + _logger.warning( "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id) raise IOError(errno.ENOENT, 'No file can be located') else: store_fname = self.__get_random_fname(boo.path) @@ -493,7 +489,7 @@ class document_storage(osv.osv): # On a migrated db, some files may have the wrong storage type # try to fix their directory. if ira.file_size: - self._doclog.warning( "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id) + _logger.warning( "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id) return None fpath = os.path.join(boo.path, ira.store_fname) return file(fpath, 'rb').read() @@ -517,7 +513,7 @@ class document_storage(osv.osv): # On a migrated db, some files may have the wrong storage type # try to fix their directory. if ira.file_size: - self._doclog.warning("ir.attachment #%d does not have a filename, trying the name." %ira.id) + _logger.warning("ir.attachment #%d does not have a filename, trying the name." %ira.id) # sfname = ira.name fpath = os.path.join(boo.path,ira.store_fname or ira.name) if os.path.exists(fpath): @@ -550,7 +546,7 @@ class document_storage(osv.osv): if boo.readonly: raise IOError(errno.EPERM, "Readonly medium") - self._doclog.debug( "Store data for ir.attachment #%d" % ira.id) + _logger.debug( "Store data for ir.attachment #%d" % ira.id) store_fname = None fname = None if boo.type == 'filestore': @@ -563,13 +559,13 @@ class document_storage(osv.osv): fp.write(data) finally: fp.close() - self._doclog.debug( "Saved data to %s" % fname) + _logger.debug( "Saved data to %s" % fname) filesize = len(data) # os.stat(fname).st_size # TODO Here, an old file would be left hanging. except Exception, e: - self._doclog.warning( "Couldn't save data to %s", path, exc_info=True) + _logger.warning( "Couldn't save data to %s", path, exc_info=True) raise except_orm(_('Error!'), str(e)) elif boo.type == 'db': filesize = len(data) @@ -592,12 +588,12 @@ class document_storage(osv.osv): fp.write(data) finally: fp.close() - self._doclog.debug("Saved data to %s", fname) + _logger.debug("Saved data to %s", fname) filesize = len(data) # os.stat(fname).st_size store_fname = os.path.join(*npath) # TODO Here, an old file would be left hanging. except Exception,e : - self._doclog.warning("Couldn't save data:", exc_info=True) + _logger.warning("Couldn't save data:", exc_info=True) raise except_orm(_('Error!'), str(e)) elif boo.type == 'virtual': @@ -616,7 +612,7 @@ class document_storage(osv.osv): mime, icont = cntIndex.doIndex(data, ira.datas_fname, ira.file_type or None, fname) except Exception: - self._doclog.debug('Cannot index file:', exc_info=True) + _logger.debug('Cannot index file:', exc_info=True) pass try: @@ -633,7 +629,7 @@ class document_storage(osv.osv): file_node.content_type = mime return True except Exception, e : - self._doclog.warning("Couldn't save data:", exc_info=True) + self._logger.warning("Couldn't save data:", exc_info=True) # should we really rollback once we have written the actual data? # at the db case (only), that rollback would be safe raise except_orm(_('Error at doc write!'), str(e)) @@ -671,9 +667,9 @@ class document_storage(osv.osv): try: os.unlink(fname) except Exception: - self._doclog.warning("Could not remove file %s, please remove manually.", fname, exc_info=True) + _logger.warning("Could not remove file %s, please remove manually.", fname, exc_info=True) else: - self._doclog.warning("Unknown unlink key %s" % ktype) + _logger.warning("Unknown unlink key %s" % ktype) return True @@ -703,9 +699,9 @@ class document_storage(osv.osv): fname = ira.store_fname if not fname: - self._doclog.warning("Trying to rename a non-stored file") + _logger.warning("Trying to rename a non-stored file") if fname != os.path.join(*npath): - self._doclog.warning("inconsistency in realstore: %s != %s" , fname, repr(npath)) + _logger.warning("inconsistency in realstore: %s != %s" , fname, repr(npath)) oldpath = os.path.join(path, npath[-1]) newpath = os.path.join(path, new_name) @@ -743,7 +739,7 @@ class document_storage(osv.osv): break par = par.parent_id if file_node.storage_id != psto: - self._doclog.debug('Cannot move file %r from %r to %r', file_node, file_node.parent, ndir_bro.name) + _logger.debug('Cannot move file %r from %r to %r', file_node, file_node.parent, ndir_bro.name) raise NotImplementedError('Cannot move files between storage media') if sbro.type in ('filestore', 'db', 'db64'): @@ -756,9 +752,9 @@ class document_storage(osv.osv): fname = ira.store_fname if not fname: - self._doclog.warning("Trying to rename a non-stored file") + _logger.warning("Trying to rename a non-stored file") if fname != os.path.join(*opath): - self._doclog.warning("inconsistency in realstore: %s != %s" , fname, repr(opath)) + _logger.warning("inconsistency in realstore: %s != %s" , fname, repr(opath)) oldpath = os.path.join(path, opath[-1]) @@ -766,12 +762,12 @@ class document_storage(osv.osv): npath = filter(lambda x: x is not None, npath) newdir = os.path.join(*npath) if not os.path.isdir(newdir): - self._doclog.debug("Must create dir %s", newdir) + _logger.debug("Must create dir %s", newdir) os.makedirs(newdir) npath.append(opath[-1]) newpath = os.path.join(*npath) - self._doclog.debug("Going to move %s from %s to %s", opath[-1], oldpath, newpath) + _logger.debug("Going to move %s from %s to %s", opath[-1], oldpath, newpath) shutil.move(oldpath, newpath) store_path = npath[1:] + [opath[-1],] diff --git a/addons/document/nodes.py b/addons/document/nodes.py index ee89f3d4c6c..273be7effa4 100644 --- a/addons/document/nodes.py +++ b/addons/document/nodes.py @@ -41,8 +41,7 @@ from StringIO import StringIO # file: objct = ir.attachement # root: if we are at the first directory of a ressource # - -logger = logging.getLogger('doc2.nodes') +_logger = logging.getLogger(__name__) def _str2time(cre): """ Convert a string with time representation (from db) into time (float) @@ -328,7 +327,7 @@ class node_class(object): if self.DAV_M_NS.has_key(ns): prefix = self.DAV_M_NS[ns] else: - logger.debug('No namespace: %s ("%s")',ns, prop) + _logger.debug('No namespace: %s ("%s")',ns, prop) return None mname = prefix + "_" + prop.replace('-','_') @@ -341,7 +340,7 @@ class node_class(object): r = m(cr) return r except AttributeError: - logger.debug('Property %s not supported' % prop, exc_info=True) + _logger.debug('Property %s not supported' % prop, exc_info=True) return None def get_dav_resourcetype(self, cr): @@ -384,13 +383,13 @@ class node_class(object): def create_child(self, cr, path, data=None): """ Create a regular file under this node """ - logger.warning("Attempted to create a file under %r, not possible.", self) + _logger.warning("Attempted to create a file under %r, not possible.", self) raise IOError(errno.EPERM, "Not allowed to create files here") def create_child_collection(self, cr, objname): """ Create a child collection (directory) under self """ - logger.warning("Attempted to create a collection under %r, not possible.", self) + _logger.warning("Attempted to create a collection under %r, not possible.", self) raise IOError(errno.EPERM, "Not allowed to create folders here") def rm(self, cr): @@ -725,7 +724,7 @@ class node_dir(node_database): assert self.parent if self.parent != ndir_node: - logger.debug('Cannot move dir %r from %r to %r', self, self.parent, ndir_node) + _logger.debug('Cannot move dir %r from %r to %r', self, self.parent, ndir_node) raise NotImplementedError('Cannot move dir to another dir') ret = {} @@ -998,7 +997,7 @@ class node_res_obj(node_class): def get_dav_eprop_DEPR(self, cr, ns, prop): # Deprecated! if ns != 'http://groupdav.org/' or prop != 'resourcetype': - logger.warning("Who asked for %s:%s?" % (ns, prop)) + _logger.warning("Who asked for %s:%s?" % (ns, prop)) return None cntobj = self.context._dirobj.pool.get('document.directory.content') uid = self.context.uid @@ -1328,7 +1327,7 @@ class node_file(node_class): ret = {} if ndir_node and self.parent != ndir_node: if not (isinstance(self.parent, node_dir) and isinstance(ndir_node, node_dir)): - logger.debug('Cannot move file %r from %r to %r', self, self.parent, ndir_node) + _logger.debug('Cannot move file %r from %r to %r', self, self.parent, ndir_node) raise NotImplementedError('Cannot move files between dynamic folders') if not ndir_obj: @@ -1452,6 +1451,7 @@ class node_content(node_class): return '' class nodefd_content(StringIO, node_descriptor): + """ A descriptor to content nodes """ def __init__(self, parent, cr, mode, ctx): @@ -1473,7 +1473,7 @@ class nodefd_content(StringIO, node_descriptor): elif mode == 'a': StringIO.__init__(self, None) else: - logging.getLogger('document.content').error("Incorrect mode %s specified", mode) + _logger.error("Incorrect mode %s specified", mode) raise IOError(errno.EINVAL, "Invalid file mode") self.mode = mode @@ -1499,13 +1499,14 @@ class nodefd_content(StringIO, node_descriptor): raise NotImplementedError cr.commit() except Exception: - logging.getLogger('document.content').exception('Cannot update db content #%d for close:', par.cnt_id) + _logger.exception('Cannot update db content #%d for close:', par.cnt_id) raise finally: cr.close() StringIO.close(self) class nodefd_static(StringIO, node_descriptor): + """ A descriptor to nodes with static data. """ def __init__(self, parent, cr, mode, ctx=None): @@ -1526,7 +1527,7 @@ class nodefd_static(StringIO, node_descriptor): elif mode == 'a': StringIO.__init__(self, None) else: - logging.getLogger('document.nodes').error("Incorrect mode %s specified", mode) + _logger.error("Incorrect mode %s specified", mode) raise IOError(errno.EINVAL, "Invalid file mode") self.mode = mode @@ -1551,7 +1552,7 @@ class nodefd_static(StringIO, node_descriptor): raise NotImplementedError cr.commit() except Exception: - logging.getLogger('document.nodes').exception('Cannot update db content #%d for close:', par.cnt_id) + _logger.exception('Cannot update db content #%d for close:', par.cnt_id) raise finally: cr.close() diff --git a/addons/document/std_index.py b/addons/document/std_index.py index e153d018bf0..49ac9a5c906 100644 --- a/addons/document/std_index.py +++ b/addons/document/std_index.py @@ -25,6 +25,7 @@ import StringIO import odt2txt import sys, zipfile, xml.dom.minidom import logging +_logger = logging.getLogger(__name__) def _to_unicode(s): try: @@ -101,9 +102,9 @@ class DocIndex(indexer): (data, _) = pop.communicate() return _to_unicode(data) except OSError: - logger = logging.getLogger('document.DocIndex') - logger.warn("Failed attempt to execute antiword (MS Word reader). Antiword is necessary to index the file %s of MIME type %s. Detailed error available at DEBUG level.", fname, self._getMimeTypes()[0]) - logger.debug("Trace of the failed file indexing attempt: ", exc_info=True) + + _logger.warn("Failed attempt to execute antiword (MS Word reader). Antiword is necessary to index the file %s of MIME type %s. Detailed error available at DEBUG level.", fname, self._getMimeTypes()[0]) + _logger.debug("Trace of the failed file indexing attempt: ", exc_info=True) return False cntIndex.register(DocIndex()) diff --git a/addons/document_ftp/ftpserver/__init__.py b/addons/document_ftp/ftpserver/__init__.py index 2e09a938a05..59aff85c633 100644 --- a/addons/document_ftp/ftpserver/__init__.py +++ b/addons/document_ftp/ftpserver/__init__.py @@ -23,9 +23,9 @@ import threading import ftpserver import authorizer import abstracted_fs -import netsvc +import logging from tools import config - +_logger = logging.getLogger(__name__) def start_server(): HOST = config.get('ftp_server_host', '127.0.0.1') PORT = int(config.get('ftp_server_port', '8021')) @@ -35,10 +35,7 @@ def start_server(): PASSIVE_PORTS = int(pps[0]), int(pps[1]) class ftp_server(threading.Thread): - def log(self, level, message): - logger = netsvc.Logger() - logger.notifyChannel('FTP', level, message) - + def run(self): autho = authorizer.authorizer() ftpserver.FTPHandler.authorizer = autho @@ -48,17 +45,17 @@ def start_server(): if PASSIVE_PORTS: ftpserver.FTPHandler.passive_ports = PASSIVE_PORTS - ftpserver.log = lambda msg: self.log(netsvc.LOG_INFO, msg) + ftpserver.log = lambda msg: _logger.info(msg) ftpserver.logline = lambda msg: None - ftpserver.logerror = lambda msg: self.log(netsvc.LOG_ERROR, msg) + ftpserver.logerror = lambda msg: _logger.error(msg) ftpd = ftpserver.FTPServer((HOST, PORT), ftpserver.FTPHandler) ftpd.serve_forever() if HOST.lower() == 'none': - netsvc.Logger().notifyChannel("FTP", netsvc.LOG_INFO, "\n Server FTP Not Started\n") + _logger.info("\n Server FTP Not Started\n") else: - netsvc.Logger().notifyChannel("FTP", netsvc.LOG_INFO, "\n Serving FTP on %s:%s\n" % (HOST, PORT)) + _logger.info("\n Serving FTP on %s:%s\n" % (HOST, PORT)) ds = ftp_server() ds.daemon = True ds.start() diff --git a/addons/document_ftp/ftpserver/abstracted_fs.py b/addons/document_ftp/ftpserver/abstracted_fs.py index 83cd1cde725..429016a2376 100644 --- a/addons/document_ftp/ftpserver/abstracted_fs.py +++ b/addons/document_ftp/ftpserver/abstracted_fs.py @@ -56,7 +56,7 @@ class abstracted_fs(object): self.cwd = '/' self.cwd_node = None self.rnfr = None - self._log = logging.getLogger('FTP.fs') + self._log = logging.getLogger(__name__) # Ok def db_list(self): diff --git a/addons/document_webdav/redirect.py b/addons/document_webdav/redirect.py index 7bc0883f9e8..31f462bcbbe 100644 --- a/addons/document_webdav/redirect.py +++ b/addons/document_webdav/redirect.py @@ -24,9 +24,9 @@ import logging import urlparse from service.websrv_lib import FixSendError, HTTPHandler, HttpOptions from service.http_server import HttpLogHandler - +_logger = logging.getLogger(__name__) class RedirectHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler): - _logger = logging.getLogger('httpd.well-known') + _HTTP_OPTIONS = { 'Allow': ['OPTIONS', 'GET', 'HEAD', 'PROPFIND'] } redirect_paths = {} @@ -80,7 +80,7 @@ class RedirectHTTPHandler(HttpLogHandler, FixSendError, HttpOptions, HTTPHandler self.send_header("Content-Length", 0) self.end_headers() # Do we need a Cache-content: header here? - self._logger.debug("redirecting %s to %s", self.path, redir_path) + _logger.debug("redirecting %s to %s", self.path, redir_path) return None def do_PROPFIND(self): diff --git a/addons/document_webdav/test_davclient.py b/addons/document_webdav/test_davclient.py index 3c20cfed5b8..a4dda231bd8 100755 --- a/addons/document_webdav/test_davclient.py +++ b/addons/document_webdav/test_davclient.py @@ -43,7 +43,7 @@ from xmlrpclib import Transport, ProtocolError import StringIO import base64 -log = logging.getLogger('http-client') +_logger = logging.getLogger(__name__) class HTTP11(httplib.HTTP): _http_vsn = 11 @@ -62,7 +62,7 @@ class PersistentTransport(Transport): if not self._http.has_key(host): host, extra_headers, x509 = self.get_host_info(host) self._http[host] = HTTP11(host) - log.debug("New connection to %s", host) + _logger.debug("New connection to %s", host) return self._http[host] def get_host_info(self, host): @@ -170,7 +170,7 @@ class SafePersistentTransport(PersistentTransport): if not self._http.has_key(host): host, extra_headers, x509 = self.get_host_info(host) self._http[host] = httplib.HTTPS(host, None, **(x509 or {})) - log.debug("New connection to %s", host) + _logger.debug("New connection to %s", host) return self._http[host] class AuthClient(object): @@ -191,8 +191,8 @@ class BasicAuthClient(AuthClient): return super(BasicAuthClient,self).getAuth(atype, realm) if not self._realm_dict.has_key(realm): - log.debug("realm dict: %r", self._realm_dict) - log.debug("missing key: \"%s\"" % realm) + _logger.debug("realm dict: %r", self._realm_dict) + _logger.debug("missing key: \"%s\"" % realm) self.resolveFailedRealm(realm) return 'Basic '+ self._realm_dict[realm] @@ -239,7 +239,7 @@ class addAuthTransport: # This line will bork if self.setAuthClient has not # been issued. That is a programming error, fix your code! auths = self._auth_client.getAuth(atype, realm) - log.debug("sending authorization: %s", auths) + _logger.debug("sending authorization: %s", auths) h.putheader('Authorization', auths) self.send_content(h, request_body) @@ -255,8 +255,8 @@ class addAuthTransport: log.warning("Why have data on a 401 auth. message?") if realm.startswith('realm="') and realm.endswith('"'): realm = realm[7:-1] - log.debug("Resp: %r %r", resp.version,resp.isclosed(), resp.will_close) - log.debug("Want to do auth %s for realm %s", atype, realm) + _logger.debug("Resp: %r %r", resp.version,resp.isclosed(), resp.will_close) + _logger.debug("Want to do auth %s for realm %s", atype, realm) if atype != 'Basic': raise ProtocolError(host+handler, 403, "Unknown authentication method: %s" % atype, resp.msg) @@ -315,7 +315,7 @@ class HTTPSConnection(httplib.HTTPSConnection): lf = (len(ssl.PEM_FOOTER)+1) if cert[0-lf] != '\n': cert = cert[:0-lf]+'\n'+cert[0-lf:] - log.debug("len-footer: %s cert: %r", lf, cert[0-lf]) + _logger.debug("len-footer: %s cert: %r", lf, cert[0-lf]) return cert @@ -390,7 +390,7 @@ class DAVClient(object): import base64 dbg = self.dbg hdrs.update(self.hdrs) - log.debug("Getting %s http://%s:%d/%s", method, self.host, self.port, path) + _logger.debug("Getting %s http://%s:%d/%s", method, self.host, self.port, path) conn = httplib.HTTPConnection(self.host, port=self.port, timeout=self.timeout) conn.set_debuglevel(dbg) if not path: @@ -409,8 +409,8 @@ class DAVClient(object): data1 = r1.read() if not self.user: raise Exception('Must auth, have no user/pass!') - log.debug("Ver: %s, closed: %s, will close: %s", r1.version,r1.isclosed(), r1.will_close) - log.debug("Want to do auth %s for realm %s", atype, realm) + _logger.debug("Ver: %s, closed: %s, will close: %s", r1.version,r1.isclosed(), r1.will_close) + _logger.debug("Want to do auth %s for realm %s", atype, realm) if atype == 'Basic' : auths = base64.encodestring(self.user + ':' + self.passwd) if auths[-1] == "\n": @@ -422,22 +422,22 @@ class DAVClient(object): else: raise Exception("Unknown auth type %s" %atype) else: - log.warning("Got 401, cannot auth") + _logger.warning("Got 401, cannot auth") raise Exception('No auth') - log.debug("Reponse: %s %s",r1.status, r1.reason) + _logger.debug("Reponse: %s %s",r1.status, r1.reason) data1 = r1.read() if method != 'GET': - log.debug("Body:\n%s\nEnd of body", data1) + _logger.debug("Body:\n%s\nEnd of body", data1) try: ctype = r1.msg.getheader('content-type') if ctype and ';' in ctype: ctype, encoding = ctype.split(';',1) if ctype == 'text/xml': doc = xml.dom.minidom.parseString(data1) - log.debug("XML Body:\n %s", doc.toprettyxml(indent="\t")) + _logger.debug("XML Body:\n %s", doc.toprettyxml(indent="\t")) except Exception: - log.warning("could not print xml", exc_info=True) + _logger.warning("could not print xml", exc_info=True) pass conn.close() return r1.status, r1.msg, data1 @@ -474,7 +474,7 @@ class DAVClient(object): s, m, d = self._http_request(path, method='OPTIONS', hdrs=hdrs) assert s == 200, "Status: %r" % s assert 'OPTIONS' in m.getheader('Allow') - log.debug('Options: %r', m.getheader('Allow')) + _logger.debug('Options: %r', m.getheader('Allow')) if expect: self._assert_headers(expect, m) @@ -493,10 +493,10 @@ class DAVClient(object): for cnod in node.childNodes: if cnod.nodeType != node.ELEMENT_NODE: if strict: - log.debug("Found %r inside <%s>", cnod, node.tagName) + _logger.debug("Found %r inside <%s>", cnod, node.tagName) continue if namespaces and (cnod.namespaceURI not in namespaces): - log.debug("Ignoring <%s> in <%s>", cnod.tagName, node.localName) + _logger.debug("Ignoring <%s> in <%s>", cnod.tagName, node.localName) continue yield cnod @@ -533,10 +533,10 @@ class DAVClient(object): assert htver == 'HTTP/1.1' rstatus = int(sta) else: - log.debug("What is <%s> inside a ?", pno.tagName) + _logger.debug("What is <%s> inside a ?", pno.tagName) else: - log.debug("Unknown node: %s", cno.tagName) + _logger.debug("Unknown node: %s", cno.tagName) res.setdefault(href,[]).append((status, res_nss)) @@ -637,7 +637,7 @@ class DAVClient(object): if lsp[1] in davprops: lsline[lsp[0]] = lsp[2] else: - log.debug("Strange status: %s", st) + _logger.debug("Strange status: %s", st) res.append(lsline) diff --git a/addons/document_webdav/webdav_server.py b/addons/document_webdav/webdav_server.py index ae75d0b80fc..65ebb49ff5f 100644 --- a/addons/document_webdav/webdav_server.py +++ b/addons/document_webdav/webdav_server.py @@ -54,7 +54,7 @@ from DAV.propfind import PROPFIND # from DAV.constants import DAV_VERSION_1, DAV_VERSION_2 from xml.dom import minidom from redirect import RedirectHTTPHandler - +_logger = logging.getLogger(__name__) khtml_re = re.compile(r' KHTML/([0-9\.]+) ') def OpenDAVConfig(**kw): @@ -73,7 +73,7 @@ def OpenDAVConfig(**kw): class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): verbose = False - _logger = logging.getLogger('webdav') + protocol_version = 'HTTP/1.1' _HTTP_OPTIONS= { 'DAV' : ['1', '2'], 'Allow' : [ 'GET', 'HEAD', 'COPY', 'MOVE', 'POST', 'PUT', @@ -127,10 +127,10 @@ class DAVHandler(HttpOptions, FixSendError, DAVRequestHandler): return self.davpath def log_message(self, format, *args): - self._logger.debug(format % args) + _logger.debug(format % args) def log_error(self, format, *args): - self._logger.warning(format % args) + _logger.warning(format % args) def _prep_OPTIONS(self, opts): ret = opts @@ -477,7 +477,7 @@ class dummy_dav_interface(object): uri2 = uri.split('/') if len(uri2) < 3: return True - logging.getLogger('webdav').debug("Requested uri: %s", uri) + _logger.debug("Requested uri: %s", uri) return None # no def is_collection(self, uri): @@ -487,6 +487,7 @@ class dummy_dav_interface(object): class DAVStaticHandler(http_server.StaticHTTPHandler): """ A variant of the Static handler, which will serve dummy DAV requests """ + verbose = False protocol_version = 'HTTP/1.1' _HTTP_OPTIONS= { 'DAV' : ['1', '2'], @@ -573,7 +574,7 @@ try: conf = OpenDAVConfig(**_dc) handler._config = conf reg_http_service(directory, DAVHandler, DAVAuthProvider) - logging.getLogger('webdav').info("WebDAV service registered at path: %s/ "% directory) + _logger.info("WebDAV service registered at path: %s/ "% directory) if not (config.get_misc('webdav', 'no_root_hack', False)): # Now, replace the static http handler with the dav-enabled one. @@ -595,7 +596,7 @@ try: reg_http_service('/', DAVStaticHandler) except Exception, e: - logging.getLogger('webdav').error('Cannot launch webdav: %s' % e) + _logger.error('Cannot launch webdav: %s' % e) def init_well_known(): @@ -616,6 +617,8 @@ def init_well_known(): init_well_known() class PrincipalsRedirect(RedirectHTTPHandler): + + redirect_paths = {} def _find_redirect(self): @@ -639,7 +642,7 @@ def init_principals_redirect(): if dbname: PrincipalsRedirect.redirect_paths[''] = '/webdav/%s/principals' % dbname reg_http_service('/principals', PrincipalsRedirect) - logging.getLogger("web-services").info( + _logger.info( "Registered HTTP redirect handler for /principals to the %s db.", dbname) diff --git a/addons/document_webdav/webdav_view.xml b/addons/document_webdav/webdav_view.xml index 404e60588e3..44af9f2af3a 100644 --- a/addons/document_webdav/webdav_view.xml +++ b/addons/document_webdav/webdav_view.xml @@ -78,7 +78,7 @@ form - + - + diff --git a/addons/edi/__init__.py b/addons/edi/__init__.py index b4a80c0f1dd..46fabbc2fd0 100644 --- a/addons/edi/__init__.py +++ b/addons/edi/__init__.py @@ -23,12 +23,13 @@ import logging import models import edi_service from models.edi import EDIMixin, edi_document +_logger = logging.getLogger(__name__) # web try: import controllers except ImportError: - logging.getLogger('init.load').warn( + _logger.warn( """Could not load openerp-web section of EDI, EDI will not behave correctly To fix, launch openerp-web in embedded mode""") diff --git a/addons/edi/edi_service.py b/addons/edi/edi_service.py index 074720a03e9..f48cc60950a 100644 --- a/addons/edi/edi_service.py +++ b/addons/edi/edi_service.py @@ -23,7 +23,7 @@ import logging import netsvc import openerp -_logger = logging.getLogger('edi.service') +_logger = logging.getLogger(__name__) class edi(netsvc.ExportService): diff --git a/addons/edi/models/edi.py b/addons/edi/models/edi.py index 589922ff178..383d8c7aaba 100644 --- a/addons/edi/models/edi.py +++ b/addons/edi/models/edi.py @@ -35,6 +35,7 @@ import pooler from osv import osv,fields,orm from tools.translate import _ from tools.safe_eval import safe_eval as eval +_logger = logging.getLogger(__name__) EXTERNAL_ID_PATTERN = re.compile(r'^([^.:]+)(?::([^.]+))?\.(\S+)$') EDI_VIEW_WEB_URL = '%s/edi/view?db=%s&token=%s' @@ -72,7 +73,6 @@ def last_update_for(record): return record_log.get('write_date') or record_log.get('create_date') or False return False -_logger = logging.getLogger('edi') class edi_document(osv.osv): _name = 'edi.document' diff --git a/addons/edi/models/res_partner.py b/addons/edi/models/res_partner.py index 357fdec061b..86d8c81b790 100644 --- a/addons/edi/models/res_partner.py +++ b/addons/edi/models/res_partner.py @@ -24,6 +24,7 @@ from osv import fields,osv from edi import EDIMixin from openerp import SUPERUSER_ID from tools.translate import _ +_logger = logging.getLogger(__name__) RES_PARTNER_EDI_STRUCT = { 'name': True, @@ -63,7 +64,7 @@ class res_partner(osv.osv, EDIMixin): code, label = 'edi_generic', 'Generic Bank Type (auto-created for EDI)' bank_code_ids = res_partner_bank_type.search(cr, uid, [('code','=',code)], context=context) if not bank_code_ids: - logging.getLogger('edi.res_partner').info('Normal bank account type is missing, creating ' + _logger.info('Normal bank account type is missing, creating ' 'a generic bank account type for EDI.') self.res_partner_bank_type.create(cr, SUPERUSER_ID, {'name': label, 'code': label}) @@ -84,7 +85,7 @@ class res_partner(osv.osv, EDIMixin): bank_name, ext_bank_id, context=import_ctx) except osv.except_osv: # failed to import it, try again with unrestricted default type - logging.getLogger('edi.res_partner').warning('Failed to import bank account using' + _logger.warning('Failed to import bank account using' 'bank type: %s, ignoring', import_ctx['default_state'], exc_info=True) return contact_id diff --git a/addons/email_template/email_template.py b/addons/email_template/email_template.py index fff9bdc0f1a..c4ed2eedb85 100644 --- a/addons/email_template/email_template.py +++ b/addons/email_template/email_template.py @@ -29,11 +29,12 @@ from osv import fields import tools from tools.translate import _ from urllib import quote as quote +_logger = logging.getLogger(__name__) try: from mako.template import Template as MakoTemplate except ImportError: - logging.getLogger('init').warning("email_template: mako templates not available, templating features will not work!") + _logger.warning("email_template: mako templates not available, templating features will not work!") class email_template(osv.osv): "Templates for sending email" @@ -75,7 +76,7 @@ class email_template(osv.osv): result = u'' return result except Exception: - logging.exception("failed to render mako template value %r", template) + _logger.exception("failed to render mako template value %r", template) return u"" def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None): diff --git a/addons/event_sale/event_sale.py b/addons/event_sale/event_sale.py index 0d6497c45f2..9e65a034544 100644 --- a/addons/event_sale/event_sale.py +++ b/addons/event_sale/event_sale.py @@ -78,7 +78,6 @@ class sale_order_line(osv.osv): dic = { 'name': order_line.order_id.partner_invoice_id.name, 'partner_id': order_line.order_id.partner_id.id, - 'contact_id': order_line.order_id.partner_invoice_id.id, 'nb_register': int(order_line.product_uom_qty), 'email': order_line.order_id.partner_id.email, 'phone': order_line.order_id.partner_id.phone, @@ -89,5 +88,5 @@ class sale_order_line(osv.osv): } registration_id = registration_obj.create(cr, uid, dic, context=context) message = _("The registration %s has been created from the Sale Order %s.") % (registration_id, order_line.order_id.name) - registration_obj.log(cr, uid, registration_id, message) + registration_obj.message_append_note(cr, uid, [registration_id], body=message, context=context) return super(sale_order_line, self).button_confirm(cr, uid, ids, context=context) diff --git a/addons/fetchmail/fetchmail.py b/addons/fetchmail/fetchmail.py index 93ebd063051..cddcf642dc4 100644 --- a/addons/fetchmail/fetchmail.py +++ b/addons/fetchmail/fetchmail.py @@ -39,7 +39,7 @@ from osv import osv, fields import tools from tools.translate import _ -logger = logging.getLogger('fetchmail') +_logger = logging.getLogger(__name__) class fetchmail_server(osv.osv): """Incoming POP/IMAP mail server account""" diff --git a/addons/google_docs/__openerp__.py b/addons/google_docs/__openerp__.py index 1df94f34221..8cc5671e08a 100644 --- a/addons/google_docs/__openerp__.py +++ b/addons/google_docs/__openerp__.py @@ -30,6 +30,7 @@ 'web': True, 'js': ['static/src/js/gdocs.js'], 'update_xml': [ + 'security/ir.model.access.csv', 'res_config_user_view.xml' ], 'depends': ['google_base_account'], diff --git a/addons/google_docs/security/ir.model.access.csv b/addons/google_docs/security/ir.model.access.csv new file mode 100644 index 00000000000..9393d586fb8 --- /dev/null +++ b/addons/google_docs/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_google_docs,google.docs.config,model_google_docs_config,,1,0,0,0 +access_google_docs,google.docs.config,model_google_docs_config,base.group_system,1,1,1,1 \ No newline at end of file diff --git a/addons/hr/hr.py b/addons/hr/hr.py index e2bca73a72e..e4c08f60559 100644 --- a/addons/hr/hr.py +++ b/addons/hr/hr.py @@ -25,6 +25,7 @@ import logging from osv import fields, osv from PIL import Image import StringIO +_logger = logging.getLogger(__name__) class hr_employee_category(osv.osv): @@ -304,7 +305,7 @@ class res_users(osv.osv): 'user_id': user_id}, context=context) except: # Tolerate a missing shortcut. See product/product.py for similar code. - logging.getLogger('orm').debug('Skipped meetings shortcut for user "%s"', data.get('name','Francois
fpi fpi - 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 Julien jth jth - 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 diff --git a/addons/hr_expense/hr_expense_view.xml b/addons/hr_expense/hr_expense_view.xml index 1a6c8560afd..dd89a3ab57d 100644 --- a/addons/hr_expense/hr_expense_view.xml +++ b/addons/hr_expense/hr_expense_view.xml @@ -14,7 +14,7 @@ - + @@ -97,7 +97,7 @@
- + diff --git a/addons/hr_timesheet/hr_timesheet.py b/addons/hr_timesheet/hr_timesheet.py index 12fbbcdedef..66911f25ff5 100644 --- a/addons/hr_timesheet/hr_timesheet.py +++ b/addons/hr_timesheet/hr_timesheet.py @@ -190,4 +190,15 @@ class hr_analytic_timesheet(osv.osv): hr_analytic_timesheet() +class account_analytic_account(osv.osv): + + _inherit = 'account.analytic.account' + _description = 'Analytic Account' + + _columns = { + 'use_timesheets': fields.boolean('Timesheets', help="Check this field if this project manages timesheets"), + } + +account_analytic_account() + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/hr_timesheet/hr_timesheet_demo.xml b/addons/hr_timesheet/hr_timesheet_demo.xml index 3ec7664bcb7..077aaf6179c 100644 --- a/addons/hr_timesheet/hr_timesheet_demo.xml +++ b/addons/hr_timesheet/hr_timesheet_demo.xml @@ -12,6 +12,18 @@ + + True + + + True + + + True + + + True + Requirements analysis and specification @@ -20,7 +32,7 @@ - + @@ -33,7 +45,7 @@ - + @@ -46,7 +58,7 @@ - + @@ -59,7 +71,7 @@ - + @@ -72,7 +84,7 @@ - + diff --git a/addons/hr_timesheet/hr_timesheet_view.xml b/addons/hr_timesheet/hr_timesheet_view.xml index 53def628bfe..a8a361ed633 100644 --- a/addons/hr_timesheet/hr_timesheet_view.xml +++ b/addons/hr_timesheet/hr_timesheet_view.xml @@ -12,7 +12,7 @@ - + @@ -78,6 +78,21 @@ + + + account.analytic.account.invoice.form + account.analytic.account + form + + + + + + + + + + Timesheet Lines diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py index 0a58efb782b..f1138d7b246 100644 --- a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py +++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py @@ -63,39 +63,58 @@ class account_analytic_account(osv.osv): _inherit = "account.analytic.account" _columns = { - 'pricelist_id': fields.many2one('product.pricelist', 'Customer Pricelist', + 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', help="The product to invoice is defined on the employee form, the price will be deduced by this pricelist on the product."), 'amount_max': fields.float('Max. Invoice Price', help="Keep empty if this contract is not limited to a total fixed price."), 'amount_invoiced': fields.function(_invoiced_calc, string='Invoiced Amount', help="Total invoiced"), - 'to_invoice': fields.many2one('hr_timesheet_invoice.factor', 'Invoice on Timesheet & Costs', - help="Fill this field if you plan to automatically generate invoices based " \ - "on the costs in this analytic account: timesheets, expenses, ..." \ - "You can configure an automatic invoice rate on analytic accounts."), + 'to_invoice': fields.many2one('hr_timesheet_invoice.factor', 'Timesheet Invoicing Ratio', + help="This field allows you to define the rate in case you plan to reinvoice " \ + "the costs in this analytic account: timesheets, expenses, ..."), } _defaults = { 'pricelist_id': lambda self, cr, uid, ctx: ctx.get('pricelist_id', False), } - def on_change_partner_id(self, cr, uid, id, partner_id, context={}): - res={} - part = self.pool.get('res.partner').browse(cr, uid, partner_id) + + def on_change_use_timesheets(self, cr, uid, ids, use_timesheets, context=None): + res = {'value': {}} + if use_timesheets: + ir_model_obj = self.pool.get('ir.model.data') + res['value']['to_invoice'] = ir_model_obj.get_object_reference(cr, uid, 'hr_timesheet_invoice', 'timesheet_invoice_factor1')[1] + return res + + def on_change_partner_id(self, cr, uid, ids,partner_id, name, context=None): + res = super(account_analytic_account,self).on_change_partner_id(cr, uid, ids,partner_id, name, context=context) + part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context) pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False if pricelist: - res['pricelist_id'] = pricelist - return {'value': res} + res['value']['pricelist_id'] = pricelist + return res def set_close(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state':'close'}, context=context) + self.write(cr, uid, ids, {'state':'close'}, context=context) + message = _("Contract has been closed.") + self.message_append_note(cr, uid, ids, body=message, context=context) + return True def set_cancel(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state':'cancelled'}, context=context) + self.write(cr, uid, ids, {'state':'cancelled'}, context=context) + message = _("Contract has been cancelled.") + self.message_append_note(cr, uid, ids, body=message, context=context) + return True def set_open(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state':'open'}, context=context) + self.write(cr, uid, ids, {'state':'open'}, context=context) + message = _("Contract has been opened.") + self.message_append_note(cr, uid, ids, body=message, context=context) + return True def set_pending(self, cr, uid, ids, context=None): - return self.write(cr, uid, ids, {'state':'pending'}, context=context) + self.write(cr, uid, ids, {'state':'pending'}, context=context) + message = _("Contract has been set as pending.") + self.message_append_note(cr, uid, ids, body=message, context=context) + return True account_analytic_account() @@ -107,10 +126,6 @@ class account_analytic_line(osv.osv): 'to_invoice': fields.many2one('hr_timesheet_invoice.factor', 'Type of Invoicing', help="It allows to set the discount while making invoice"), } - def unlink(self, cursor, user, ids, context=None): - return super(account_analytic_line,self).unlink(cursor, user, ids, - context=context) - def write(self, cr, uid, ids, vals, context=None): self._check_inv(cr, uid, ids, vals) return super(account_analytic_line,self).write(cr, uid, ids, vals, diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml index 6aaf63a0c03..d3d310e6e9a 100644 --- a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml +++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml @@ -5,23 +5,18 @@ account.analytic.account.invoice.form account.analytic.account form - + 30 + - - - - - - + + + - - - - - - - + + + +
- - - - - - - - - - - + + + + + + + + + + + + + + +
diff --git a/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml b/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml index 993a6c67408..c43b1206541 100644 --- a/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml +++ b/addons/hr_timesheet_sheet/hr_timesheet_sheet_view.xml @@ -99,7 +99,7 @@ - + diff --git a/addons/import_base/import_framework.py b/addons/import_base/import_framework.py index 44243aa952c..8bcda79c64e 100644 --- a/addons/import_base/import_framework.py +++ b/addons/import_base/import_framework.py @@ -30,7 +30,7 @@ import logging import StringIO import traceback pp = pprint.PrettyPrinter(indent=4) - +_logger = logging.getLogger(__name__) @@ -60,7 +60,6 @@ class import_framework(Thread): self.context = context or {} self.email = email_to_notify self.table_list = [] - self.logger = logging.getLogger(module_name) self.initialize() """ @@ -165,7 +164,7 @@ class import_framework(Thread): data_i is a map external field_name => value and each data_i have a external id => in data_id['id'] """ - self.logger.info(' Importing %s into %s' % (table, model)) + _logger.info(' Importing %s into %s' % (table, model)) if not datas: return (0, 'No data found') mapping['id'] = 'id_new' @@ -188,7 +187,7 @@ class import_framework(Thread): model_obj = self.obj.pool.get(model) if not model_obj: raise ValueError(_("%s is not a valid model name") % model) - self.logger.debug(_(" fields imported : ") + str(fields)) + _logger.debug(_(" fields imported : ") + str(fields)) (p, r, warning, s) = model_obj.import_data(self.cr, self.uid, fields, res, mode='update', current_module=self.module_name, noupdate=True, context=self.context) for (field, field_name) in self_dependencies: self._import_self_dependencies(model_obj, field, datas) @@ -431,9 +430,9 @@ class import_framework(Thread): 'auto_delete' : True}) email_obj.send(self.cr, self.uid, [email_id]) if error: - self.logger.error(_("Import failed due to an unexpected error")) + _logger.error(_("Import failed due to an unexpected error")) else: - self.logger.info(_("Import finished, notification email sended")) + _logger.info(_("Import finished, notification email sended")) def get_email_subject(self, result, error=False): """ diff --git a/addons/import_sugarcrm/sugar.py b/addons/import_sugarcrm/sugar.py index 35103a41173..df2aa386992 100644 --- a/addons/import_sugarcrm/sugar.py +++ b/addons/import_sugarcrm/sugar.py @@ -33,7 +33,7 @@ import import_sugarcrm import logging import sys - +_logger = logging.getLogger(__name__) debug = False @@ -119,7 +119,7 @@ def get_contact_by_email(portType, username, password, email_address=None): email_list.append(list.Email_address) return email_list except Exception,e: - logging.getLogger('sugarcrm_soap').error('Exception: %s\n' % (tools.ustr(e))) + _logger.error('Exception: %s\n' % (tools.ustr(e))) return False def get_document_revision_search(portType, sessionid, module_id=None): diff --git a/addons/l10n_be_invoice_bba/invoice.py b/addons/l10n_be_invoice_bba/invoice.py index 997036c0cec..4cdbca84c0a 100644 --- a/addons/l10n_be_invoice_bba/invoice.py +++ b/addons/l10n_be_invoice_bba/invoice.py @@ -23,8 +23,8 @@ import re, time, random from osv import fields, osv from tools.translate import _ -import netsvc -logger=netsvc.Logger() +import logging +_logger = logging.getLogger(__name__) """ account.invoice object: @@ -41,7 +41,7 @@ class account_invoice(osv.osv): context=context) res[[i for i,x in enumerate(res) if x[0] == 'none'][0]] = ('none', 'Free Communication') res.append(('bba', 'BBA Structured Communication')) - #logger.notifyChannel('addons.'+self._name, netsvc.LOG_WARNING, 'reference_type = %s' %res ) + #l_logger.warning('reference_type = %s' %res ) return res def check_bbacomm(self, val): @@ -68,7 +68,7 @@ class account_invoice(osv.osv): result = super(account_invoice, self).onchange_partner_id(cr, uid, ids, type, partner_id, date_invoice, payment_term, partner_bank_id, company_id) # reference_type = self.default_get(cr, uid, ['reference_type'])['reference_type'] -# logger.notifyChannel('addons.'+self._name, netsvc.LOG_WARNING, 'partner_id %s' % partner_id) +# _logger.warning('partner_id %s' % partner_id) reference = False reference_type = 'none' if partner_id: @@ -215,5 +215,5 @@ class account_invoice(osv.osv): ] account_invoice() - -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/l10n_be_invoice_bba/partner.py b/addons/l10n_be_invoice_bba/partner.py index 04a51c3a6ab..8951be5dfa2 100644 --- a/addons/l10n_be_invoice_bba/partner.py +++ b/addons/l10n_be_invoice_bba/partner.py @@ -24,8 +24,6 @@ from osv import fields, osv import time from tools.translate import _ -import netsvc -logger=netsvc.Logger() class res_partner(osv.osv): """ add field to indicate default 'Communication Type' on customer invoices """ diff --git a/addons/l10n_multilang/l10n_multilang.py b/addons/l10n_multilang/l10n_multilang.py index c4130f2761d..c4c6be74e4b 100644 --- a/addons/l10n_multilang/l10n_multilang.py +++ b/addons/l10n_multilang/l10n_multilang.py @@ -22,8 +22,8 @@ from osv import fields, osv import os from tools.translate import _ -import netsvc -logger=netsvc.Logger() +import logging +_logger = logging.getLogger(__name__) class wizard_multi_charts_accounts(osv.osv_memory): """ @@ -80,8 +80,7 @@ class wizard_multi_charts_accounts(osv.osv_memory): if context.get('lang') == lang: self.pool.get(out_obj._name).write(cr, uid, out_ids[j], {in_field: value[in_id]}) else: - logger.notifyChannel('addons.'+self._name, netsvc.LOG_WARNING, - 'Language: %s. Translation from template: there is no translation available for %s!' %(lang, src[in_id]))#out_obj._name)) + _logger.info('Language: %s. Translation from template: there is no translation available for %s!' %(lang, src[in_id]))#out_obj._name)) return True def execute(self, cr, uid, ids, context=None): diff --git a/addons/mail/mail_message.py b/addons/mail/mail_message.py index f86ae29c80f..e45d4f07807 100644 --- a/addons/mail/mail_message.py +++ b/addons/mail/mail_message.py @@ -36,7 +36,7 @@ from osv import fields from tools.translate import _ from openerp import SUPERUSER_ID -_logger = logging.getLogger('mail') +_logger = logging.getLogger(__name__) def format_date_tz(date, tz=None): if not date: diff --git a/addons/mail/mail_subscription_view.xml b/addons/mail/mail_subscription_view.xml index 52b72127b78..7dbaea098dc 100644 --- a/addons/mail/mail_subscription_view.xml +++ b/addons/mail/mail_subscription_view.xml @@ -10,7 +10,7 @@ mail.subscription.tree mail.subscription tree - 10 + 10 @@ -28,7 +28,7 @@ mail.notification.tree mail.notification tree - 10 + 10 diff --git a/addons/mail/mail_thread.py b/addons/mail/mail_thread.py index 0f404155f42..e0f2bfaf455 100644 --- a/addons/mail/mail_thread.py +++ b/addons/mail/mail_thread.py @@ -68,9 +68,14 @@ class mail_thread(osv.osv): res[id] = self.message_load_ids(cr, uid, [id], context=context) return res + def _search_message_ids(self, cr, uid, obj, name, args, context=None): + msg_obj = self.pool.get('mail.message') + msg_ids = msg_obj.search(cr, uid, ['&', ('res_id', 'in', args[0][2]), ('model', '=', self._name)], context=context) + return [('id', 'in', msg_ids)] + # OpenChatter: message_ids is a dummy field that should not be used _columns = { - 'message_ids': fields.function(_get_message_ids, method=True, + 'message_ids': fields.function(_get_message_ids, method=True, fnct_search=_search_message_ids, type='one2many', obj='mail.message', string='Temp messages', _fields_id = 'res_id'), } @@ -294,7 +299,7 @@ class mail_thread(osv.osv): } to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context)) - partner_id = hasattr(thread, 'partner_id') and (thread.partner_id and thread.partner_id.id or False) or False + partner_id = ('partner_id' in thread._columns.keys()) and (thread.partner_id and thread.partner_id.id or False) or False if not partner_id and thread._name == 'res.partner': partner_id = thread.id data = { diff --git a/addons/portal/wizard/portal_wizard.py b/addons/portal/wizard/portal_wizard.py index 1ec4cbb0e4b..881be3be05f 100644 --- a/addons/portal/wizard/portal_wizard.py +++ b/addons/portal/wizard/portal_wizard.py @@ -27,7 +27,7 @@ from tools.misc import email_re from tools.translate import _ from base.res.res_users import _lang_get - +_logger = logging.getLogger(__name__) # welcome email sent to new portal users (note that calling tools.translate._ @@ -174,7 +174,7 @@ class wizard(osv.osv_memory): body = _(WELCOME_EMAIL_BODY) % data res = mail_message_obj.schedule_with_attach(cr, uid, email_from , [email_to], subject, body, context=context) if not res: - logging.getLogger('res.portal.wizard').warning( + _logger.warning( 'Failed to send email from %s to %s', email_from, email_to) return {'type': 'ir.actions.act_window_close'} diff --git a/addons/portal/wizard/share_wizard.py b/addons/portal/wizard/share_wizard.py index b5c9d165cc6..b2ed8703421 100644 --- a/addons/portal/wizard/share_wizard.py +++ b/addons/portal/wizard/share_wizard.py @@ -21,6 +21,8 @@ from osv import osv, fields from tools.translate import _ +import logging +_logger = logging.getLogger(__name__) UID_ROOT = 1 SHARED_DOCS_MENU = "Documents" @@ -164,19 +166,19 @@ class share_wizard_portal(osv.osv_memory): # v6.1, the algorithm for combining them will OR the rules, hence # extending the visible data. Rules.write(cr, UID_ROOT, share_rule_ids, {'groups': [(4,target_group.id)]}) - self._logger.debug("Linked sharing rules from temporary sharing group to group %s", target_group) + _logger.debug("Linked sharing rules from temporary sharing group to group %s", target_group) # Copy the access rights. This is appropriate too because # groups have the UNION of all permissions granted by their # access right lines. for access_line in share_group.model_access: Rights.copy(cr, UID_ROOT, access_line.id, default={'group_id': target_group.id}) - self._logger.debug("Copied access rights from temporary sharing group to group %s", target_group) + _logger.debug("Copied access rights from temporary sharing group to group %s", target_group) # finally, delete it after removing its users Groups.write(cr, UID_ROOT, [share_group_id], {'users': [(6,0,[])]}) Groups.unlink(cr, UID_ROOT, [share_group_id]) - self._logger.debug("Deleted temporary sharing group %s", share_group_id) + _logger.debug("Deleted temporary sharing group %s", share_group_id) def _finish_result_lines(self, cr, uid, wizard_data, share_group_id, context=None): super(share_wizard_portal,self)._finish_result_lines(cr, uid, wizard_data, share_group_id, context=context) diff --git a/addons/product_visible_discount/i18n/sv.po b/addons/product_visible_discount/i18n/sv.po new file mode 100644 index 00000000000..1b440cb2d4c --- /dev/null +++ b/addons/product_visible_discount/i18n/sv.po @@ -0,0 +1,62 @@ +# Swedish translation for openobject-addons +# Copyright (c) 2012 Rosetta Contributors and Canonical Ltd 2012 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2012. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-02-08 00:37+0000\n" +"PO-Revision-Date: 2012-06-26 22:45+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Swedish \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2012-06-27 05:28+0000\n" +"X-Generator: Launchpad (build 15482)\n" + +#. module: product_visible_discount +#: code:addons/product_visible_discount/product_visible_discount.py:153 +#, python-format +msgid "No Sale Pricelist Found!" +msgstr "Ingen försäljningsprislista funnen!" + +#. module: product_visible_discount +#: field:product.pricelist,visible_discount:0 +msgid "Visible Discount" +msgstr "Synlig rabatt" + +#. module: product_visible_discount +#: code:addons/product_visible_discount/product_visible_discount.py:145 +#, python-format +msgid "No Purchase Pricelist Found!" +msgstr "" + +#. module: product_visible_discount +#: model:ir.model,name:product_visible_discount.model_account_invoice_line +msgid "Invoice Line" +msgstr "Fakturarad" + +#. module: product_visible_discount +#: model:ir.model,name:product_visible_discount.model_product_pricelist +msgid "Pricelist" +msgstr "Prislista" + +#. module: product_visible_discount +#: code:addons/product_visible_discount/product_visible_discount.py:145 +#, python-format +msgid "You must first define a pricelist on the supplier form!" +msgstr "" + +#. module: product_visible_discount +#: model:ir.model,name:product_visible_discount.model_sale_order_line +msgid "Sales Order Line" +msgstr "Orderrad" + +#. module: product_visible_discount +#: code:addons/product_visible_discount/product_visible_discount.py:153 +#, python-format +msgid "You must first define a pricelist on the customer form!" +msgstr "" diff --git a/addons/project/project.py b/addons/project/project.py index 429672d3269..ab02cc870bb 100644 --- a/addons/project/project.py +++ b/addons/project/project.py @@ -165,14 +165,29 @@ class project(osv.osv): res[task.project_id.id] += 1 return res + def _get_followers(self, cr, uid, ids, name, arg, context=None): + ''' + Functional field that computes the users that are 'following' a thread. + ''' + res = {} + for project in self.browse(cr, uid, ids, context=context): + l = set() + for message in project.message_ids: + l.add(message.user_id and message.user_id.id or False) + res[project.id] = list(filter(None, l)) + return res + + def _search_followers(self, cr, uid, obj, name, args, context=None): + project_obj = self.pool.get('project.project') + project_ids = project_obj.search(cr, uid, [('message_ids.user_id.id', 'in', args[0][2])], context=context) + return [('id', 'in', project_ids)] + _columns = { 'complete_name': fields.function(_complete_name, string="Project Name", type='char', size=250), 'active': fields.boolean('Active', help="If the active field is set to False, it will allow you to hide the project without removing it."), 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of Projects."), 'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', help="Link this project to an analytic account if you need financial management on projects. It enables you to connect projects with budgets, planning, cost and revenue analysis, timesheets on projects, etc.", ondelete="cascade", required=True), 'priority': fields.integer('Sequence', help="Gives the sequence order when displaying the list of projects"), - 'warn_manager': fields.boolean('Notify Manager', help="If you check this field, the project manager will receive an email each time a task is completed by his team.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}), - 'members': fields.many2many('res.users', 'project_user_rel', 'project_id', 'uid', 'Project Members', help="Project's members are users who can have an access to the tasks related to this project.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}), 'tasks': fields.one2many('project.task', 'project_id', "Task Activities"), @@ -197,19 +212,18 @@ class project(osv.osv): 'project.task': (_get_projects_from_tasks, ['planned_hours', 'remaining_hours', 'work_ids', 'state'], 20), }), 'resource_calendar_id': fields.many2one('resource.calendar', 'Working Time', help="Timetable working hours to adjust the gantt diagram report", states={'close':[('readonly',True)]} ), - 'warn_customer': fields.boolean('Warn Partner', help="If you check this, the user will have a popup when closing a task that propose a message to send by email to the customer.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}), - 'warn_header': fields.text('Mail Header', help="Header added at the beginning of the email for the warning message sent to the customer when a task is closed.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}), - 'warn_footer': fields.text('Mail Footer', help="Footer added at the beginning of the email for the warning message sent to the customer when a task is closed.", states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}), 'type_ids': fields.many2many('project.task.type', 'project_task_type_rel', 'project_id', 'type_id', 'Tasks Stages', states={'close':[('readonly',True)], 'cancelled':[('readonly',True)]}), - 'use_tasks': fields.boolean('Use Tasks', help="Check this field if this project is aimed at managing tasks"), 'task_count': fields.function(_task_count, type='integer', string="Open Tasks"), 'color': fields.integer('Color Index'), - 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'), + 'privacy_visibility': fields.selection([('public','Public'), ('followers','Followers Only')], 'Privacy / Visibility'), + 'state': fields.selection([('template', 'Template'),('draft','New'),('open','In Progress'), ('cancelled', 'Cancelled'),('pending','Pending'),('close','Closed')], 'Status', required=True,), + 'followers': fields.function(_get_followers, method=True, fnct_search=_search_followers, + type='many2many', relation='res.users', string='Followers'), } def dummy(self, cr, uid, ids, context): return True - + def _get_type_common(self, cr, uid, context): ids = self.pool.get('project.task.type').search(cr, uid, [('case_default','=',1)], context=context) return ids @@ -217,10 +231,11 @@ class project(osv.osv): _order = "sequence" _defaults = { 'active': True, + 'type': 'contract', + 'state': 'open', 'priority': 1, 'sequence': 10, 'type_ids': _get_type_common, - 'use_tasks': True, } # TODO: Why not using a SQL contraints ? @@ -894,28 +909,12 @@ class task(base_stage, osv.osv): return True def action_close(self, cr, uid, ids, context=None): - """ This action closes the task, then opens the wizard to send an - email to the partner or the project manager. + """ This action closes the task """ task_id = len(ids) and ids[0] or False self._check_child_task(cr, uid, ids, context=context) if not task_id: return False - task = self.browse(cr, uid, task_id, context=context) - project = task.project_id - res = self.do_close(cr, uid, [task_id], context=context) - if project.warn_manager or project.warn_customer: - return { - 'name': _('Send Email after close task'), - 'view_type': 'form', - 'view_mode': 'form', - 'res_model': 'mail.compose.message', - 'type': 'ir.actions.act_window', - 'target': 'new', - 'nodestroy': True, - 'context': {'active_id': task.id, - 'active_model': 'project.task'} - } - return res + return self.do_close(cr, uid, [task_id], context=context) def do_close(self, cr, uid, ids, context=None): """ Compatibility when changing to case_close. """ @@ -1217,16 +1216,49 @@ class project_work(osv.osv): class account_analytic_account(osv.osv): - _inherit = 'account.analytic.account' _description = 'Analytic Account' + _columns = { + 'use_tasks': fields.boolean('Tasks Management',help="If check,this contract will be available in the project menu and you will be able to manage tasks or track issues"), + 'company_uom_id': fields.related('company_id', 'project_time_mode_id', type='many2one', relation='product.uom'), + } + + def _trigger_project_creation(self, cr, uid, vals, context=None): + ''' + This function is used to decide if a project needs to be automatically created or not when an analytic account is created. It returns True if it needs to be so, False otherwise. + ''' + return vals.get('use_tasks') + + def project_create(self, cr, uid, analytic_account_id, vals, context=None): + ''' + This function is called at the time of analytic account creation and is used to create a project automatically linked to it if the conditions are meet. + ''' + project_pool = self.pool.get('project.project') + project_id = project_pool.search(cr, uid, [('analytic_account_id','=', analytic_account_id)]) + if not project_id and self._trigger_project_creation(cr, uid, vals, context=context): + project_values = { + 'name': vals.get('name'), + 'analytic_account_id': analytic_account_id, + } + return project_pool.create(cr, uid, project_values, context=context) + return False def create(self, cr, uid, vals, context=None): if context is None: context = {} if vals.get('child_ids', False) and context.get('analytic_project_copy', False): vals['child_ids'] = [] - return super(account_analytic_account, self).create(cr, uid, vals, context=context) + analytic_account_id = super(account_analytic_account, self).create(cr, uid, vals, context=context) + self.project_create(cr, uid, analytic_account_id, vals, context=context) + return analytic_account_id + + def write(self, cr, uid, ids, vals, context=None): + name = vals.get('name') + for account in self.browse(cr, uid, ids, context=context): + if not name: + vals['name'] = account.name + self.project_create(cr, uid, account.id, vals, context=context) + return super(account_analytic_account, self).write(cr, uid, ids, vals, context=context) def unlink(self, cr, uid, ids, *args, **kwargs): project_obj = self.pool.get('project.project') diff --git a/addons/project/project_demo.xml b/addons/project/project_demo.xml index f685f14c85b..07951280dc7 100644 --- a/addons/project/project_demo.xml +++ b/addons/project/project_demo.xml @@ -286,14 +286,12 @@ - 1 OpenERP Integration - 1 10 Study + Prototype diff --git a/addons/project/project_view.xml b/addons/project/project_view.xml index 4c79441ad18..ac67cbe0319 100644 --- a/addons/project/project_view.xml +++ b/addons/project/project_view.xml @@ -10,7 +10,16 @@ web_icon_hover="images/project-hover.png"/> - + + + @@ -34,7 +43,7 @@