From 48dbf92a9c0751af0d27af0890e4d19275ab2027 Mon Sep 17 00:00:00 2001 From: "Amit Parmar (OpenERP)" Date: Fri, 27 May 2011 12:55:23 +0530 Subject: [PATCH 001/526] [EDI]: develop a ir_edi_document class bzr revid: aar@tinyerp.com-20110527072523-v93lpjnpxm765bqd --- openerp/addons/base/ir/__init__.py | 2 +- openerp/addons/base/ir/ir_edi.py | 215 +++++++++++++++++++++++++++++ 2 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 openerp/addons/base/ir/ir_edi.py diff --git a/openerp/addons/base/ir/__init__.py b/openerp/addons/base/ir/__init__.py index 9b4eb96df5f..673c4f7e0ab 100644 --- a/openerp/addons/base/ir/__init__.py +++ b/openerp/addons/base/ir/__init__.py @@ -36,6 +36,6 @@ import ir_rule import wizard import ir_config_parameter import osv_memory_autovacuum - +import ir_edi # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py new file mode 100644 index 00000000000..8b980d4cf4c --- /dev/null +++ b/openerp/addons/base/ir/ir_edi.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 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 . +# +############################################################################## + +from osv import osv,fields +import json +import hashlib +import time +import base64 +import urllib2 + +class ir_edi_document(osv.osv): + _name = 'ir.edi.document' + _description = 'ir_edi_document' + _columns = { + 'name': fields.char('EDI token', size = 128), + 'document': fields.text() + + } + + def safe_unique_id(model, database_id): + + """Generate a unique string to represent a (model,database id) pair + without being too long, without revealing the database id, and + with a very low probability of collisions. + """ + msg = "%s-%s-%s" % (model, time.time(), database_id) + digest = hashlib.sha1(msg).digest() + digest = ''.join(chr(ord(x) ^ ord(y)) for (x,y) in zip(digest[:9], digest[9:-2])) + # finally, use the b64-encoded folded digest as ID part of the unique ID: + digest = base64.urlsafe_b64encode(digest) + + return '%s' % (digest) + + def new_edi_token(self,cr,uid,ids,browse_records,database_id,context=None): + + """Generate new unique tokent value to uniquely identify each edi_document""" + + db_uuid = self.safe_unique_id(browse_records, database_id) + edi_token = hashlib.sha256('%s-%s-%s' % (time.time(),db_uuid,time.time())).hexdigest() + + + return edi_token + + def serialize(self,edi_list_of_dicts): + + """Serialize the list of dictionaries using json dumps method""" + + serialized_list = json.dumps(edi_list_of_dicts) + return serialized_list + + def generate_edi(self,browse_record_list): + + """ Generate the list of dictionaries using edi_export method of edi class """ + + edi = edi() + edi_list = [] + for record in browse_record_list: + edi_struct = edi.edi_export(record) + edi_list.append(edi_struct[0]) + return self.serialize(edi_list) + + def get_document(self,cr,uid,ids,edi_token,context=None): + + """Get the edi document from database using given edi token """ + + records = self.search(cr,uid,[('name','=',edi_token)],context=context) + if records: + doc_string = self.read(cr,uid,records,['document'],context=context) + else: + raise osv.except_osv(_('Error !'), + _('Desired EDI Document does not Exist')) + + return True + + def load_edi(self,cr,uid,ids,edi_list_of_dicts,context=None): + + """loads the values from list of dictionaries to the corresponding OpenERP records + using the edi_import method of edi class""" + edi = edi() + edi_list = [] + for record in edi_list_of_dicts: + edi_record = edi.edi_import(record) + + + return True + + def deserialize(self,edi_document_string): + """ Deserialized the edi document string""" + + deserialize_string = json.loads(edi_document_string) + return self.load_edi(deserialize_string) + + def export_doc(self, cr, uid, ids,browse_records,database_id,context=None): + """the method handles the flow of the edi document generation and store it in + the database and return the edi_token of the particular document""" + + edi_document = { + 'name': self.new_edi_token(browse_records,database_id), + 'document': self.generate_edi(browse_records) + } + + unique_id = self.create(cr,uid,edi_document,context=context) + return edi_document['name'] + + def import_doc(self, cr, uid, ids, edi_document=None,edi_url=None,context=None): + + """the method handles the flow of importing particular edi document and + updates the database values on the basis of the edi document using + edi_loads method """ + + if edi_document is not None: + self.deserialize(edi_document) + elif edi_url is not None: + edi_document = urllib2.urlopen(edi_url).read() + self.deserialize(edi_document) + + return True + +ir_edi_document() + + +class edi(object): + _name = 'edi' + _description = 'edi document handler' + + """Mixin class for OSV objects that want be exposed as EDI documents. + Classes that inherit from this mixin class should override the + ``edi_import()`` and ``edi_export()`` methods to implement their + specific behavior, based on the primitives provided by this superclass.""" + + + def edi_metadata(self, cr, uid, browse_rec_list, context=None): + """Return a list representing the boilerplate EDI structure for + exporting the record in the given browse_rec_list, including + the metadata fields""" + # generic implementation! + return + + def edi_m2o(self, browse_rec, context=None): + """Return a list representing a M2O EDI value for + the given browse_record.""" + # generic implementation! + return + + def edi_o2m(self, browse_rec_list, context=None): + """Return a list representing a O2M EDI value for + the browse_records from the given browse_record_list.""" + # generic implementation! + return + + def edi_m2m(self, browse_rec_list, context=None): + """Return a list representing a M2M EDI value for + the browse_records from the given browse_record_list.""" + # generic implementation! + return + + def edi_export(self, cr, uid, ids, edi_struct=None, context=None): + """Returns a list of dicts representing an edi.document containing the + browse_records with ``ids``, using the generic algorithm. + :param edi_struct: if provided, edi_struct should be a dictionary + with a skeleton of the OSV fields to export as edi. + Basic fields can have any key as value, but o2m + values should have a sample skeleton dict as value. + For example, for a res.partner record: + edi_struct: { + 'name': True, + 'company_id': True, + 'address': { + 'name': True, + 'street': True, + } + } + Any field not specified in the edi_struct will not + be included in the exported data. + """ + # generic implementation! + + edi_struct = { + 'Openerp_fields':'', + '__model':'', + '__module':'', + '__id':'', + '__last_update':'', + '__version':'', + '__attachments':'', + + } + + + + return edi_struct + def edi_import(self, cr, uid, edi_document, context=None): + """Imports a list of dicts representing an edi.document, using the + generic algorithm. + """ + # generic implementation! + pass From 2e7ee8692142df851053967954e8eb55ffb13203 Mon Sep 17 00:00:00 2001 From: "Amit Parmar (OpenERP)" Date: Fri, 27 May 2011 19:01:31 +0530 Subject: [PATCH 002/526] [EDI]: working on a edi class bzr revid: aar@tinyerp.com-20110527133131-a1romjw8fft97950 --- openerp/addons/base/ir/ir_edi.py | 35 +++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 8b980d4cf4c..2187b4f89ca 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -74,7 +74,7 @@ class ir_edi_document(osv.osv): edi_list = [] for record in browse_record_list: edi_struct = edi.edi_export(record) - edi_list.append(edi_struct[0]) + edi_list.append(edi_struct) return self.serialize(edi_list) def get_document(self,cr,uid,ids,edi_token,context=None): @@ -193,8 +193,8 @@ class edi(object): """ # generic implementation! - edi_struct = { - 'Openerp_fields':'', + self.edi_document = { + '__model':'', '__module':'', '__id':'', @@ -203,10 +203,31 @@ class edi(object): '__attachments':'', } - - - - return edi_struct + data_object = self.pool.get('ir.model.data') + fields_object = self.pool.get('ir.model.fields') + for field in edi_struct.keys(): + if field in self.edi_document.keys(): + if field == 'model': + model_name = edi_struct['model'] + + record_ids = data_object.search(cr,uid,[('model','=','hr.holidays')]) + for fname in data_object.browse(cr,uid,record_ids): + if fname.name: + xml_ID = fname.name + self.edi_metadata(field,model_name) + else: + self.metadata(field) + else: + + f_ids = fields_object.search(cr,uid,[('name','=',field),('model','=',model_name)],context=context) + for fname in fields_object.browse(cr,uid,f_ids): + if fname.ttype == 'many2one': + self.edi_document[field] = self.edi_m2o(edi_struct[field]) + elif fname.ttype == 'one2many': + self.edi_ducument[field] = self.edi_o2m(edi_struct[field]) + elif fname.ttype == 'many2many': + self.edi_document[field] = self.edi_m2m(edi_struct[field]) + return self.edi_document def edi_import(self, cr, uid, edi_document, context=None): """Imports a list of dicts representing an edi.document, using the generic algorithm. From 2853ca743f27769f67803e405353c8656d7600b6 Mon Sep 17 00:00:00 2001 From: "Amit Parmar (OpenERP)" Date: Sat, 28 May 2011 19:09:43 +0530 Subject: [PATCH 003/526] [EDI]: working on a edi class bzr revid: aar@tinyerp.com-20110528133943-1r1hle6k75dvr17o --- openerp/addons/base/ir/ir_edi.py | 37 +++++++++++++++++++------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 2187b4f89ca..251108cbf04 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -147,12 +147,16 @@ class edi(object): specific behavior, based on the primitives provided by this superclass.""" - def edi_metadata(self, cr, uid, browse_rec_list, context=None): + def edi_metadata(self, cr, uid, browse_rec_list,model=None, context=None): """Return a list representing the boilerplate EDI structure for exporting the record in the given browse_rec_list, including the metadata fields""" # generic implementation! - return + if model is not None: + self.edi_document['__model'] = model + + + return True def edi_m2o(self, browse_rec, context=None): """Return a list representing a M2O EDI value for @@ -195,28 +199,30 @@ class edi(object): self.edi_document = { - '__model':'', - '__module':'', - '__id':'', - '__last_update':'', - '__version':'', - '__attachments':'', + 'model':'', + 'module':'', + 'id':'', + 'last_update':'', + 'version':'', + 'attachments':'', } + if 'model' not in edi_struct.keys(): + return False data_object = self.pool.get('ir.model.data') + fields_object = self.pool.get('ir.model.fields') - for field in edi_struct.keys(): - if field in self.edi_document.keys(): + + for field in self.edi_document.keys(): + + if field in edi_struct.keys(): if field == 'model': model_name = edi_struct['model'] - - record_ids = data_object.search(cr,uid,[('model','=','hr.holidays')]) + record_ids = data_object.search(cr,uid,[('model','=',model_name)]) for fname in data_object.browse(cr,uid,record_ids): if fname.name: xml_ID = fname.name - self.edi_metadata(field,model_name) - else: - self.metadata(field) + else: f_ids = fields_object.search(cr,uid,[('name','=',field),('model','=',model_name)],context=context) @@ -229,6 +235,7 @@ class edi(object): self.edi_document[field] = self.edi_m2m(edi_struct[field]) return self.edi_document def edi_import(self, cr, uid, edi_document, context=None): + """Imports a list of dicts representing an edi.document, using the generic algorithm. """ From d6f19568d0b08c2b2110a357fa443d176c9c515e Mon Sep 17 00:00:00 2001 From: "Amit Parmar (OpenERP)" Date: Mon, 30 May 2011 19:04:28 +0530 Subject: [PATCH 004/526] [EDI]: working on edi import bzr revid: aar@tinyerp.com-20110530133428-v68yubyv3rb9r4xb --- openerp/addons/base/ir/ir_edi.py | 161 ++++++++++++++++++++----------- 1 file changed, 107 insertions(+), 54 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 251108cbf04..9d66d4ed866 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -26,6 +26,20 @@ import time import base64 import urllib2 +def safe_unique_id(model, database_id): + + """Generate a unique string to represent a (model,database id) pair + without being too long, without revealing the database id, and + with a very low probability of collisions. + """ + msg = "%s-%s-%s" % (model, time.time(), database_id) + digest = hashlib.sha1(msg).digest() + digest = ''.join(chr(ord(x) ^ ord(y)) for (x,y) in zip(digest[:9], digest[9:-2])) + # finally, use the b64-encoded folded digest as ID part of the unique ID: + digest = base64.urlsafe_b64encode(digest) + + return '%s' % (digest) + class ir_edi_document(osv.osv): _name = 'ir.edi.document' _description = 'ir_edi_document' @@ -35,25 +49,13 @@ class ir_edi_document(osv.osv): } - def safe_unique_id(model, database_id): - - """Generate a unique string to represent a (model,database id) pair - without being too long, without revealing the database id, and - with a very low probability of collisions. - """ - msg = "%s-%s-%s" % (model, time.time(), database_id) - digest = hashlib.sha1(msg).digest() - digest = ''.join(chr(ord(x) ^ ord(y)) for (x,y) in zip(digest[:9], digest[9:-2])) - # finally, use the b64-encoded folded digest as ID part of the unique ID: - digest = base64.urlsafe_b64encode(digest) - - return '%s' % (digest) + def new_edi_token(self,cr,uid,ids,browse_records,database_id,context=None): """Generate new unique tokent value to uniquely identify each edi_document""" - db_uuid = self.safe_unique_id(browse_records, database_id) + db_uuid = safe_unique_id(browse_records, database_id) edi_token = hashlib.sha256('%s-%s-%s' % (time.time(),db_uuid,time.time())).hexdigest() @@ -88,7 +90,7 @@ class ir_edi_document(osv.osv): raise osv.except_osv(_('Error !'), _('Desired EDI Document does not Exist')) - return True + return doc_string def load_edi(self,cr,uid,ids,edi_list_of_dicts,context=None): @@ -147,34 +149,91 @@ class edi(object): specific behavior, based on the primitives provided by this superclass.""" - def edi_metadata(self, cr, uid, browse_rec_list,model=None, context=None): + def edi_metadata(self, cr, uid, ids, browse_rec_list, context=None): """Return a list representing the boilerplate EDI structure for exporting the record in the given browse_rec_list, including the metadata fields""" # generic implementation! - if model is not None: - self.edi_document['__model'] = model + data_object = self.pool.get('ir.model.data') + if self.edi_document['__model'] is not None: + model_ids = data_object.search(cr,uid,[('model','=',self.edi_document['__model'])],context=context) + for data in data_object.browse(cr,uid,model_ids): + self.edi_document['__module'] = data.module + if data.name is not None: + self.edi_document['__id'] = context['active_id']+':'+data.name + self.edi_document['__last_updata'] = context['__last_update'].values()[0] return True - def edi_m2o(self, browse_rec, context=None): + def edi_m2o(self, cr, uid, ids, browse_rec,model=None,context=None): """Return a list representing a M2O EDI value for the given browse_record.""" # generic implementation! - return + res = [] + data_object = self.pool.get('ir.model.data') + uu_id = data_model.search(cr,uid,[('model','=',model)]) + for xml in data_model.browse(cr,uid,uu_id): + if xml: + xml_ID = xml.name + else: + xml_ID = None + for data in browse_rec: + if data.name is not None: + if xml_ID is not None: + res.append(safe_unique_id(model,data.id)+':'+xml_ID) + else: + res.append(safe_unique_id(model,data.id)+':'+model+'-'+safe_unique_id(model,data.id)) + res.append(data.name) + return res - def edi_o2m(self, browse_rec_list, context=None): + def edi_o2m(self, cr, uid, ids, browse_rec_list, model=None, context=None): """Return a list representing a O2M EDI value for the browse_records from the given browse_record_list.""" # generic implementation! - return + res = [] + init_data = {} + data_object = self.pool.get('ir.model.data') + uu_id = data_model.search(cr,uid,[('model','=',model)]) + for xml in data_model.browse(cr,uid,uu_id): + if xml: + xml_ID = xml.name + else: + xml_ID = None + data_model = self.pool.get(model) + for record in browse_rec_list: + if xml_ID is not None: + init_data['__id'] = safe_unique_id(model,record.id)+':'+xml_ID + else: + init_data['__id'] = safe_unique_id(model,record.id)+':'+model+'-'+safe_unique_id(model,record.id) + init_data['__last_update'] =record.write_date + init_data.update(datamodel.read(cr,uid,[record],[],context=context)[0]) + res.append(init_data) + return res - def edi_m2m(self, browse_rec_list, context=None): + def edi_m2m(self, cr, uid, ids, browse_rec_list,model=None, context=None): """Return a list representing a M2M EDI value for the browse_records from the given browse_record_list.""" # generic implementation! - return + res = [] + init_data = [] + data_object = self.pool.get('ir.model.data') + uu_id = data_model.search(cr,uid,[('model','=',model)]) + for xml in data_model.browse(cr,uid,uu_id): + if xml: + xml_ID = xml.name + else: + xml_ID = None + data_model = self.pool.get(model) + for record in browse_rec_list: + if xml_ID is not None: + init_data.append(safe_unique_id(model,record.id)+':'+xml_ID) + else: + init_data.append(safe_unique_id(model,record.id)+':'+model+'-'+safe_unique_id(model,record.id)) + if record.name is not None: + init_data.append('name:'+record.name) + res.append(init_data) + return res def edi_export(self, cr, uid, ids, edi_struct=None, context=None): """Returns a list of dicts representing an edi.document containing the @@ -199,40 +258,32 @@ class edi(object): self.edi_document = { - 'model':'', - 'module':'', - 'id':'', - 'last_update':'', - 'version':'', - 'attachments':'', + '__model':'', + '__module':'', + '__id':'', + '__last_update':'', + '__version':'', + '__attachments':'', } - if 'model' not in edi_struct.keys(): - return False - data_object = self.pool.get('ir.model.data') - - fields_object = self.pool.get('ir.model.fields') + self.edi_document['__model'] = self._name - for field in self.edi_document.keys(): - - if field in edi_struct.keys(): - if field == 'model': - model_name = edi_struct['model'] - record_ids = data_object.search(cr,uid,[('model','=',model_name)]) - for fname in data_object.browse(cr,uid,record_ids): - if fname.name: - xml_ID = fname.name + + fields_object = self.pool.get('ir.model.fields') + model_object = self.pool.get(self.edi_document['__model']) + edi_metadata(context=context) + for field in self.edi_struct.keys(): + f_ids = fields_object.search(cr,uid,[('name','=',field),('model','=',self.edi_document['__model'])],context=context) + for fname in fields_object.browse(cr,uid,f_ids): + if fname.ttype == 'many2one': - else: - - f_ids = fields_object.search(cr,uid,[('name','=',field),('model','=',model_name)],context=context) - for fname in fields_object.browse(cr,uid,f_ids): - if fname.ttype == 'many2one': - self.edi_document[field] = self.edi_m2o(edi_struct[field]) - elif fname.ttype == 'one2many': - self.edi_ducument[field] = self.edi_o2m(edi_struct[field]) - elif fname.ttype == 'many2many': - self.edi_document[field] = self.edi_m2m(edi_struct[field]) + self.edi_document[field] = self.edi_m2o(edi_struct[field],fname.relation) + elif fname.ttype == 'one2many': + self.edi_ducument[field] = self.edi_o2m(edi_struct[field],fname.relation) + elif fname.ttype == 'many2many': + self.edi_document[field] = self.edi_m2m(edi_struct[field],fname.relation) + else: + self.edi_document[field] = model_object.read(cr,uid,[context['active_id']],[field],context=context) return self.edi_document def edi_import(self, cr, uid, edi_document, context=None): @@ -240,4 +291,6 @@ class edi(object): generic algorithm. """ # generic implementation! + for field in edi_document.keys(): + pass From 4deb609685c4fa002d13bb102fb141b09b4f6782 Mon Sep 17 00:00:00 2001 From: "Amit Parmar (OpenERP)" Date: Wed, 1 Jun 2011 19:03:10 +0530 Subject: [PATCH 005/526] [EDI]: working on edi import bzr revid: aar@tinyerp.com-20110601133310-nklnhlymx8u38nsg --- openerp/addons/base/ir/ir_edi.py | 81 ++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 25 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 9d66d4ed866..a87453fd277 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -38,7 +38,7 @@ def safe_unique_id(model, database_id): # finally, use the b64-encoded folded digest as ID part of the unique ID: digest = base64.urlsafe_b64encode(digest) - return '%s' % (digest) + return '%s-%s' % (model,digest) class ir_edi_document(osv.osv): _name = 'ir.edi.document' @@ -51,7 +51,7 @@ class ir_edi_document(osv.osv): - def new_edi_token(self,cr,uid,ids,browse_records,database_id,context=None): + def new_edi_token(self,browse_records,database_id): """Generate new unique tokent value to uniquely identify each edi_document""" @@ -71,15 +71,14 @@ class ir_edi_document(osv.osv): def generate_edi(self,browse_record_list): """ Generate the list of dictionaries using edi_export method of edi class """ - - edi = edi() + edi_list = [] for record in browse_record_list: - edi_struct = edi.edi_export(record) + edi_struct = record.edi_export(record) edi_list.append(edi_struct) return self.serialize(edi_list) - def get_document(self,cr,uid,ids,edi_token,context=None): + def get_document(self,cr,uid,edi_token,context=None): """Get the edi document from database using given edi token """ @@ -96,21 +95,21 @@ class ir_edi_document(osv.osv): """loads the values from list of dictionaries to the corresponding OpenERP records using the edi_import method of edi class""" - edi = edi() + edi_list = [] for record in edi_list_of_dicts: - edi_record = edi.edi_import(record) + edi_record = record.edi_import(record) return True - def deserialize(self,edi_document_string): + def deserialize(self,cr,uid,edi_document_string,context=None): """ Deserialized the edi document string""" deserialize_string = json.loads(edi_document_string) - return self.load_edi(deserialize_string) + return self.load_edi(cr,uid,deserialize_string,context=context) - def export_doc(self, cr, uid, ids,browse_records,database_id,context=None): + def export_doc(self, cr, uid, browse_records, database_id, context=None): """the method handles the flow of the edi document generation and store it in the database and return the edi_token of the particular document""" @@ -129,10 +128,10 @@ class ir_edi_document(osv.osv): edi_loads method """ if edi_document is not None: - self.deserialize(edi_document) + self.deserialize(cr,uid,edi_document,context=context) elif edi_url is not None: edi_document = urllib2.urlopen(edi_url).read() - self.deserialize(edi_document) + self.deserialize(cr,uid,edi_document,context=context) return True @@ -149,7 +148,7 @@ class edi(object): specific behavior, based on the primitives provided by this superclass.""" - def edi_metadata(self, cr, uid, ids, browse_rec_list, context=None): + def edi_metadata(self, cr, uid,browse_rec_list, context=None): """Return a list representing the boilerplate EDI structure for exporting the record in the given browse_rec_list, including the metadata fields""" @@ -162,14 +161,18 @@ class edi(object): self.edi_document['__module'] = data.module if data.name is not None: self.edi_document['__id'] = context['active_id']+':'+data.name + else: + self.edi_document['__id'] = context['active_id']+':'+safe_unique_id(data.model,) self.edi_document['__last_updata'] = context['__last_update'].values()[0] return True - def edi_m2o(self, cr, uid, ids, browse_rec,model=None,context=None): + def edi_m2o(self, cr, uid,browse_rec): """Return a list representing a M2O EDI value for the given browse_record.""" # generic implementation! + + model = browse_rec._name res = [] data_object = self.pool.get('ir.model.data') uu_id = data_model.search(cr,uid,[('model','=',model)]) @@ -183,23 +186,26 @@ class edi(object): if xml_ID is not None: res.append(safe_unique_id(model,data.id)+':'+xml_ID) else: - res.append(safe_unique_id(model,data.id)+':'+model+'-'+safe_unique_id(model,data.id)) + res.append(safe_unique_id(model,data.id)+':'+safe_unique_id(model,data.id)) res.append(data.name) return res - def edi_o2m(self, cr, uid, ids, browse_rec_list, model=None, context=None): + def edi_o2m(self, cr, uid,line = None,browse_rec_list, context=None): """Return a list representing a O2M EDI value for the browse_records from the given browse_record_list.""" # generic implementation! res = [] init_data = {} + model = browse_rec_list._name data_object = self.pool.get('ir.model.data') uu_id = data_model.search(cr,uid,[('model','=',model)]) + for xml in data_model.browse(cr,uid,uu_id): if xml: xml_ID = xml.name else: xml_ID = None + data_model = self.pool.get(model) for record in browse_rec_list: if xml_ID is not None: @@ -207,14 +213,16 @@ class edi(object): else: init_data['__id'] = safe_unique_id(model,record.id)+':'+model+'-'+safe_unique_id(model,record.id) init_data['__last_update'] =record.write_date - init_data.update(datamodel.read(cr,uid,[record],[],context=context)[0]) + init_data.update(self.determine_type(cr,uid,ids,line,record,context=context)) + res.append(init_data) return res - def edi_m2m(self, cr, uid, ids, browse_rec_list,model=None, context=None): + def edi_m2m(self, cr, uid, line = None, browse_rec_list, context=None): """Return a list representing a M2M EDI value for the browse_records from the given browse_record_list.""" # generic implementation! + model = browse_rec_list[0]._name res = [] init_data = [] data_object = self.pool.get('ir.model.data') @@ -271,20 +279,43 @@ class edi(object): fields_object = self.pool.get('ir.model.fields') model_object = self.pool.get(self.edi_document['__model']) - edi_metadata(context=context) - for field in self.edi_struct.keys(): + edi_metadata(edi_struct,context=context) + record = model_object.read(cr,uid,ids,[],context=context) + for field in edi_struct.keys(): f_ids = fields_object.search(cr,uid,[('name','=',field),('model','=',self.edi_document['__model'])],context=context) + for fname in fields_object.browse(cr,uid,f_ids): if fname.ttype == 'many2one': - self.edi_document[field] = self.edi_m2o(edi_struct[field],fname.relation) + self.edi_document[field] = self.edi_m2o(cr, uid, browse_rec = record[0][field]) elif fname.ttype == 'one2many': - self.edi_ducument[field] = self.edi_o2m(edi_struct[field],fname.relation) + + self.edi_ducument[field] = self.edi_o2m(cr,uid,line = edi_struct[field],browse_rec_list = record[0][field]) elif fname.ttype == 'many2many': - self.edi_document[field] = self.edi_m2m(edi_struct[field],fname.relation) + self.edi_document[field] = self.edi_m2m(cr,uid,line = edi_struct[field],browse_rec_list = record[0][field]) else: - self.edi_document[field] = model_object.read(cr,uid,[context['active_id']],[field],context=context) + self.edi_document[field] = record[0][field] + + return self.edi_document + def determine_type(self,cr,uid,ids,record_line,browse_record=None,context=None): + model = browse_record._name + fields_object = self.pool.get('ir.model.fields') + obj = self.pool.get(model) + field_data = obj.read(cr,uid,browse_record.id,record_line.keys())[0] + for field in field_data.keys(): + field_ids = field_object.search(cr,uid,[field]) + for field_type in field_object.browse(cr,uid,field_ids): + if field_type.ttype == 'many2one': + record_line.update(field,self.edi_m2o(cr,uid,browse_rec = field_data[field])) + elif field_type.ttype == 'one2many': + pass + elif field_type.ttype == 'many2many': + pass + else: + record_line.update(field,field_data[field]) + return record_line + def edi_import(self, cr, uid, edi_document, context=None): """Imports a list of dicts representing an edi.document, using the From 1e3ae46ec7a48a80b8625ae18aa654c936a8876c Mon Sep 17 00:00:00 2001 From: "Amit Parmar (OpenERP)" Date: Thu, 2 Jun 2011 19:12:18 +0530 Subject: [PATCH 006/526] [EDI]: developing edi_import method bzr revid: aar@tinyerp.com-20110602134218-32aolwbdbwkp63qp --- openerp/addons/base/ir/ir_edi.py | 78 ++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index a87453fd277..d5146b4c655 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -240,7 +240,9 @@ class edi(object): init_data.append(safe_unique_id(model,record.id)+':'+model+'-'+safe_unique_id(model,record.id)) if record.name is not None: init_data.append('name:'+record.name) - res.append(init_data) + res.append(init_data) + + # blob: return res def edi_export(self, cr, uid, ids, edi_struct=None, context=None): @@ -279,7 +281,7 @@ class edi(object): fields_object = self.pool.get('ir.model.fields') model_object = self.pool.get(self.edi_document['__model']) - edi_metadata(edi_struct,context=context) + edi_metadata(cr, uid, edi_struct, context=context) record = model_object.read(cr,uid,ids,[],context=context) for field in edi_struct.keys(): f_ids = fields_object.search(cr,uid,[('name','=',field),('model','=',self.edi_document['__model'])],context=context) @@ -322,6 +324,76 @@ class edi(object): generic algorithm. """ # generic implementation! + new = True + resource = {} + model = edi_document['__model'] + model_object = self.pool.get(model) + data_model = self.pool.get('ir.model.data') + field_model = self.pool.get('ir.model.fields') for field in edi_document.keys(): + if field == '__id': + db_id , xml_ID = edi_document[field].split(':') + self.data_ids = data_model.search(cr,uid,[('name','=',xml_ID)]) - pass + + f_ids = fields_object.search(cr,uid,[('name','=',field),('model','=',model)],context=context) + for fname in fields_object.browse(cr,uid,f_ids): + + if fname.ttype == 'many2one': + field_ids,field_xml = edi_document[field][0].split(':') + field_ids = int(field_ids) + model_ids = data_model.search(cr,uid,[('name','=',field_xml)]) + if not len(model_ids): + obj = self.pool.get(fname.relation) + model1_ids = obj.search(cr,uid,[('name','=',edi_document[field][1])]) + if len(model1_ids) == 1: + resourse[field] = data_model.create(cr,uid,{'module':'edi_import','name':field_xml,'res_id':model1_ids})[0] + + elif not len(model1_ids) or len(model1_ids) > 1: + edi_document[field][1] = obj.create(cr,uid,{'name':edi_document[field][1]}) + resource[field] = data_model.create(cr,uid,{'module':'edi_import','name':field_xml,'res_id':edi_document[field][0]})[0] + else: + resource[field] = model_ids[0] + + elif fname.ttype == 'one2many': + for records in edi_document[field]: + res = {} + field_ids,field_xml = records['__id'].split(':') + field_ids = int(field_ids) + model3_ids = data_model.search(cr,uid,[('name','=',field_xml)]) + for fields in records.keys(): + if fields != '__id' or fields != '__last_update': + res[fields] = records[fields] + if not len(model3_ids): + resource[field][records] = (4,self.edi_import(cr,uid,res,context = context)[0]) + else: + resource[field][records] = (4,model3_ids[0])obj = self.pool.get(model) + record_ids = obj.create(cr,uid,resource,context=context) + return record_ids + + elif fname.ttype == 'many2many': + for records in edi_document[field]: + field_ids,field_xml = records[0].split(':') + field_ids = int(field_ids) + model4_ids = data_model.search(cr,uid,[('name','=',field_xml)]) + if not len(model4_ids): + obj = self.pool.get(fname.relation) + name,value = records[1].split(':') + model5_ids = obj.search(cr,uid,[(name,'=',value)]) + if len(model5_ids) == 1: + resource[field][records] = data_model.create(cr,uid,{'module':'edi_import','name':field_xml,'res_id':model5_ids})[0] + elif not len(model5_ids) or len(model5_ids) > 1: + edi_document[field][records][1] = obj.create(cr,uid,{'name':edi_document[field][1]}) + resource[field] = (4,data_model.create(cr,uid,{'module':'edi_import','name':field_xml,'res_id':edi_document[field][records][1]})[0]) + else: + resource[field] = (4,model4_ids[0]) + else: + resource[field] = edi_document[field] + if len(self.data_ids): + obj = self.pool.get(model) + record_ids = obj.write(cr,uid,resource,context=context) + return record_ids + else: + obj = self.pool.get(model) + record_ids = obj.create(cr,uid,resource,context=context) + return record_ids From f1ce7642fbf5edcb1cb392f4a7a1feb0a3725888 Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Fri, 3 Jun 2011 12:59:50 +0530 Subject: [PATCH 007/526] [REVIEW] edi: optimize code bzr revid: hmo@tinyerp.com-20110603072950-441p4kd4vyvzpo7a --- openerp/addons/base/ir/ir_edi.py | 344 +++++++++++++++++-------------- 1 file changed, 190 insertions(+), 154 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index d4d3692203d..2a0c9282de9 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -27,10 +27,24 @@ import base64 import urllib2 def safe_unique_id(model, database_id): - """Generate a unique string to represent a (model,database id) pair without being too long, without revealing the database id, and with a very low probability of collisions. + + Each EDI record and each relationship value are represented using a unique + database identifier. These database identifiers include the database unique + ID, as a way to uniquely refer to any record within any OpenERP instance, + without conflict. + + For OpenERP records that have an existing "XML ID" (i.e. an entry in + ir.model.data), the EDI unique identifier for this record will be made of + "%s:%s" % (the database's UUID, the XML ID). The database's UUID MUST + NOT contain a colon characters (this is guaranteed by the UUID algorithm). + + For OpenERP records that have no existing "XML ID", a new one should be + created during the EDI export. It is recommended that the generated XML ID + contains a readable reference to the record model, plus a unique value that + hides the database ID. """ msg = "%s-%s-%s" % (model, time.time(), database_id) digest = hashlib.sha1(msg).digest() @@ -42,10 +56,10 @@ def safe_unique_id(model, database_id): class ir_edi_document(osv.osv): _name = 'ir.edi.document' - _description = 'ir_edi_document' + _description = 'To represent the EDI export of any OpenERP record, it could be an invoice, a project task, etc. _columns = { - 'name': fields.char("EDI token", size = 128), - 'document': fields.text("Document") + 'name': fields.char("EDI token", size = 128, help="EDI Token is a unique identifier for the EDI document."), + 'document': fields.text("Document", help="hold the serialization of the EDI document.") } _sql_constraints = [ @@ -53,90 +67,115 @@ class ir_edi_document(osv.osv): ] - def new_edi_token(self,browse_records,database_id): - - """Generate new unique tokent value to uniquely identify each edi_document""" - - db_uuid = safe_unique_id(browse_records, database_id) - edi_token = hashlib.sha256('%s-%s-%s' % (time.time(),db_uuid,time.time())).hexdigest() - - + def new_edi_token(self, record): + """ + Return a new, random unique token to identify an edi.document + :param record: It's a object of browse_record of any model + """ + db_uuid = safe_unique_id(record._name, record.id) + edi_token = hashlib.sha256('%s-%s-%s' % (time.time(), db_uuid, time.time())).hexdigest() return edi_token - def serialize(self,edi_list_of_dicts): - - """Serialize the list of dictionaries using json dumps method""" - - serialized_list = json.dumps(edi_list_of_dicts) + def serialize(self, edi_documents): + """Serialize the list of dictionaries using json dumps method + perform a JSON serialization of a list of dicts prepared by generate_edi() and return a UTF-8 encoded string that could be passed to deserialize() + :param edi_dicts: it's list of edi_dict + """ + serialized_list = json.dumps(edi_dicts) return serialized_list - def generate_edi(self,browse_record_list): - - """ Generate the list of dictionaries using edi_export method of edi class """ - + def generate_edi(self, cr, uid, records, context=None): + """ + Generate the list of dictionaries using edi_export method of edi class + :param records: it's a object of browse_record_list of any model + """ edi_list = [] - for record in browse_record_list: - edi_struct = record.edi_export(record) + for record in record_list: + edi_struct = record.edi_export(cr, uid, record, context=context) edi_list.append(edi_struct) return self.serialize(edi_list) - def get_document(self,cr,uid,edi_token,context=None): + def get_document(self, cr, uid, edi_token, context=None): + """ + Get the edi document from database using given edi token + returns the string serialization that is in the database (column: document) for the given edi_token or raise. + """ - """Get the edi document from database using given edi token """ - - records = self.search(cr,uid,[('name','=',edi_token)],context=context) + records = self.name_search(cr, uid, edi_token, context=context) if records: - doc_string = self.read(cr,uid,records,['document'],context=context) + edi = self.browse(cr, uid, records[0], context=context) + return edi.document else: raise osv.except_osv(_('Error !'), _('Desired EDI Document does not Exist')) - return doc_string + - def load_edi(self,cr,uid,ids,edi_list_of_dicts,context=None): - - """loads the values from list of dictionaries to the corresponding OpenERP records - using the edi_import method of edi class""" - - edi_list = [] - for record in edi_list_of_dicts: - edi_record = record.edi_import(record) - - - return True - - def deserialize(self,cr,uid,edi_document_string,context=None): - """ Deserialized the edi document string""" - - deserialize_string = json.loads(edi_document_string) - return self.load_edi(cr,uid,deserialize_string,context=context) - - def export_doc(self, cr, uid, browse_records, database_id, context=None): - """the method handles the flow of the edi document generation and store it in - the database and return the edi_token of the particular document""" - - edi_document = { - 'name': self.new_edi_token(browse_records,database_id), - 'document': self.generate_edi(browse_records) - } - - unique_id = self.create(cr,uid,edi_document,context=context) - return edi_document['name'] - - def import_doc(self, cr, uid, ids, edi_document=None,edi_url=None,context=None): - - """the method handles the flow of importing particular edi document and - updates the database values on the basis of the edi document using - edi_loads method """ - - if edi_document is not None: - self.deserialize(cr,uid,edi_document,context=context) - elif edi_url is not None: - edi_document = urllib2.urlopen(edi_url).read() - self.deserialize(cr,uid,edi_document,context=context) + def load_edi(self, cr, uid, edi_documents, context=None): + """ + loads the values from list of dictionaries to the corresponding OpenERP records + using the edi_import method of edi class + For each edi record (dict) in the list, call the corresponding osv.edi_import() method, based on the __model attribute (see section 2.4 of for spec of + osv.edi_import) + :param edi_dicts: list of edi_dict + """ + + for edi_document in edi_documents: + model = edi_document.get('__model') + assert model, _('model should be provided in EDI Dict') + model_obj = self.pool.get(model) + model_obj.edi_import(cr, uid, edi_document, context=context) return True + def deserialize(self, edi_document_string): + """ Deserialized the edi document string + perform JSON deserialization from an edi document string, and returns a list of dicts + """ + edi_document = json.loads(edi_document_string) + return edi_document + + def export(self, cr, uid, records, context=None): + """ + The method handles the flow of the edi document generation and store it in + the database and return the edi_token of the particular document + Steps: + * call generate_edi() to get a serialization and new_edi_token() to get a unique ID + * serialize the list returned by generate_edi() using serialize(), and save it in database with unique ID. + * return the unique ID + + : param records: list of browse_record of any model + """ + exported_ids = [] + for record in records: + document = self.generate_edi(cr, uid, [record], context) + token = self.new_edi_token(record) + self.create(cr, uid, { + 'name': token, + 'document': document + }, context=context) + + exported_ids.append(token) + return exported_ids + + def import(self, cr, uid, edi_document=None, edi_url=None, context=None): + """ + The method handles the flow of importing particular edi document and + updates the database values on the basis of the edi document using + edi_loads method + + * N: a serialized edi.document or the URL to download a serialized document + * If a URL is provided, download it first to get the document + * Calls deserialize() to get the resulting list of dicts from the document + * Call load_edi() with the list of dicts, to create or update the corresponding OpenERP records based on the edi.document. + """ + + if edi_url and not edi_document: + edi_document = urllib2.urlopen(edi_url).read() + assert edi_document, _('EDI Document should be provided') + edi_documents = self.deserialize(edi_document) + return self.load_edi(cr, uid, edi_documents, context=context) + ir_edi_document() @@ -150,102 +189,99 @@ class edi(object): specific behavior, based on the primitives provided by this superclass.""" - def edi_metadata(self, cr, uid,browse_rec_list, context=None): + def edi_metadata(self, cr, uid, records, context=None): """Return a list representing the boilerplate EDI structure for exporting the record in the given browse_rec_list, including - the metadata fields""" - # generic implementation! - data_object = self.pool.get('ir.model.data') + the metadata fields - if self.edi_document['__model'] is not None: - model_ids = data_object.search(cr,uid,[('model','=',self.edi_document['__model'])],context=context) - for data in data_object.browse(cr,uid,model_ids): - self.edi_document['__module'] = data.module - if data.name is not None: - self.edi_document['__id'] = context['active_id']+':'+data.name - else: - self.edi_document['__id'] = context['active_id']+':'+safe_unique_id(data.model,) - self.edi_document['__last_updata'] = context['__last_update'].values()[0] + The metadata fields MUST always include: + - __model': the OpenERP model name + - __module': the OpenERP module containing the model + - __id': the unique (cross-DB) identifier for this record + - __last_update': last update date of record, ISO date string in UTC + - __version': a list of components for the version + - __attachments': a list (possibly empty) of dicts describing the files attached to this record. + """ + # generic implementation! + attachment_object = self.pool.get('ir.attachment') + edi_dict_list = [] + for record in records: + attachment_ids = attachment_object.search(cr, uid, [('res_model','=', record._name), ('res_id', '=', record.id)]) + attachment_dict_list = [] + for attachment in attachment_object.browse(cr, uid, attachment_ids, context=context): + attachment_dict_list.append({ + 'name' : attachment.name, + 'content': base64.encodestring(attachment.datas), + 'file_name': attachment.datas_fname, + }) + edi_dict = { + '__model' : record._name, + '__module' : record._module, + '__id': record.id, + '__last_update': record.write_date, #TODO: convert into UTC + '__version': [], # ? + '__attachments': attachment_dict_list + } + edi_dict_list.append(edi_dict) - return True + return edi_dict_list - def edi_m2o(self, cr, uid,browse_rec): + def edi_m2o(self, cr, uid, record, context=None): """Return a list representing a M2O EDI value for - the given browse_record.""" + the given browse_record. + M2O are passed as pair (ID, Name) + Exmaple: ['db-uuid:xml-id', 'Partner Name'] + """ # generic implementation! - - model = browse_rec._name - res = [] data_object = self.pool.get('ir.model.data') - uu_id = data_model.search(cr,uid,[('model','=',model)]) - for xml in data_model.browse(cr,uid,uu_id): - if xml: - xml_ID = xml.name - else: - xml_ID = None - for data in browse_rec: - if data.name is not None: - if xml_ID is not None: - res.append(safe_unique_id(model,data.id)+':'+xml_ID) - else: - res.append(safe_unique_id(model,data.id)+':'+safe_unique_id(model,data.id)) - res.append(data.name) - return res + db_uuid = safe_unique_id(record._name, record.id) + xml_ids = data_object.search(cr, uid, [('model','=',record._name),('res_id','=',record.id)]) + if xml_ids: + xml_id = data_object.browse(cr, uid, xml_ids[0], context=context) + xml_id = xml_id.name + db_uuid = '%s:%s' %(db_uuid, xml_id) + name = record.name + return [db_uuid, name] + - def edi_o2m(self, cr, uid,line = None,browse_rec_list, context=None): + def edi_o2m(self, cr, uid, records, context=None): """Return a list representing a O2M EDI value for - the browse_records from the given browse_record_list.""" - # generic implementation! - res = [] - init_data = {} - model = browse_rec_list._name - data_object = self.pool.get('ir.model.data') - uu_id = data_model.search(cr,uid,[('model','=',model)]) - - for xml in data_model.browse(cr,uid,uu_id): - if xml: - xml_ID = xml.name - else: - xml_ID = None - - data_model = self.pool.get(model) - for record in browse_rec_list: - if xml_ID is not None: - init_data['__id'] = safe_unique_id(model,record.id)+':'+xml_ID - else: - init_data['__id'] = safe_unique_id(model,record.id)+':'+model+'-'+safe_unique_id(model,record.id) - init_data['__last_update'] =record.write_date - init_data.update(self.determine_type(cr,uid,ids,line,record,context=context)) - - res.append(init_data) - return res + the browse_records from the given browse_record_list. - def edi_m2m(self, cr, uid, line = None, browse_rec_list, context=None): - """Return a list representing a M2M EDI value for - the browse_records from the given browse_record_list.""" + Example: + [ # O2M fields would be a list of dicts, with their + { '__id': 'db-uuid:xml-id', # own __id. + '__last_update': 'iso date', # The update date, just in case... + 'name': 'some name', + ... + }], + """ # generic implementation! - model = browse_rec_list[0]._name - res = [] - init_data = [] - data_object = self.pool.get('ir.model.data') - uu_id = data_model.search(cr,uid,[('model','=',model)]) - for xml in data_model.browse(cr,uid,uu_id): - if xml: - xml_ID = xml.name - else: - xml_ID = None - data_model = self.pool.get(model) - for record in browse_rec_list: - if xml_ID is not None: - init_data.append(safe_unique_id(model,record.id)+':'+xml_ID) - else: - init_data.append(safe_unique_id(model,record.id)+':'+model+'-'+safe_unique_id(model,record.id)) - if record.name is not None: - init_data.append('name:'+record.name) - res.append(init_data) - - # blob: - return res + dict_list = [] + for record in records: + model_obj = self.pool.get(record._name) + dict_list += model_obj.edi_export(cr, uid, [record.id], context=context) + return dict_list + + + def edi_m2m(self, cr, uid, records, context=None): + """Return a list representing a M2M EDI value for + the browse_records from the given browse_record_list. + + Example: + 'related_tasks': [ # M2M fields would exported as a list of pairs, + ['db-uuid:xml-id1', # similar to a list of M2O values. + 'Task 01: bla bla'], + ['db-uuid:xml-id2', + 'Task 02: bla bla'] + ] + """ + # generic implementation! + dict_list = [] + for record in records: + dict_list.append(self.edi_o2m(cr, uid, record, context=None)) + + return dict_list def edi_export(self, cr, uid, ids, edi_struct=None, context=None): """Returns a list of dicts representing an edi.document containing the From ef7ee72f406efb58ea5c9863d4b3fa5d698f547a Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Fri, 3 Jun 2011 14:34:08 +0530 Subject: [PATCH 008/526] [REVIEW] edi: refactor code in import/export method bzr revid: hmo@tinyerp.com-20110603090408-fcumns7va18trdwx --- openerp/addons/base/ir/ir_edi.py | 235 +++++++++++++++---------------- 1 file changed, 112 insertions(+), 123 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 2a0c9282de9..c428bca7cb2 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -89,10 +89,8 @@ class ir_edi_document(osv.osv): Generate the list of dictionaries using edi_export method of edi class :param records: it's a object of browse_record_list of any model """ - edi_list = [] - for record in record_list: - edi_struct = record.edi_export(cr, uid, record, context=context) - edi_list.append(edi_struct) + + edi_list = self.edi_export(cr, uid, records, context=context) return self.serialize(edi_list) def get_document(self, cr, uid, edi_token, context=None): @@ -283,6 +281,8 @@ class edi(object): return dict_list + + def edi_export(self, cr, uid, ids, edi_struct=None, context=None): """Returns a list of dicts representing an edi.document containing the browse_records with ``ids``, using the generic algorithm. @@ -303,135 +303,124 @@ class edi(object): be included in the exported data. """ # generic implementation! - - self.edi_document = { - - '__model':'', - '__module':'', - '__id':'', - '__last_update':'', - '__version':'', - '__attachments':'', - - } - self.edi_document['__model'] = self._name + if context is None: + context = {} + _fields = self.fields_get(cr, uid, context=context) - fields_object = self.pool.get('ir.model.fields') - model_object = self.pool.get(self.edi_document['__model']) - edi_metadata(cr, uid, edi_struct, context=context) - record = model_object.read(cr,uid,ids,[],context=context) - for field in edi_struct.keys(): - f_ids = fields_object.search(cr,uid,[('name','=',field),('model','=',self.edi_document['__model'])],context=context) - - for fname in fields_object.browse(cr,uid,f_ids): - if fname.ttype == 'many2one': - - self.edi_document[field] = self.edi_m2o(cr, uid, browse_rec = record[0][field]) - elif fname.ttype == 'one2many': - - self.edi_ducument[field] = self.edi_o2m(cr,uid,line = edi_struct[field],browse_rec_list = record[0][field]) - elif fname.ttype == 'many2many': - self.edi_document[field] = self.edi_m2m(cr,uid,line = edi_struct[field],browse_rec_list = record[0][field]) + fields_to_export = edi_struct and edi_struct.keys() or _fields.key() + + edi_dict_list = [] + for row in self.browse(cr, uid, ids, fields_to_export, context): + edi_dict = {} + edi_dict.update(self.edi_metadata(cr, uid, [row.id], context=context)[0]) + for field in fields_to_export: + cols = self._fields[field] + if cols['type'] == 'many2one': + m2o_record = row.getattrs(field) + value = self.m2o(cr, uid, m2o_record, context=context) + elif cols['type'] == 'many2many': + m2m_records = row.getattrs(field) + value = self.m2m(cr, uid, m2m_records, context=context) + elif cols['type'] == 'one2many': + o2m_records = row.getattrs(field) + value = self.o2m(cr, uid, o2m_records, context=context) else: - self.edi_document[field] = record[0][field] + value = row.getattrs(field) + edi_dict[field] = value + edi_dict_list.append(edi_dict) + return edi_dict_list - - return self.edi_document - def determine_type(self,cr,uid,ids,record_line,browse_record=None,context=None): - model = browse_record._name - fields_object = self.pool.get('ir.model.fields') - obj = self.pool.get(model) - field_data = obj.read(cr,uid,browse_record.id,record_line.keys())[0] - for field in field_data.keys(): - field_ids = field_object.search(cr,uid,[field]) - for field_type in field_object.browse(cr,uid,field_ids): - if field_type.ttype == 'many2one': - record_line.update(field,self.edi_m2o(cr,uid,browse_rec = field_data[field])) - elif field_type.ttype == 'one2many': - pass - elif field_type.ttype == 'many2many': - pass - else: - record_line.update(field,field_data[field]) - return record_line def edi_import(self, cr, uid, edi_document, context=None): """Imports a list of dicts representing an edi.document, using the generic algorithm. + + All relationship fields are exported in a special way, and provide their own + unique identifier, so that we can avoid duplication of records when importing. + Note: for all ir.model.data entries, the "module" value to use for read/write + should always be "edi_import", and the "name" value should be the full + db_id provided in the EDI. + + 1: Many2One + M2O fields are always exported as a pair [db_id, name], where db_id + is in the form "db_uuid:xml_id", both being values that come directly + from the original database. + The import should behave like this: + a. Look in ir.model.data for a record that matches the db_id. + If found, replace the m2o value with the correct database ID and stop. + If not found, continue to next step. + b. Perform name_search(name) to look for a record that matches the + given m2o name. If only one record is found, create the missing + ir.model.data record to link it to the db_id, and the replace the m2o + value with the correct database ID, then stop. If zero result or + multiple results are found, go to next step. + c. Create the new record using the only field value that is known: the + name, and create the ir.model.data entry to map to it. + This should work for many models, and if not, the module should + provide a custom EDI import logic to care for it. + + 2: One2Many + O2M fields are always exported as a list of dicts, where each dict corresponds + to a full EDI record. The import should not update existing records + if they already exist, it should only link them to the parent object. + a. Look for a record that matches the db_id provided in the __id field. If + found, keep the corresponding database id, and connect it to the parent + using a write value like (4,db_id). + b. If not found via db_id, create a new entry using the same method that + imports a full EDI record (recursive call!), grab the resulting db id, + and use it to connect to the parent via a write value like (4, db_id). + + 3: Many2Many + M2M fields are always exported as a list of pairs similar to M2O. + For each pair in the M2M: + a. Perform the same steps as for a Many2One (see 1.2.1.1) + b. After finding the database ID of the final record in the database, + connect it to the parent record via a write value like (4, db_id). """ # generic implementation! - new = True - resource = {} - model = edi_document['__model'] - model_object = self.pool.get(model) - data_model = self.pool.get('ir.model.data') - field_model = self.pool.get('ir.model.fields') + fields = edi_document.keys() + fields_to_import = [] + data_line = [] + _fields = self.fields_get(cr, uid, context=context) for field in edi_document.keys(): - if field == '__id': - db_id , xml_ID = edi_document[field].split(':') - self.data_ids = data_model.search(cr,uid,[('name','=',xml_ID)]) - - - f_ids = fields_object.search(cr,uid,[('name','=',field),('model','=',model)],context=context) - for fname in fields_object.browse(cr,uid,f_ids): - - if fname.ttype == 'many2one': - field_ids,field_xml = edi_document[field][0].split(':') - field_ids = int(field_ids) - model_ids = data_model.search(cr,uid,[('name','=',field_xml)]) - if not len(model_ids): - obj = self.pool.get(fname.relation) - model1_ids = obj.search(cr,uid,[('name','=',edi_document[field][1])]) - if len(model1_ids) == 1: - resourse[field] = data_model.create(cr,uid,{'module':'edi_import','name':field_xml,'res_id':model1_ids})[0] - - elif not len(model1_ids) or len(model1_ids) > 1: - edi_document[field][1] = obj.create(cr,uid,{'name':edi_document[field][1]}) - resource[field] = data_model.create(cr,uid,{'module':'edi_import','name':field_xml,'res_id':edi_document[field][0]})[0] - else: - resource[field] = model_ids[0] - - elif fname.ttype == 'one2many': - for records in edi_document[field]: - res = {} - field_ids,field_xml = records['__id'].split(':') - field_ids = int(field_ids) - model3_ids = data_model.search(cr,uid,[('name','=',field_xml)]) - for fields in records.keys(): - if fields != '__id' or fields != '__last_update': - res[fields] = records[fields] - if not len(model3_ids): - resource[field][records] = (4,self.edi_import(cr,uid,res,context = context)[0]) - else: - resource[field][records] = (4,model3_ids[0])obj = self.pool.get(model) - record_ids = obj.create(cr,uid,resource,context=context) - return record_ids - - elif fname.ttype == 'many2many': - for records in edi_document[field]: - field_ids,field_xml = records[0].split(':') - field_ids = int(field_ids) - model4_ids = data_model.search(cr,uid,[('name','=',field_xml)]) - if not len(model4_ids): - obj = self.pool.get(fname.relation) - name,value = records[1].split(':') - model5_ids = obj.search(cr,uid,[(name,'=',value)]) - if len(model5_ids) == 1: - resource[field][records] = data_model.create(cr,uid,{'module':'edi_import','name':field_xml,'res_id':model5_ids})[0] - elif not len(model5_ids) or len(model5_ids) > 1: - edi_document[field][records][1] = obj.create(cr,uid,{'name':edi_document[field][1]}) - resource[field] = (4,data_model.create(cr,uid,{'module':'edi_import','name':field_xml,'res_id':edi_document[field][records][1]})[0]) - else: - resource[field] = (4,model4_ids[0]) + if not field.startwith('__'): + field_to_import.append(field) + edi_field_value = edi_document[field] + if _fields[field]['type'] == 'many2one': + #Look in ir.model.data for a record that matches the db_id. + #If found, replace the m2o value with the correct database ID and stop. + #If not found, continue to next step. + + #Perform name_search(name) to look for a record that matches the + #given m2o name. If only one record is found, create the missing + #ir.model.data record to link it to the db_id, and the replace the m2o + #value with the correct database ID, then stop. If zero result or + #multiple results are found, go to next step. + + #Create the new record using the only field value that is known: the + #name, and create the ir.model.data entry to map to it. + #This should work for many models, and if not, the module should + #provide a custom EDI import logic to care for it. + pass + elif _fields[field]['type'] == 'one2many': + #Look for a record that matches the db_id provided in the __id field. If + #found, keep the corresponding database id, and connect it to the parent + #using a write value like (4,db_id). + + #If not found via db_id, create a new entry using the same method that + #imports a full EDI record (recursive call!), grab the resulting db id, + #and use it to connect to the parent via a write value like (4, db_id). + elif _fields[field]['type'] == 'many2many': + #Perform the same steps as for a Many2One (see 1.2.1.1) + + + + #After finding the database ID of the final record in the database, + #connect it to the parent record via a write value like (4, db_id). else: - resource[field] = edi_document[field] - if len(self.data_ids): - obj = self.pool.get(model) - record_ids = obj.write(cr,uid,resource,context=context) - return record_ids - else: - obj = self.pool.get(model) - record_ids = obj.create(cr,uid,resource,context=context) - return record_ids + data_line.append(edi_field_value) + + return self.import_data(cr, uid, field_to_import, [data_line], mode='init', current_module='edi_import', noupdate=False, context=context, filename=None) + From eaec4a25f23e4182bc5620f1a9dcef664f811861 Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Fri, 3 Jun 2011 14:41:09 +0530 Subject: [PATCH 009/526] [REVIEW] edi: refactor code in generate_edi() bzr revid: hmo@tinyerp.com-20110603091109-e1gn7mr0gpcr88sy --- openerp/addons/base/ir/ir_edi.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index c428bca7cb2..1fc9371b66c 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -90,7 +90,10 @@ class ir_edi_document(osv.osv): :param records: it's a object of browse_record_list of any model """ - edi_list = self.edi_export(cr, uid, records, context=context) + edi_list = [] + for record in records: + record_model_obj = self.pool.get(record._name) + edi_list += record_model_obj.edi_export(cr, uid, [record], context=context) return self.serialize(edi_list) def get_document(self, cr, uid, edi_token, context=None): From 9cb3205a6432ccc7ba2458416786774b39bc50d6 Mon Sep 17 00:00:00 2001 From: "Yogesh (OpenERP)" Date: Fri, 3 Jun 2011 15:25:26 +0530 Subject: [PATCH 010/526] [FIX] fix problem of search multiple conditions O2M with active=False then not work. lp bug: https://launchpad.net/bugs/711919 fixed bzr revid: ysa@tinyerp.com-20110603095526-s26wgkahwc1d6zfd --- openerp/osv/expression.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 1922d5e8388..4c74a11506a 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -108,6 +108,10 @@ class expression(object): self.__main_table = table self.__all_tables.add(table) + active = False + for exp in self.__exp: + if exp[0] == 'active': + active = exp i = -1 while i + 1','<=','>='] ) and 'in' or operator - + dict_op = {'not in':'!=','in':'=','=':'in','!=':'not in','<>':'not in'} if isinstance(right,tuple): right = list(right) @@ -319,7 +326,7 @@ class expression(object): elif isinstance(right,(list,tuple)): m2o_str = True for ele in right: - if not isinstance(ele, basestring): + if not isinstance(ele, basestring): m2o_str = False break elif right == []: @@ -335,7 +342,7 @@ class expression(object): new_op = '!=' #Is it ok to put 'left' and not 'id' ? self.__exp[i] = (left,new_op,False) - + if m2o_str: self.__exp[i] = _get_expression(field_obj,cr, uid, left, right, operator, context=context) else: From 2a9a5e2a0e3fdf1bc104e1a5c923baf85f1d963b Mon Sep 17 00:00:00 2001 From: "Yogesh (OpenERP)" Date: Fri, 3 Jun 2011 15:42:17 +0530 Subject: [PATCH 011/526] Improve code. bzr revid: ysa@tinyerp.com-20110603101217-v69rcqtpcfics925 --- openerp/osv/expression.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openerp/osv/expression.py b/openerp/osv/expression.py index 4c74a11506a..8fb85ce7612 100644 --- a/openerp/osv/expression.py +++ b/openerp/osv/expression.py @@ -154,10 +154,10 @@ class expression(object): # Making search easier when there is a left operand as field.o2m or field.m2m if field._type in ['many2many','one2many']: right = field_obj.search(cr, uid, [(fargs[1], operator, right)], context=context) + domain = [(fargs[0],'in', right)] if active: - right1 = table.search(cr, uid, [(fargs[0],'in', right),active], context=context) - else: - right1 = table.search(cr, uid, [(fargs[0],'in', right)], context=context) + domain.append(active) + right1 = table.search(cr, uid, domain, context=context) if right1 == []: self.__exp[i] = ( 'id', '=', 0 ) else: From 332d385390a48cd82670182c6b76e92bbf35397e Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Fri, 3 Jun 2011 16:15:39 +0530 Subject: [PATCH 012/526] [REVIEW] edi: refactor code of edi_import bzr revid: hmo@tinyerp.com-20110603104539-nh0i45zv6zna2wau --- openerp/addons/base/ir/ir_edi.py | 96 ++++++++++++++++++++++++-------- 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 1fc9371b66c..35b1b411a38 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -386,44 +386,92 @@ class edi(object): fields = edi_document.keys() fields_to_import = [] data_line = [] + model_data = self.pool.get('ir.model.data') _fields = self.fields_get(cr, uid, context=context) + current_module = 'edi_import' + values = {} for field in edi_document.keys(): if not field.startwith('__'): field_to_import.append(field) edi_field_value = edi_document[field] - if _fields[field]['type'] == 'many2one': - #Look in ir.model.data for a record that matches the db_id. - #If found, replace the m2o value with the correct database ID and stop. - #If not found, continue to next step. + if _fields[field]['type'] in ('many2one', 'many2many'): - #Perform name_search(name) to look for a record that matches the - #given m2o name. If only one record is found, create the missing - #ir.model.data record to link it to the db_id, and the replace the m2o - #value with the correct database ID, then stop. If zero result or - #multiple results are found, go to next step. + if _fields[field]['type'] == 'many2one': + edi_parent_documents = [edi_field_value] + else: + edi_parent_documents = edi_field_value - #Create the new record using the only field value that is known: the - #name, and create the ir.model.data entry to map to it. - #This should work for many models, and if not, the module should - #provide a custom EDI import logic to care for it. - pass + parent_lines = [] + + for edi_parent_document in edi_parent_documents: + #Look in ir.model.data for a record that matches the db_id. + #If found, replace the m2o value with the correct database ID and stop. + #If not found, continue to next step + + db_uuid, xml_id = tuple(edi_parent_document[0].split(':')) + data_ids = model_data.name_search(cr, uid, xml_id) + if data_ids: + d = model_data.browse(cr, uid, data_ids[0], context=context) + parent_lines.append(d.res_id) + + else: + #Perform name_search(name) to look for a record that matches the + #given m2o name. If only one record is found, create the missing + #ir.model.data record to link it to the db_id, and the replace the m2o + #value with the correct database ID, then stop. If zero result or + #multiple results are found, go to next step. + #Create the new record using the only field value that is known: the + #name, and create the ir.model.data entry to map to it. + + relation_object = self.get.pool(_fields[field]['relation']) + relation_ids = relation_object.name_search(cr, uid, edi_parent_document[1]) + if relation_ids and len(relation_ids) == 1: + relation_id = relation_ids[0] + else: + relation_id = relation_object.create(cr, uid, {'name': edi_parent_document[1]}, context=context) + model_data.create(cr, uid, { + 'name': xml_id, + 'model': relation_object._name, + #'module':relation_object._module, + 'res_id':relation_id, + },context=context) + + parent_lines.append(relation_id) + + if _fields[field]['type'] == 'many2one': + values[field] = parent_lines[0] + else: + many2many_ids = [] + for m2m_id in parent_lines: + many2many_ids.append((4,m2m_id)) + values[field] = many2many_ids + + elif _fields[field]['type'] == 'one2many': #Look for a record that matches the db_id provided in the __id field. If #found, keep the corresponding database id, and connect it to the parent #using a write value like (4,db_id). - #If not found via db_id, create a new entry using the same method that #imports a full EDI record (recursive call!), grab the resulting db id, #and use it to connect to the parent via a write value like (4, db_id). - elif _fields[field]['type'] == 'many2many': - #Perform the same steps as for a Many2One (see 1.2.1.1) - - - - #After finding the database ID of the final record in the database, - #connect it to the parent record via a write value like (4, db_id). + + relations = [] + relation_object = self.get.pool(_fields[field]['relation']) + for edi_relation_document in edi_field_value: + db_uuid, xml_id = tuple(edi_relation_document.get('__id').split(':')) + data_ids = model_data.name_search(cr, uid, xml_id) + if data_ids: + d = model_data.browse(cr, uid, data_ids[0], context=context) + relations.append(d.res_id) + else: + r = relation_object.edi_import(cr, uid, edi_relation_document, context=context) + relations.append(r) + one2many_ids = [] + for o2m_id in relations: + one2many_ids.append((4,o2m_id)) + values[field] = one2many_ids else: - data_line.append(edi_field_value) + values[field] = edi_field_value - return self.import_data(cr, uid, field_to_import, [data_line], mode='init', current_module='edi_import', noupdate=False, context=context, filename=None) + return model_data._update(cr, uid, self._name, current_module, values, context=context) From 023ce68e841d91ae3610bf50774f61b2be7de090 Mon Sep 17 00:00:00 2001 From: "Amit Parmar (OpenERP)" Date: Mon, 6 Jun 2011 11:22:57 +0530 Subject: [PATCH 013/526] [EDI]: worked on the local time to UTC time conversion bzr revid: aar@tinyerp.com-20110606055257-d33gwzaidwxufn64 --- openerp/addons/base/ir/ir_edi.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 4ed7b0a4d3a..2cb40c4dfa7 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -26,6 +26,7 @@ import time import base64 import urllib2 import release +import datetime def safe_unique_id(model, database_id): """Generate a unique string to represent a (model,database id) pair @@ -216,14 +217,21 @@ class edi(object): 'content': base64.encodestring(attachment.datas), 'file_name': attachment.datas_fname, }) + ''' Here we are obtaining the release version ''' version = [] for ver in release.version.split('.'): version.append(int(ver)) + ''' Here we are converting local time into UTC time''' + UTC_OFFSET_TIMEDELTA = datetime.datetime.utcnow() - datetime.datetime.now() + local_datetime = datetime.datetime.strptime(record.write_date, "%Y-%m-%d %H:%M:%S") + result_utc_datetime = local_datetime - UTC_OFFSET_TIMEDELTA + write_date = result_utc_datetime.strftime("%Y-%m-%d %H:%M:%S") + edi_dict = { '__model' : record._name, '__module' : record._module, '__id': record.id, - '__last_update': record.write_date, #TODO: convert into UTC + '__last_update': write_date, #TODO: convert into UTC '__version': version, '__attachments': attachment_dict_list } From 110483e93d4a6cf1f461002be93a607b4fb1ec65 Mon Sep 17 00:00:00 2001 From: "Yogesh (OpenERP)" Date: Wed, 8 Jun 2011 16:11:28 +0530 Subject: [PATCH 014/526] [FIX] fix problem of ir.property created if same value exists then record not create in ir.property object. lp bug: https://launchpad.net/bugs/726592 fixed bzr revid: ysa@tinyerp.com-20110608104128-7mdykmk0ovqenz6u --- openerp/osv/fields.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 306e7d007f3..ec1845344ab 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -1047,7 +1047,14 @@ class property(function): default_val = self._get_default(obj, cr, uid, prop_name, context) - if id_val is not default_val: + property_create = False + if hasattr(default_val, '_id'): + if default_val._id != id_val: + property_create = True + elif id_val != default_val: + property_create = True + + if property_create: def_id = self._field_get(cr, uid, obj._name, prop_name) company = obj.pool.get('res.company') cid = company._company_default_get(cr, uid, obj._name, def_id, From 4203df9c260fda5bcc5f2e60504483d1078b770c Mon Sep 17 00:00:00 2001 From: "Yogesh (OpenERP)" Date: Mon, 13 Jun 2011 11:40:41 +0530 Subject: [PATCH 015/526] [IMP] add yaml test case. bzr revid: ysa@tinyerp.com-20110613061041-15edtf67mj0oa8hq --- .../addons/base/test/test_osv_expression.yml | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/test/test_osv_expression.yml b/openerp/addons/base/test/test_osv_expression.yml index 4916fe06203..1f76a210293 100644 --- a/openerp/addons/base/test/test_osv_expression.yml +++ b/openerp/addons/base/test/test_osv_expression.yml @@ -40,7 +40,7 @@ - !assert {model: res.partner, search: "[('category_id', '=', False)]"}: - category_id in (False, None, []) -- +- Filtering on invalid value across x2many relationship should return an empty set - !assert {model: res.partner, search: "[('address.city','=','foo')]", count: 0, string: "Searching for address.city = foo should give empty results"} @@ -110,4 +110,21 @@ res_ids = self.search(cr, uid, [('company_id.partner_id', 'not in', [])]) res_ids.sort() assert res_ids == all_ids, "Searching against empty set failed, returns %r" % res_ids - +- + Testing for Many2Many field with category supplier and active=False +- + !python {model: res.partner }: | + vals = {'category_id': [(6, 0, [ref("base.res_partner_category_8")])], + 'name': 'OpenERP Test', + 'active': False, + 'address': [(0, 0, {'country_id': ref("base.be")})] + } + self.create(cr, uid, vals, context=context) + res_ids = self.search(cr, uid, [('category_id', 'ilike', 'supplier'), ('active', '=', False)]) + assert len(res_ids) != 0, "Record not Found with category supplier and active False." +- + Testing for One2Many field with country Belgium and active=False +- + !python {model: res.partner }: | + res_ids = self.search(cr, uid, [('address.country_id','=','Belgium'),('active','=',False)]) + assert len(res_ids) != 0, "Record not Found with country Belgium and active False." From ab62f7b7802571f56a56fce7389b777d6fee2583 Mon Sep 17 00:00:00 2001 From: "Amit Parmar (OpenERP)" Date: Tue, 21 Jun 2011 18:46:32 +0530 Subject: [PATCH 016/526] [EDI] revised edi class bzr revid: aar@tinyerp.com-20110621131632-rdwn6in74q5t1wkj --- openerp/addons/base/__openerp__.py | 3 +- openerp/addons/base/ir/ir_edi.py | 316 +++++++++++++++++------------ 2 files changed, 191 insertions(+), 128 deletions(-) diff --git a/openerp/addons/base/__openerp__.py b/openerp/addons/base/__openerp__.py index 75e0a8c56d0..b0c437352f9 100644 --- a/openerp/addons/base/__openerp__.py +++ b/openerp/addons/base/__openerp__.py @@ -71,7 +71,7 @@ 'res/partner/wizard/partner_clear_ids_view.xml', 'res/partner/wizard/partner_wizard_ean_check_view.xml', 'res/partner/partner_data.xml', - + #'test/test_edi_document.yml', 'res/ir_property_view.xml', 'security/base_security.xml', 'publisher_warranty/publisher_warranty_view.xml', @@ -96,6 +96,7 @@ 'test/test_ir_rule.yml', # <-- These tests modify/add/delete ir_rules. 'test/test_config_users.yml', 'test/test_ir_values.yml', + ], 'installable': True, 'active': True, diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 2cb40c4dfa7..fe8320491bf 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -20,14 +20,13 @@ ############################################################################## from osv import osv,fields -import json +import simplejson import hashlib import time import base64 import urllib2 -import release -import datetime - +import unicodedata +import openerp.release as release def safe_unique_id(model, database_id): """Generate a unique string to represent a (model,database id) pair without being too long, without revealing the database id, and @@ -58,7 +57,7 @@ def safe_unique_id(model, database_id): class ir_edi_document(osv.osv): _name = 'ir.edi.document' - _description = 'To represent the EDI export of any OpenERP record, it could be an invoice, a project task, etc.' + _description = 'To represent' _columns = { 'name': fields.char("EDI token", size = 128, help="EDI Token is a unique identifier for the EDI document."), 'document': fields.text("Document", help="hold the serialization of the EDI document.") @@ -83,7 +82,7 @@ class ir_edi_document(osv.osv): perform a JSON serialization of a list of dicts prepared by generate_edi() and return a UTF-8 encoded string that could be passed to deserialize() :param edi_dicts: it's list of edi_dict """ - serialized_list = json.dumps(edi_dicts) + serialized_list = simplejson.dumps(edi_documents) return serialized_list def generate_edi(self, cr, uid, records, context=None): @@ -95,7 +94,7 @@ class ir_edi_document(osv.osv): edi_list = [] for record in records: record_model_obj = self.pool.get(record._name) - edi_list += record_model_obj.edi_export(cr, uid, [record], context=context) + edi_list += record_model_obj.edi_export(cr, uid, [record],context=context) return self.serialize(edi_list) def get_document(self, cr, uid, edi_token, context=None): @@ -104,14 +103,12 @@ class ir_edi_document(osv.osv): returns the string serialization that is in the database (column: document) for the given edi_token or raise. """ - records = self.name_search(cr, uid, edi_token, context=context) + records = self.search(cr, uid, [('name','=',edi_token)]) if records: - edi = self.browse(cr, uid, records[0], context=context) - return edi.document - else: - raise osv.except_osv(_('Error !'), - _('Desired EDI Document does not Exist')) - + for edi in self.browse(cr, uid, records, context=context): + return edi.document + else: + pass def load_edi(self, cr, uid, edi_documents, context=None): @@ -135,10 +132,11 @@ class ir_edi_document(osv.osv): """ Deserialized the edi document string perform JSON deserialization from an edi document string, and returns a list of dicts """ - edi_document = json.loads(edi_document_string) + edi_document = simplejson.loads(edi_document_string) + return edi_document - def export(self, cr, uid, records, context=None): + def export_edi(self, cr, uid, records, context=None): """ The method handles the flow of the edi document generation and store it in the database and return the edi_token of the particular document @@ -161,7 +159,7 @@ class ir_edi_document(osv.osv): exported_ids.append(token) return exported_ids - def import(self, cr, uid, edi_document=None, edi_url=None, context=None): + def import_edi(self, cr, uid, edi_document=None, edi_url=None, context=None): """ The method handles the flow of importing particular edi document and updates the database values on the basis of the edi document using @@ -175,8 +173,10 @@ class ir_edi_document(osv.osv): if edi_url and not edi_document: edi_document = urllib2.urlopen(edi_url).read() - assert edi_document, _('EDI Document should be provided') - edi_documents = self.deserialize(edi_document) + #assert edi_document, _('EDI Document should be provided') + else: + + edi_documents = self.deserialize(edi_document) return self.load_edi(cr, uid, edi_documents, context=context) ir_edi_document() @@ -206,8 +206,11 @@ class edi(object): - __attachments': a list (possibly empty) of dicts describing the files attached to this record. """ # generic implementation! + model_data = self.pool.get('ir.model.data') + data_ids = [] attachment_object = self.pool.get('ir.attachment') edi_dict_list = [] + db_uuid = '' for record in records: attachment_ids = attachment_object.search(cr, uid, [('res_model','=', record._name), ('res_id', '=', record.id)]) attachment_dict_list = [] @@ -217,22 +220,24 @@ class edi(object): 'content': base64.encodestring(attachment.datas), 'file_name': attachment.datas_fname, }) - ''' Here we are obtaining the release version ''' version = [] - for ver in release.version.split('.'): - version.append(int(ver)) - ''' Here we are converting local time into UTC time''' - UTC_OFFSET_TIMEDELTA = datetime.datetime.utcnow() - datetime.datetime.now() - local_datetime = datetime.datetime.strptime(record.write_date, "%Y-%m-%d %H:%M:%S") - result_utc_datetime = local_datetime - UTC_OFFSET_TIMEDELTA - write_date = result_utc_datetime.strftime("%Y-%m-%d %H:%M:%S") + + for ver in release.major_version.split('.'): + version.append(ver) + data_ids = model_data.search(cr, uid, [('res_id','=',record.id)]) + + if len(data_ids): + xml_obj = model_data.browse(cr,uid,data_ids[0]) + uuid = safe_unique_id(record._name,record.id) + db_uuid = '%s:%s' % (uuid,xml_obj.name) + edi_dict = { '__model' : record._name, '__module' : record._module, - '__id': record.id, - '__last_update': write_date, #TODO: convert into UTC - '__version': version, + '__id': db_uuid, + '__last_update': False, #record.write_date, #TODO: convert into UTC + '__version': version, # ? '__attachments': attachment_dict_list } edi_dict_list.append(edi_dict) @@ -249,10 +254,15 @@ class edi(object): data_object = self.pool.get('ir.model.data') db_uuid = safe_unique_id(record._name, record.id) xml_ids = data_object.search(cr, uid, [('model','=',record._name),('res_id','=',record.id)]) + if xml_ids: xml_id = data_object.browse(cr, uid, xml_ids[0], context=context) xml_id = xml_id.name - db_uuid = '%s:%s' %(db_uuid, xml_id) + else: + xml_id = record._name + db_uuid = '%s:%s' %(db_uuid, xml_id) + + name = record.name return [db_uuid, name] @@ -271,9 +281,12 @@ class edi(object): """ # generic implementation! dict_list = [] + for record in records: + model_obj = self.pool.get(record._name) - dict_list += model_obj.edi_export(cr, uid, [record.id], context=context) + dict_list += model_obj.edi_export(cr, uid, [record], context=context) + return dict_list @@ -291,14 +304,15 @@ class edi(object): """ # generic implementation! dict_list = [] + for record in records: - dict_list.append(self.edi_o2m(cr, uid, record, context=None)) - + dict_list.append(self.edi_o2m(cr, uid, [record], context=None)) + return dict_list - def edi_export(self, cr, uid, ids, edi_struct=None, context=None): + def edi_export(self, cr, uid, records, edi_struct=None, context=None): """Returns a list of dicts representing an edi.document containing the browse_records with ``ids``, using the generic algorithm. :param edi_struct: if provided, edi_struct should be a dictionary @@ -318,32 +332,39 @@ class edi(object): be included in the exported data. """ # generic implementation! + if context is None: context = {} _fields = self.fields_get(cr, uid, context=context) - fields_to_export = edi_struct and edi_struct.keys() or _fields.key() - + fields_to_export = edi_struct and edi_struct.keys() or _fields.keys() edi_dict_list = [] - for row in self.browse(cr, uid, ids, fields_to_export, context): + value = None + for row in records: edi_dict = {} - edi_dict.update(self.edi_metadata(cr, uid, [row.id], context=context)[0]) + edi_dict.update(self.edi_metadata(cr, uid, [row], context=context)[0]) + for field in fields_to_export: - cols = self._fields[field] - if cols['type'] == 'many2one': - m2o_record = row.getattrs(field) - value = self.m2o(cr, uid, m2o_record, context=context) - elif cols['type'] == 'many2many': - m2m_records = row.getattrs(field) - value = self.m2m(cr, uid, m2m_records, context=context) - elif cols['type'] == 'one2many': - o2m_records = row.getattrs(field) - value = self.o2m(cr, uid, o2m_records, context=context) + cols = _fields[field] + if _fields[field].has_key('function') or _fields[field].has_key('related_columns'): + + pass else: - value = row.getattrs(field) + if cols['type'] == 'many2one': + m2o_record = getattr(row, field) + value = self.edi_m2o(cr, uid, m2o_record, context=context) + elif cols['type'] == 'many2many': + m2m_records = getattr(row, field) + value = self.edi_m2m(cr, uid, m2m_records, context=context) + elif cols['type'] == 'one2many': + o2m_records = getattr(row, field) + value = self.edi_o2m(cr, uid, o2m_records, context=context ) + else: + value = getattr(row, field) edi_dict[field] = value edi_dict_list.append(edi_dict) + return edi_dict_list @@ -395,6 +416,19 @@ class edi(object): connect it to the parent record via a write value like (4, db_id). """ # generic implementation! + + def convert(data): + if isinstance(data, unicode): + return str(data) + elif isinstance(data, dict): + return dict(map(convert, data.iteritems())) + elif isinstance(data, (list, tuple, set, frozenset)): + return type(data)(map(convert, data)) + else: + return data + #edi_document = convert(edi_document) + + fields = edi_document.keys() fields_to_import = [] data_line = [] @@ -403,87 +437,115 @@ class edi(object): current_module = 'edi_import' values = {} for field in edi_document.keys(): - if not field.startwith('__'): - field_to_import.append(field) + if field.startswith('__') and field == '__id': + db_uuid, xml_ids = tuple(edi_document[field].split(':')) + + elif not field.startswith('__'): + + + fields_to_import.append(field) edi_field_value = edi_document[field] - if _fields[field]['type'] in ('many2one', 'many2many'): - - if _fields[field]['type'] == 'many2one': - edi_parent_documents = [edi_field_value] - else: - edi_parent_documents = edi_field_value - - parent_lines = [] - - for edi_parent_document in edi_parent_documents: - #Look in ir.model.data for a record that matches the db_id. - #If found, replace the m2o value with the correct database ID and stop. - #If not found, continue to next step - - db_uuid, xml_id = tuple(edi_parent_document[0].split(':')) - data_ids = model_data.name_search(cr, uid, xml_id) - if data_ids: - d = model_data.browse(cr, uid, data_ids[0], context=context) - parent_lines.append(d.res_id) - - else: - #Perform name_search(name) to look for a record that matches the - #given m2o name. If only one record is found, create the missing - #ir.model.data record to link it to the db_id, and the replace the m2o - #value with the correct database ID, then stop. If zero result or - #multiple results are found, go to next step. - #Create the new record using the only field value that is known: the - #name, and create the ir.model.data entry to map to it. - - relation_object = self.get.pool(_fields[field]['relation']) - relation_ids = relation_object.name_search(cr, uid, edi_parent_document[1]) - if relation_ids and len(relation_ids) == 1: - relation_id = relation_ids[0] - else: - relation_id = relation_object.create(cr, uid, {'name': edi_parent_document[1]}, context=context) - model_data.create(cr, uid, { - 'name': xml_id, - 'model': relation_object._name, - #'module':relation_object._module, - 'res_id':relation_id, - },context=context) - - parent_lines.append(relation_id) - - if _fields[field]['type'] == 'many2one': - values[field] = parent_lines[0] - else: - many2many_ids = [] - for m2m_id in parent_lines: - many2many_ids.append((4,m2m_id)) - values[field] = many2many_ids - + if _fields[field].has_key('function') or _fields[field].has_key('related_columns'): - elif _fields[field]['type'] == 'one2many': - #Look for a record that matches the db_id provided in the __id field. If - #found, keep the corresponding database id, and connect it to the parent - #using a write value like (4,db_id). - #If not found via db_id, create a new entry using the same method that - #imports a full EDI record (recursive call!), grab the resulting db id, - #and use it to connect to the parent via a write value like (4, db_id). - - relations = [] - relation_object = self.get.pool(_fields[field]['relation']) - for edi_relation_document in edi_field_value: - db_uuid, xml_id = tuple(edi_relation_document.get('__id').split(':')) - data_ids = model_data.name_search(cr, uid, xml_id) - if data_ids: - d = model_data.browse(cr, uid, data_ids[0], context=context) - relations.append(d.res_id) + pass + else: + if _fields[field]['type'] in ('many2one', 'many2many'): + + if _fields[field]['type'] == 'many2one': + edi_parent_documents = [edi_field_value] + else: + edi_parent_documents = edi_field_value + + parent_lines = [] + + for edi_parent_document in edi_parent_documents: + #Look in ir.model.data for a record that matches the db_id. + #If found, replace the m2o value with the correct database ID and stop. + #If not found, continue to next step + if edi_parent_document[0].find(':') != -1 and edi_parent_document[1] != None: + db_uuid, xml_id = tuple(edi_parent_document[0].split(':')) + data_ids = model_data.name_search(cr, uid, xml_id) + if data_ids: + for data in model_data.browse(cr, uid, [data_ids[0][0]], context=context): + parent_lines.append(data.res_id) + print "..............................................",data.res_id + else: + #Perform name_search(name) to look for a record that matches the + #given m2o name. If only one record is found, create the missing + #ir.model.data record to link it to the db_id, and the replace the m2o + #value with the correct database ID, then stop. If zero result or + #multiple results are found, go to next step. + #Create the new record using the only field value that is known: the + #name, and create the ir.model.data entry to map to it. + + relation_object = self.pool.get(_fields[field]['relation']) + print "parent_document",edi_parent_document[1] + relation_ids = relation_object.search(cr, uid, [('name','=',edi_parent_document[1])]) + if relation_ids and len(relation_ids) == 1: + relation_id = relation_ids[0] + else: + relation_id = relation_object.create(cr, uid, {'name': edi_parent_document[1]}, context=context) + model_data.create(cr, uid, { + 'name': xml_id, + 'model': relation_object._name, + 'module':relation_object._module, + 'res_id':relation_id + }, context=context) + + parent_lines.append(relation_id) + + else: + pass + if len(parent_lines): + if _fields[field]['type'] == 'many2one': + values[field] = parent_lines[0] + + else: + many2many_ids = [] + for m2m_id in parent_lines: + many2many_ids.append((4,m2m_id)) + values[field] = many2many_ids + + + elif _fields[field]['type'] == 'one2many': + #Look for a record that matches the db_id provided in the __id field. If + #found, keep the corresponding database id, and connect it to the parent + #using a write value like (4,db_id). + #If not found via db_id, create a new entry using the same method that + #imports a full EDI record (recursive call!), grab the resulting db id, + #and use it to connect to the parent via a write value like (4, db_id). + + relations = [] + relation_object = self.pool.get(_fields[field]['relation']) + for edi_relation_document in edi_field_value: + '''print "pppppppppppppppppppppppppppppppppppppppppp" + print edi_field_value + print ":::::::::::::::::::::::::::::::::::::::::::::" + print edi_relation_document + print edi_parent_document + print "ppppppppppppppppppppppppppppppppppp" + if edi_relation_document['__id'].find(':') != -1: + db_uuid, xml_id = tuple(edi_relation_document['__id'].split(':')) + print xml_id + data_ids = model_data.name_search(cr, uid, xml_id) + print "dddddddddddddddddddddddddddddddddddddddddddddd",data_ids + if data_ids: + for data in model_data.browse(cr,uid,[data_ids[0][0]]): + relations.append(data.res_id) + else:''' r = relation_object.edi_import(cr, uid, edi_relation_document, context=context) relations.append(r) - one2many_ids = [] - for o2m_id in relations: - one2many_ids.append((4,o2m_id)) - values[field] = one2many_ids - else: - values[field] = edi_field_value + print "relation id ssssssssssssssssssss",relations + one2many_ids = [] + for o2m_id in relations: + one2many_ids.append((4,o2m_id)) + values[field] = one2many_ids + else: + values[field] = edi_field_value + for val in values.keys(): + print '::::::::::::::::::::::',val,'::::::::::;;;;;;;;;;;;;;;;;;;;;;' + print values[val] return model_data._update(cr, uid, self._name, current_module, values, context=context) - +# vim: ts=4 sts=4 sw=4 si et \ No newline at end of file From e60caf7d51b5f86f3597d610708a42ba3eeadbc6 Mon Sep 17 00:00:00 2001 From: "Amit Parmar (OpenERP)" Date: Tue, 21 Jun 2011 18:49:27 +0530 Subject: [PATCH 017/526] [EDI] revised edi class bzr revid: aar@tinyerp.com-20110621131927-872hkmor321f1gdf --- openerp/addons/base/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/__openerp__.py b/openerp/addons/base/__openerp__.py index b0c437352f9..bccaf7f660b 100644 --- a/openerp/addons/base/__openerp__.py +++ b/openerp/addons/base/__openerp__.py @@ -71,7 +71,7 @@ 'res/partner/wizard/partner_clear_ids_view.xml', 'res/partner/wizard/partner_wizard_ean_check_view.xml', 'res/partner/partner_data.xml', - #'test/test_edi_document.yml', + 'test/test_edi_document.yml', 'res/ir_property_view.xml', 'security/base_security.xml', 'publisher_warranty/publisher_warranty_view.xml', From 395b953f47d1098732aab9999535a8e6bca88329 Mon Sep 17 00:00:00 2001 From: "Amit Parmar (OpenERP)" Date: Tue, 21 Jun 2011 18:51:33 +0530 Subject: [PATCH 018/526] [EDI] revised edi class bzr revid: aar@tinyerp.com-20110621132133-g24z7zi1ln16h06d --- openerp/addons/base/test/test_edi_documents.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 openerp/addons/base/test/test_edi_documents.yml diff --git a/openerp/addons/base/test/test_edi_documents.yml b/openerp/addons/base/test/test_edi_documents.yml new file mode 100644 index 00000000000..1b66caaf4dc --- /dev/null +++ b/openerp/addons/base/test/test_edi_documents.yml @@ -0,0 +1,16 @@ +- + Tesing of Export functionality +- + !python {model: ir.edi.document}: | + partner_obj = self.pool.get('res.partner') + partner_ids = partner_obj.search(cr, uid, []) + + if partner_ids: + partners = partner_obj.browse(cr, uid, partner_ids) + tokens = self.export_edi(cr, uid, partners) + print '>>>>>>>>>>',tokens + for token in tokens: + document = self.get_document(cr, uid, token, context=context) + a = self.import_edi(cr, uid, edi_document = document) + print '////////////',a + From 6d4b05e3d4840629fe04bffb75ef078174dfa65b Mon Sep 17 00:00:00 2001 From: "Amit Parmar (OpenERP)" Date: Wed, 22 Jun 2011 14:41:23 +0530 Subject: [PATCH 019/526] [EDI] refactored code of edi class bzr revid: aar@tinyerp.com-20110622091123-6zal2su3vy8nv1nm --- openerp/addons/base/__openerp__.py | 2 +- openerp/addons/base/ir/ir_edi.py | 79 +++---------------- .../addons/base/test/test_edi_documents.yml | 4 +- 3 files changed, 15 insertions(+), 70 deletions(-) diff --git a/openerp/addons/base/__openerp__.py b/openerp/addons/base/__openerp__.py index bccaf7f660b..6406ec7b716 100644 --- a/openerp/addons/base/__openerp__.py +++ b/openerp/addons/base/__openerp__.py @@ -71,7 +71,6 @@ 'res/partner/wizard/partner_clear_ids_view.xml', 'res/partner/wizard/partner_wizard_ean_check_view.xml', 'res/partner/partner_data.xml', - 'test/test_edi_document.yml', 'res/ir_property_view.xml', 'security/base_security.xml', 'publisher_warranty/publisher_warranty_view.xml', @@ -96,6 +95,7 @@ 'test/test_ir_rule.yml', # <-- These tests modify/add/delete ir_rules. 'test/test_config_users.yml', 'test/test_ir_values.yml', + 'test/test_edi_documents.yml', ], 'installable': True, diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index fe8320491bf..fa41da161cc 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -20,12 +20,11 @@ ############################################################################## from osv import osv,fields -import simplejson import hashlib +import json import time import base64 import urllib2 -import unicodedata import openerp.release as release def safe_unique_id(model, database_id): """Generate a unique string to represent a (model,database id) pair @@ -82,7 +81,7 @@ class ir_edi_document(osv.osv): perform a JSON serialization of a list of dicts prepared by generate_edi() and return a UTF-8 encoded string that could be passed to deserialize() :param edi_dicts: it's list of edi_dict """ - serialized_list = simplejson.dumps(edi_documents) + serialized_list = json.dumps(edi_documents) return serialized_list def generate_edi(self, cr, uid, records, context=None): @@ -94,7 +93,7 @@ class ir_edi_document(osv.osv): edi_list = [] for record in records: record_model_obj = self.pool.get(record._name) - edi_list += record_model_obj.edi_export(cr, uid, [record],context=context) + edi_list += record_model_obj.edi_export(cr, uid, [record], context=context) return self.serialize(edi_list) def get_document(self, cr, uid, edi_token, context=None): @@ -103,13 +102,12 @@ class ir_edi_document(osv.osv): returns the string serialization that is in the database (column: document) for the given edi_token or raise. """ - records = self.search(cr, uid, [('name','=',edi_token)]) + records = self.name_search(cr, uid, edi_token) if records: - for edi in self.browse(cr, uid, records, context=context): - return edi.document + edi = self.browse(cr, uid, records, context=context) + return edi.document else: pass - def load_edi(self, cr, uid, edi_documents, context=None): """ @@ -132,7 +130,7 @@ class ir_edi_document(osv.osv): """ Deserialized the edi document string perform JSON deserialization from an edi document string, and returns a list of dicts """ - edi_document = simplejson.loads(edi_document_string) + edi_document = json.loads(edi_document_string) return edi_document @@ -173,15 +171,13 @@ class ir_edi_document(osv.osv): if edi_url and not edi_document: edi_document = urllib2.urlopen(edi_url).read() - #assert edi_document, _('EDI Document should be provided') + assert edi_document, _('EDI Document should be provided') else: - edi_documents = self.deserialize(edi_document) return self.load_edi(cr, uid, edi_documents, context=context) ir_edi_document() - class edi(object): _name = 'edi' _description = 'edi document handler' @@ -191,7 +187,6 @@ class edi(object): ``edi_import()`` and ``edi_export()`` methods to implement their specific behavior, based on the primitives provided by this superclass.""" - def edi_metadata(self, cr, uid, records, context=None): """Return a list representing the boilerplate EDI structure for exporting the record in the given browse_rec_list, including @@ -221,11 +216,10 @@ class edi(object): 'file_name': attachment.datas_fname, }) version = [] - for ver in release.major_version.split('.'): version.append(ver) - data_ids = model_data.search(cr, uid, [('res_id','=',record.id)]) + data_ids = model_data.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)]) if len(data_ids): xml_obj = model_data.browse(cr,uid,data_ids[0]) @@ -261,12 +255,9 @@ class edi(object): else: xml_id = record._name db_uuid = '%s:%s' %(db_uuid, xml_id) - - name = record.name return [db_uuid, name] - def edi_o2m(self, cr, uid, records, context=None): """Return a list representing a O2M EDI value for the browse_records from the given browse_record_list. @@ -289,7 +280,6 @@ class edi(object): return dict_list - def edi_m2m(self, cr, uid, records, context=None): """Return a list representing a M2M EDI value for the browse_records from the given browse_record_list. @@ -310,8 +300,6 @@ class edi(object): return dict_list - - def edi_export(self, cr, uid, records, edi_struct=None, context=None): """Returns a list of dicts representing an edi.document containing the browse_records with ``ids``, using the generic algorithm. @@ -336,19 +324,15 @@ class edi(object): if context is None: context = {} _fields = self.fields_get(cr, uid, context=context) - - fields_to_export = edi_struct and edi_struct.keys() or _fields.keys() edi_dict_list = [] value = None for row in records: edi_dict = {} edi_dict.update(self.edi_metadata(cr, uid, [row], context=context)[0]) - for field in fields_to_export: cols = _fields[field] if _fields[field].has_key('function') or _fields[field].has_key('related_columns'): - pass else: if cols['type'] == 'many2one': @@ -364,10 +348,8 @@ class edi(object): value = getattr(row, field) edi_dict[field] = value edi_dict_list.append(edi_dict) - return edi_dict_list - def edi_import(self, cr, uid, edi_document, context=None): """Imports a list of dicts representing an edi.document, using the @@ -417,18 +399,6 @@ class edi(object): """ # generic implementation! - def convert(data): - if isinstance(data, unicode): - return str(data) - elif isinstance(data, dict): - return dict(map(convert, data.iteritems())) - elif isinstance(data, (list, tuple, set, frozenset)): - return type(data)(map(convert, data)) - else: - return data - #edi_document = convert(edi_document) - - fields = edi_document.keys() fields_to_import = [] data_line = [] @@ -437,23 +407,15 @@ class edi(object): current_module = 'edi_import' values = {} for field in edi_document.keys(): - if field.startswith('__') and field == '__id': - db_uuid, xml_ids = tuple(edi_document[field].split(':')) - - elif not field.startswith('__'): - - + if not field.startswith('__'): fields_to_import.append(field) edi_field_value = edi_document[field] if _fields[field].has_key('function') or _fields[field].has_key('related_columns'): - pass else: if _fields[field]['type'] in ('many2one', 'many2many'): - if _fields[field]['type'] == 'many2one': edi_parent_documents = [edi_field_value] - else: edi_parent_documents = edi_field_value @@ -469,7 +431,6 @@ class edi(object): if data_ids: for data in model_data.browse(cr, uid, [data_ids[0][0]], context=context): parent_lines.append(data.res_id) - print "..............................................",data.res_id else: #Perform name_search(name) to look for a record that matches the #given m2o name. If only one record is found, create the missing @@ -506,8 +467,6 @@ class edi(object): for m2m_id in parent_lines: many2many_ids.append((4,m2m_id)) values[field] = many2many_ids - - elif _fields[field]['type'] == 'one2many': #Look for a record that matches the db_id provided in the __id field. If #found, keep the corresponding database id, and connect it to the parent @@ -519,24 +478,15 @@ class edi(object): relations = [] relation_object = self.pool.get(_fields[field]['relation']) for edi_relation_document in edi_field_value: - '''print "pppppppppppppppppppppppppppppppppppppppppp" - print edi_field_value - print ":::::::::::::::::::::::::::::::::::::::::::::" - print edi_relation_document - print edi_parent_document - print "ppppppppppppppppppppppppppppppppppp" if edi_relation_document['__id'].find(':') != -1: db_uuid, xml_id = tuple(edi_relation_document['__id'].split(':')) - print xml_id data_ids = model_data.name_search(cr, uid, xml_id) - print "dddddddddddddddddddddddddddddddddddddddddddddd",data_ids if data_ids: for data in model_data.browse(cr,uid,[data_ids[0][0]]): relations.append(data.res_id) - else:''' - r = relation_object.edi_import(cr, uid, edi_relation_document, context=context) - relations.append(r) - print "relation id ssssssssssssssssssss",relations + else: + r = relation_object.edi_import(cr, uid, edi_relation_document, context=context) + relations.append(r) one2many_ids = [] for o2m_id in relations: one2many_ids.append((4,o2m_id)) @@ -544,8 +494,5 @@ class edi(object): else: values[field] = edi_field_value - for val in values.keys(): - print '::::::::::::::::::::::',val,'::::::::::;;;;;;;;;;;;;;;;;;;;;;' - print values[val] return model_data._update(cr, uid, self._name, current_module, values, context=context) # vim: ts=4 sts=4 sw=4 si et \ No newline at end of file diff --git a/openerp/addons/base/test/test_edi_documents.yml b/openerp/addons/base/test/test_edi_documents.yml index 1b66caaf4dc..06656800d4f 100644 --- a/openerp/addons/base/test/test_edi_documents.yml +++ b/openerp/addons/base/test/test_edi_documents.yml @@ -4,13 +4,11 @@ !python {model: ir.edi.document}: | partner_obj = self.pool.get('res.partner') partner_ids = partner_obj.search(cr, uid, []) - if partner_ids: partners = partner_obj.browse(cr, uid, partner_ids) tokens = self.export_edi(cr, uid, partners) - print '>>>>>>>>>>',tokens for token in tokens: document = self.get_document(cr, uid, token, context=context) a = self.import_edi(cr, uid, edi_document = document) - print '////////////',a + From b5050763692468b73263455c54b28ad0351bba5c Mon Sep 17 00:00:00 2001 From: "Yogesh (OpenERP)" Date: Fri, 24 Jun 2011 14:16:56 +0530 Subject: [PATCH 020/526] [FIX] add get method in refrence field and check if refrence id not exists then return false. lp bug: https://launchpad.net/bugs/783961 fixed bzr revid: ysa@tinyerp.com-20110624084656-c2jd3xskk10fwt35 --- openerp/osv/fields.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index f55e4b9c027..aa042ca6541 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -148,9 +148,23 @@ class integer_big(_column): class reference(_column): _type = 'reference' + _classic_read = False # post-process to handle missing target + def __init__(self, string, selection, size, **args): _column.__init__(self, string=string, size=size, selection=selection, **args) + def get(self, cr, obj, ids, name, uid=None, context=None, values=None): + result = {} + # copy initial values fetched previously. + for row in values: + result[row['id']] = row[name] + # verify target object exists + for id, value in result.iteritems(): + if value: + model, res_id = value.split(',') + if not obj.pool.get(model).exists(cr, uid, [int(res_id)], context=context): + result[id] = False + return result class char(_column): _type = 'char' From 5ac271fa1fa85fc24a34c76ca5258d45a422955f Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Mon, 27 Jun 2011 13:59:45 +0530 Subject: [PATCH 021/526] [IMP] edi: improve metadata function to create xml record if does not found bzr revid: hmo@tinyerp.com-20110627082945-xtoibwmlsl8187yg --- openerp/addons/base/ir/ir_edi.py | 50 ++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index ad9fd2a2adf..8db5b397116 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -26,6 +26,9 @@ import time import base64 import urllib2 import openerp.release as release + +edi_module = 'edi_import' + def safe_unique_id(model, database_id): """Generate a unique string to represent a (model,database id) pair without being too long, without revealing the database id, and @@ -201,8 +204,9 @@ class edi(object): - __version': a list of components for the version - __attachments': a list (possibly empty) of dicts describing the files attached to this record. """ - # generic implementation! - model_data = self.pool.get('ir.model.data') + if context is None: + context = {} + model_data_pool = self.pool.get('ir.model.data') data_ids = [] attachment_object = self.pool.get('ir.attachment') edi_dict_list = [] @@ -214,6 +218,7 @@ class edi(object): except: pass version.append(ver) + for record in records: attachment_ids = attachment_object.search(cr, uid, [('res_model','=', record._name), ('res_id', '=', record.id)]) attachment_dict_list = [] @@ -225,21 +230,32 @@ class edi(object): }) - data_ids = model_data.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)]) - + data_ids = model_data_pool.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)]) + uuid = safe_unique_id(record._name, record.id) + if len(data_ids): - xml_record = model_data.browse(cr, uid, data_ids[0]) - uuid = safe_unique_id(record._name, record.id) - db_uuid = '%s:%s.%s' % (uuid, xml_record.module, xml_record.name) - + xml_record_id = data_ids[0] + else: + xml_record_id = model_data_pool.create(cr, uid, { + 'name': uuid, + 'model': record._name, + 'module': edi_module, + 'res_id': record.id}, context=context) + + xml_record = model_data_pool.browse(cr, uid, xml_record_id, context=context) + db_uuid = '%s:%s.%s' % (uuid, xml_record.module, xml_record.name) + edi_dict = { - '__model' : record._name, - '__module' : record._module, '__id': db_uuid, '__last_update': False, #record.write_date, #TODO: convert into UTC - '__version': version, - '__attachments': attachment_dict_list } + if not context.get('o2m_export'): + edi_dict.update({ + '__model' : record._name, + '__module' : record._module, + '__version': version, + '__attachments': attachment_dict_list + }) edi_dict_list.append(edi_dict) return edi_dict_list @@ -278,11 +294,14 @@ class edi(object): """ # generic implementation! dict_list = [] - + if context is None: + context = {} + ctx = context.copy() + ctx.update({'o2m_export':True}) for record in records: model_obj = self.pool.get(record._name) - dict_list += model_obj.edi_export(cr, uid, [record], context=context) + dict_list += model_obj.edi_export(cr, uid, [record], context=ctx) return dict_list @@ -410,7 +429,6 @@ class edi(object): data_line = [] model_data = self.pool.get('ir.model.data') _fields = self.fields_get(cr, uid, context=context) - current_module = 'edi_import' values = {} for field in edi_document.keys(): if not field.startswith('__'): @@ -500,5 +518,5 @@ class edi(object): else: values[field] = edi_field_value - return model_data._update(cr, uid, self._name, current_module, values, context=context) + return model_data._update(cr, uid, self._name, edi_module, values, context=context) # vim: ts=4 sts=4 sw=4 si et From 25dcd09489096441f2588b06db2ad23bab0fd462 Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Mon, 27 Jun 2011 17:51:45 +0530 Subject: [PATCH 022/526] [IMP] EDI: introduce new methods in edi class edi_import_relation: create record of relation object if does not match with relation value edi_xml_id: generate xml id with unique db id. create record of ir.model.data if xml id is not found for particular record. bzr revid: hmo@tinyerp.com-20110627122145-v2d2wtkjut68rcba --- openerp/addons/base/ir/ir_edi.py | 74 ++++++++++++++++---------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 8db5b397116..9672b9a2207 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -190,6 +190,22 @@ class edi(object): Classes that inherit from this mixin class should override the ``edi_import()`` and ``edi_export()`` methods to implement their specific behavior, based on the primitives provided by this superclass.""" + + def edi_xml_id(self, cr, uid, record, context=None): + model_data_pool = self.pool.get('ir.model.data') + uuid = safe_unique_id(record._name, record.id) + data_ids = model_data_pool.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)]) + if len(data_ids): + xml_record_id = data_ids[0] + else: + xml_record_id = model_data_pool.create(cr, uid, { + 'name': uuid, + 'model': record._name, + 'module': edi_module, + 'res_id': record.id}, context=context) + xml_record = model_data_pool.browse(cr, uid, xml_record_id, context=context) + db_uuid = '%s:%s.%s' % (uuid, xml_record.module, xml_record.name) + return db_uuid def edi_metadata(self, cr, uid, records, context=None): """Return a list representing the boilerplate EDI structure for @@ -206,7 +222,6 @@ class edi(object): """ if context is None: context = {} - model_data_pool = self.pool.get('ir.model.data') data_ids = [] attachment_object = self.pool.get('ir.attachment') edi_dict_list = [] @@ -230,21 +245,7 @@ class edi(object): }) - data_ids = model_data_pool.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)]) - uuid = safe_unique_id(record._name, record.id) - - if len(data_ids): - xml_record_id = data_ids[0] - else: - xml_record_id = model_data_pool.create(cr, uid, { - 'name': uuid, - 'model': record._name, - 'module': edi_module, - 'res_id': record.id}, context=context) - - xml_record = model_data_pool.browse(cr, uid, xml_record_id, context=context) - db_uuid = '%s:%s.%s' % (uuid, xml_record.module, xml_record.name) - + db_uuid = self.edi_xml_id(cr, uid, record, context=context) edi_dict = { '__id': db_uuid, '__last_update': False, #record.write_date, #TODO: convert into UTC @@ -267,20 +268,13 @@ class edi(object): Exmaple: ['db-uuid:xml-id', 'Partner Name'] """ # generic implementation! - data_object = self.pool.get('ir.model.data') - db_uuid = safe_unique_id(record._name, record.id) - xml_ids = data_object.search(cr, uid, [('model','=',record._name),('res_id','=',record.id)]) - - if xml_ids: - xml_id = data_object.browse(cr, uid, xml_ids[0], context=context) - xml_id = '%s.%s' %(xml_id.module, xml_id.name) - else: - xml_id = record._name - db_uuid = '%s:%s' %(db_uuid, xml_id) - name = record.name + db_uuid = self.edi_xml_id(cr, uid, record, context=context) + relation_model_pool = self.pool.get(record._name) + name = relation_model_pool.name_get(cr, uid, [record.id], context=context) + name = name and name[0][1] or False return [db_uuid, name] - def edi_o2m(self, cr, uid, records, context=None): + def edi_o2m(self, cr, uid, records, edi_struct=None, context=None): """Return a list representing a O2M EDI value for the browse_records from the given browse_record_list. @@ -301,7 +295,7 @@ class edi(object): for record in records: model_obj = self.pool.get(record._name) - dict_list += model_obj.edi_export(cr, uid, [record], context=ctx) + dict_list += model_obj.edi_export(cr, uid, [record], edi_struct=edi_struct, context=ctx) return dict_list @@ -368,12 +362,22 @@ class edi(object): elif cols['type'] == 'many2many': value = self.edi_m2m(cr, uid, record, context=context) elif cols['type'] == 'one2many': - value = self.edi_o2m(cr, uid, record, context=context ) + value = self.edi_o2m(cr, uid, record, edi_struct=edi_struct.get(field, {}), context=context ) else: value = record edi_dict[field] = value edi_dict_list.append(edi_dict) return edi_dict_list + + def edi_import_relation(self, cr, uid, relation_model, relation_value, values={}, context=None): + relation_object = self.pool.get(relation_model) + relation_ids = relation_object.name_search(cr, uid, relation_value, context=context) + if relation_ids and len(relation_ids) == 1: + relation_id = relation_ids[0][0] + else: + values.update({'name': relation_value}) + relation_id = relation_object.create(cr, uid, values, context=context) + return relation_id def edi_import(self, cr, uid, edi_document, context=None): @@ -465,13 +469,9 @@ class edi(object): #multiple results are found, go to next step. #Create the new record using the only field value that is known: the #name, and create the ir.model.data entry to map to it. - - relation_object = self.pool.get(_fields[field]['relation']) - relation_ids = relation_object.search(cr, uid, [('name','=',edi_parent_document[1])]) - if relation_ids and len(relation_ids) == 1: - relation_id = relation_ids[0] - else: - relation_id = relation_object.create(cr, uid, {'name': edi_parent_document[1]}, context=context) + relation_model = _fields[field]['relation'] + relation_id = self.edi_import_relation(cr, uid, relation_model, edi_parent_document[1], context=context) + relation_object = self.pool.get(relation_model) model_data.create(cr, uid, { 'name': xml_id, 'model': relation_object._name, From 5cc4b243b683f7ecf69b1ae2e18247d82950e50b Mon Sep 17 00:00:00 2001 From: mtr Date: Tue, 28 Jun 2011 17:16:33 +0530 Subject: [PATCH 023/526] [FIX] hr_holidays:-Exception raised on confirmation of leave request is now translated in user's lang. lp bug: https://launchpad.net/bugs/798739 fixed bzr revid: mtr@mtr-20110628114633-78h4k4q3d8jectjo --- addons/hr_holidays/hr_holidays.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index 7074fd5cecc..4c000aab968 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -224,14 +224,20 @@ class hr_holidays(osv.osv): return True def holidays_validate(self, cr, uid, ids, *args): - self.check_holidays(cr, uid, ids) + context = {} + lang = self.pool.get('res.users').browse(cr, uid, uid).context_lang + context.update({'lang': lang}) + self.check_holidays(cr, uid, ids, context=context) obj_emp = self.pool.get('hr.employee') ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)]) manager = ids2 and ids2[0] or False return self.write(cr, uid, ids, {'state':'validate1', 'manager_id': manager}) def holidays_validate2(self, cr, uid, ids, *args): - self.check_holidays(cr, uid, ids) + context = {} + lang = self.pool.get('res.users').browse(cr, uid, uid).context_lang + context.update({'lang': lang}) + self.check_holidays(cr, uid, ids, context=context) obj_emp = self.pool.get('hr.employee') ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)]) manager = ids2 and ids2[0] or False @@ -277,7 +283,10 @@ class hr_holidays(osv.osv): return True def holidays_confirm(self, cr, uid, ids, *args): - self.check_holidays(cr, uid, ids) + context = {} + lang = self.pool.get('res.users').browse(cr, uid, uid).context_lang + context.update({'lang': lang}) + self.check_holidays(cr, uid, ids, context=context) return self.write(cr, uid, ids, {'state':'confirm'}) def holidays_refuse(self, cr, uid, ids, *args): @@ -302,8 +311,10 @@ class hr_holidays(osv.osv): return True - def check_holidays(self, cr, uid, ids): + def check_holidays(self, cr, uid, ids, context=None): holi_status_obj = self.pool.get('hr.holidays.status') + if context is None: + context = {} for record in self.browse(cr, uid, ids): if record.holiday_type == 'employee' and record.type == 'remove': if record.employee_id and not record.holiday_status_id.limit: From 3be8619d32f3ca9ca3859fde34585ccb3f3d3ae8 Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Wed, 29 Jun 2011 12:10:37 +0530 Subject: [PATCH 024/526] [IMP] web_service: introduce EDI Services to provide service like get_edi_document, import_edi_document, import_edi_url bzr revid: hmo@tinyerp.com-20110629064037-2wb76qua48m1g2bh --- openerp/service/web_services.py | 42 +++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index 39457807868..37dadc195fb 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -37,6 +37,47 @@ import openerp.modules import locale import logging from cStringIO import StringIO +import threading + +class edi(netsvc.ExportService): + def exp_get_edi_document(self, edi_token): + db_name = getattr(threading.currentThread(), 'dbname', None) + if db_name: + cr = pooler.get_db_only(db_name).cursor() + else: + raise Exception("No database cursor found!") + pool = pooler.get_pool(db_name) + edi_pool = pool.get('ir.edi.document') + return edi_pool.get_document(cr, 1, edi_token) + + def exp_import_edi_document(self, db, uid, passwd, edi_document, context=None): + cr = pooler.get_db_only(db).cursor() + pool = pooler.get_pool(db) + edi_pool = pool.get('ir.edi.document') + return edi_pool.import_edi(cr, uid, edi_document=edi_document, context=context) + + def exp_import_edi_url(self, db, uid, passwd, edi_url, context=None): + cr = pooler.get_db_only(db).cursor() + pool = pooler.get_pool(db) + edi_pool = pool.get('ir.edi.document') + return edi_pool.import_edi(cr, uid, edi_url=edi_url, context=context) + + def __init__(self, name="edi"): + netsvc.ExportService.__init__(self, name) + self.joinGroup("web-services") + + def dispatch(self, method, auth, params): + if method in ['import_edi_document', 'import_edi_url']: + (db, uid, passwd ) = params[0:3] + security.check(db, uid, passwd) + elif method in ['get_edi_document']: + # params = params + # No security check for these methods + pass + else: + raise KeyError("Method not found: %s" % method) + fn = getattr(self, 'exp_'+method) + return fn(*params) class db(netsvc.ExportService): def __init__(self, name="db"): @@ -763,6 +804,7 @@ def start_web_services(): objects_proxy() wizard() report_spool() + edi() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 784b6af17c37b278c30c8b8d14e8ad3be9751abb Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Wed, 29 Jun 2011 15:49:28 +0530 Subject: [PATCH 025/526] [FIX] edi: In export_edi method, edi_struct should be have blank dict if None value bzr revid: hmo@tinyerp.com-20110629101928-djc9hvlm0pmqm362 --- openerp/addons/base/ir/ir_edi.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 9672b9a2207..0ee29e13a3a 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -342,6 +342,8 @@ class edi(object): if context is None: context = {} + if edi_struct is None: + edi_struct = {} _fields = self.fields_get(cr, uid, context=context) fields_to_export = edi_struct and edi_struct.keys() or _fields.keys() edi_dict_list = [] From c2ba873ab842a98efa4f12cb9b73784c6fd577bc Mon Sep 17 00:00:00 2001 From: mtr Date: Thu, 30 Jun 2011 14:06:02 +0530 Subject: [PATCH 026/526] [IMP] hr_holidays: improved code for translation using context bzr revid: mtr@mtr-20110630083602-au21nsi3yyw97qnp --- addons/hr_holidays/hr_holidays.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index 4c000aab968..3dfc9370ecc 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -224,9 +224,8 @@ class hr_holidays(osv.osv): return True def holidays_validate(self, cr, uid, ids, *args): - context = {} lang = self.pool.get('res.users').browse(cr, uid, uid).context_lang - context.update({'lang': lang}) + context = {'lang': lang} self.check_holidays(cr, uid, ids, context=context) obj_emp = self.pool.get('hr.employee') ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)]) @@ -234,9 +233,8 @@ class hr_holidays(osv.osv): return self.write(cr, uid, ids, {'state':'validate1', 'manager_id': manager}) def holidays_validate2(self, cr, uid, ids, *args): - context = {} lang = self.pool.get('res.users').browse(cr, uid, uid).context_lang - context.update({'lang': lang}) + context = {'lang': lang} self.check_holidays(cr, uid, ids, context=context) obj_emp = self.pool.get('hr.employee') ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)]) @@ -283,9 +281,8 @@ class hr_holidays(osv.osv): return True def holidays_confirm(self, cr, uid, ids, *args): - context = {} lang = self.pool.get('res.users').browse(cr, uid, uid).context_lang - context.update({'lang': lang}) + context = {'lang': lang} self.check_holidays(cr, uid, ids, context=context) return self.write(cr, uid, ids, {'state':'confirm'}) @@ -313,8 +310,6 @@ class hr_holidays(osv.osv): def check_holidays(self, cr, uid, ids, context=None): holi_status_obj = self.pool.get('hr.holidays.status') - if context is None: - context = {} for record in self.browse(cr, uid, ids): if record.holiday_type == 'employee' and record.type == 'remove': if record.employee_id and not record.holiday_status_id.limit: From a7e16487e484cffa2a0b817340808816c2ee139c Mon Sep 17 00:00:00 2001 From: Mustufa Rangwala Date: Thu, 30 Jun 2011 14:59:05 +0530 Subject: [PATCH 027/526] [IMP] Hr_holidays: Improve the code by making generic method for context lang get bzr revid: mra@mra-laptop-20110630092905-4ggbh1i7bnm7ibqb --- addons/hr_holidays/hr_holidays.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index 3dfc9370ecc..7756e218ce0 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -223,9 +223,12 @@ class hr_holidays(osv.osv): wf_service.trg_create(uid, 'hr.holidays', id, cr) return True - def holidays_validate(self, cr, uid, ids, *args): + def get_user_lang(self, cr, uid, ids): lang = self.pool.get('res.users').browse(cr, uid, uid).context_lang - context = {'lang': lang} + return {'lang': lang} + + def holidays_validate(self, cr, uid, ids, *args): + context = self.get_user_lang(cr, uid, ids) self.check_holidays(cr, uid, ids, context=context) obj_emp = self.pool.get('hr.employee') ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)]) @@ -233,8 +236,7 @@ class hr_holidays(osv.osv): return self.write(cr, uid, ids, {'state':'validate1', 'manager_id': manager}) def holidays_validate2(self, cr, uid, ids, *args): - lang = self.pool.get('res.users').browse(cr, uid, uid).context_lang - context = {'lang': lang} + context = self.get_user_lang(cr, uid, ids) self.check_holidays(cr, uid, ids, context=context) obj_emp = self.pool.get('hr.employee') ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)]) @@ -281,8 +283,7 @@ class hr_holidays(osv.osv): return True def holidays_confirm(self, cr, uid, ids, *args): - lang = self.pool.get('res.users').browse(cr, uid, uid).context_lang - context = {'lang': lang} + context = self.get_user_lang(cr, uid, ids) self.check_holidays(cr, uid, ids, context=context) return self.write(cr, uid, ids, {'state':'confirm'}) From d93d0aa0faf14c043eaf7eebf1e49a1b65a0e16f Mon Sep 17 00:00:00 2001 From: mtr Date: Tue, 5 Jul 2011 12:41:51 +0530 Subject: [PATCH 028/526] [IMP] hr_holidays: passed context as an argument in all the methods() bzr revid: mtr@mtr-20110705071151-lhg6wqbphg9hoysw --- addons/hr_holidays/hr_holidays.py | 39 +++++++++++++------------ addons/hr_holidays/hr_holidays_view.xml | 12 ++++---- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index 7756e218ce0..e2cf2a5bace 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -161,7 +161,7 @@ class hr_holidays(osv.osv): leave_ids = obj_res_leave.search(cr, uid, [('holiday_id', 'in', ids)], context=context) return obj_res_leave.unlink(cr, uid, leave_ids) - def onchange_type(self, cr, uid, ids, holiday_type): + def onchange_type(self, cr, uid, ids, holiday_type, context=None): result = {'value': {'employee_id': False}} if holiday_type == 'employee': ids_employee = self.pool.get('hr.employee').search(cr, uid, [('user_id','=', uid)]) @@ -172,7 +172,7 @@ class hr_holidays(osv.osv): return result # TODO: can be improved using resource calendar method - def _get_number_of_days(self, date_from, date_to): + def _get_number_of_days(self, date_from, date_to, context=None): """Returns a float equals to the timedelta between two dates given as string.""" DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" @@ -188,10 +188,12 @@ class hr_holidays(osv.osv): raise osv.except_osv(_('Warning!'),_('You cannot delete a leave which is not in draft state !')) return super(hr_holidays, self).unlink(cr, uid, ids, context) - def onchange_date_from(self, cr, uid, ids, date_to, date_from): + def onchange_date_from(self, cr, uid, ids, date_to, date_from, context=None): result = {} + if context is None: + context = {} if date_to and date_from: - diff_day = self._get_number_of_days(date_from, date_to) + diff_day = self._get_number_of_days(date_from, date_to, context=context) result['value'] = { 'number_of_days_temp': round(diff_day)+1 } @@ -212,7 +214,7 @@ class hr_holidays(osv.osv): } return {'warning': warning} - def set_to_draft(self, cr, uid, ids, *args): + def set_to_draft(self, cr, uid, ids, context=None): self.write(cr, uid, ids, { 'state': 'draft', 'manager_id': False, @@ -223,20 +225,18 @@ class hr_holidays(osv.osv): wf_service.trg_create(uid, 'hr.holidays', id, cr) return True - def get_user_lang(self, cr, uid, ids): - lang = self.pool.get('res.users').browse(cr, uid, uid).context_lang - return {'lang': lang} - - def holidays_validate(self, cr, uid, ids, *args): - context = self.get_user_lang(cr, uid, ids) + def holidays_validate(self, cr, uid, ids, context=None): + if context is None: + context = {} self.check_holidays(cr, uid, ids, context=context) obj_emp = self.pool.get('hr.employee') ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)]) manager = ids2 and ids2[0] or False return self.write(cr, uid, ids, {'state':'validate1', 'manager_id': manager}) - def holidays_validate2(self, cr, uid, ids, *args): - context = self.get_user_lang(cr, uid, ids) + def holidays_validate2(self, cr, uid, ids, context=None): + if context is None: + context = {} self.check_holidays(cr, uid, ids, context=context) obj_emp = self.pool.get('hr.employee') ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)]) @@ -282,20 +282,23 @@ class hr_holidays(osv.osv): wf_service.trg_validate(uid, 'hr.holidays', leave_id, 'second_validate', cr) return True - def holidays_confirm(self, cr, uid, ids, *args): - context = self.get_user_lang(cr, uid, ids) + def holidays_confirm(self, cr, uid, ids, context=None): + if context is None: + context = {} self.check_holidays(cr, uid, ids, context=context) return self.write(cr, uid, ids, {'state':'confirm'}) - def holidays_refuse(self, cr, uid, ids, *args): + def holidays_refuse(self, cr, uid, ids, context=None): + if context is None: + context = {} obj_emp = self.pool.get('hr.employee') ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)]) manager = ids2 and ids2[0] or False self.write(cr, uid, ids, {'state': 'refuse', 'manager_id2': manager}) - self.holidays_cancel(cr, uid, ids) + self.holidays_cancel(cr, uid, ids, context=context) return True - def holidays_cancel(self, cr, uid, ids, *args): + def holidays_cancel(self, cr, uid, ids, context=None): obj_crm_meeting = self.pool.get('crm.meeting') for record in self.browse(cr, uid, ids): # Delete the meeting diff --git a/addons/hr_holidays/hr_holidays_view.xml b/addons/hr_holidays/hr_holidays_view.xml index 608fda554fb..45f8aa8c3f5 100644 --- a/addons/hr_holidays/hr_holidays_view.xml +++ b/addons/hr_holidays/hr_holidays_view.xml @@ -71,7 +71,7 @@
- + @@ -83,8 +83,8 @@ - - + + @@ -115,7 +115,7 @@ - + @@ -181,8 +181,8 @@ - - + + From 9784142fe7bc141f9695dbf0d6d4ed7d18c08a67 Mon Sep 17 00:00:00 2001 From: mtr Date: Tue, 5 Jul 2011 14:45:25 +0530 Subject: [PATCH 029/526] [IMP] hr_holidays: removed condition checked for context and context passed in xml bzr revid: mtr@mtr-20110705091525-hm0772szzv6xa3zc --- addons/hr_holidays/hr_holidays.py | 10 ---------- addons/hr_holidays/hr_holidays_view.xml | 12 ++++++------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/addons/hr_holidays/hr_holidays.py b/addons/hr_holidays/hr_holidays.py index e2cf2a5bace..2fcd594bee0 100644 --- a/addons/hr_holidays/hr_holidays.py +++ b/addons/hr_holidays/hr_holidays.py @@ -190,8 +190,6 @@ class hr_holidays(osv.osv): def onchange_date_from(self, cr, uid, ids, date_to, date_from, context=None): result = {} - if context is None: - context = {} if date_to and date_from: diff_day = self._get_number_of_days(date_from, date_to, context=context) result['value'] = { @@ -226,8 +224,6 @@ class hr_holidays(osv.osv): return True def holidays_validate(self, cr, uid, ids, context=None): - if context is None: - context = {} self.check_holidays(cr, uid, ids, context=context) obj_emp = self.pool.get('hr.employee') ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)]) @@ -235,8 +231,6 @@ class hr_holidays(osv.osv): return self.write(cr, uid, ids, {'state':'validate1', 'manager_id': manager}) def holidays_validate2(self, cr, uid, ids, context=None): - if context is None: - context = {} self.check_holidays(cr, uid, ids, context=context) obj_emp = self.pool.get('hr.employee') ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)]) @@ -283,14 +277,10 @@ class hr_holidays(osv.osv): return True def holidays_confirm(self, cr, uid, ids, context=None): - if context is None: - context = {} self.check_holidays(cr, uid, ids, context=context) return self.write(cr, uid, ids, {'state':'confirm'}) def holidays_refuse(self, cr, uid, ids, context=None): - if context is None: - context = {} obj_emp = self.pool.get('hr.employee') ids2 = obj_emp.search(cr, uid, [('user_id', '=', uid)]) manager = ids2 and ids2[0] or False diff --git a/addons/hr_holidays/hr_holidays_view.xml b/addons/hr_holidays/hr_holidays_view.xml index 45f8aa8c3f5..608fda554fb 100644 --- a/addons/hr_holidays/hr_holidays_view.xml +++ b/addons/hr_holidays/hr_holidays_view.xml @@ -71,7 +71,7 @@ - + @@ -83,8 +83,8 @@ - - + + @@ -115,7 +115,7 @@ - + @@ -181,8 +181,8 @@ - - + + From b9e581303b0d426f627e88e795f7bf20f4bdfd44 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 5 Jul 2011 19:00:53 +0200 Subject: [PATCH 030/526] [IMP] ir_cron: each job in its own thread, first stab. bzr revid: vmt@openerp.com-20110705170053-q3xgeoq21oc7dh8h --- openerp-server | 1 + openerp/addons/base/ir/ir_cron.py | 113 ++++++++++++++++++++++++------ 2 files changed, 93 insertions(+), 21 deletions(-) diff --git a/openerp-server b/openerp-server index 3586b11c7d2..347cd9f6723 100755 --- a/openerp-server +++ b/openerp-server @@ -109,6 +109,7 @@ if config['db_name']: cr.rollback() pool.get('ir.cron')._poolJobs(db.dbname) + # pool.get('ir.cron').restart(db.dbname) # jobs will start to be processed later, when start_agent below is called. cr.close() diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 054a15257ef..ba25f59525d 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -21,6 +21,8 @@ import time import logging +import threading +import psycopg2 from datetime import datetime from dateutil.relativedelta import relativedelta import netsvc @@ -74,6 +76,19 @@ class ir_cron(osv.osv, netsvc.Agent): 'doall' : lambda *a: 1 } + def f(a, b, c): + print ">>> in f" + + def expensive(a, b, c): + print ">>> in expensive" + time.sleep(80) + print ">>> out expensive" + + def expensive_2(a, b, c): + print ">>> in expensive_2" + time.sleep(80) + print ">>> out expensive_2" + def _check_args(self, cr, uid, ids, context=None): try: for this in self.browse(cr, uid, ids, context): @@ -109,45 +124,96 @@ class ir_cron(osv.osv, netsvc.Agent): except Exception, e: self._handle_callback_exception(cr, uid, model, func, args, job_id, e) - def _poolJobs(self, db_name, check=False): + def _compute_nextcall(self, job, now): + """ Compute the nextcall for a job exactly as _run_job does. """ + nextcall = datetime.strptime(job['nextcall'], '%Y-%m-%d %H:%M:%S') + numbercall = job['numbercall'] + + while nextcall < now and numbercall: + if numbercall > 0: + numbercall -= 1 + if numbercall: + nextcall += _intervalTypes[job['interval_type']](job['interval_number']) + + return nextcall.strftime('%Y-%m-%d %H:%M:%S') + + def _run_job(self, cr, job, now): + """ Run a given job. """ + try: + nextcall = datetime.strptime(job['nextcall'], '%Y-%m-%d %H:%M:%S') + numbercall = job['numbercall'] + + ok = False + while nextcall < now and numbercall: + if numbercall > 0: + numbercall -= 1 + if not ok or job['doall']: + self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id']) + if numbercall: + nextcall += _intervalTypes[job['interval_type']](job['interval_number']) + ok = True + addsql = '' + if not numbercall: + addsql = ', active=False' + cr.execute("update ir_cron set nextcall=%s, numbercall=%s"+addsql+" where id=%s", (nextcall.strftime('%Y-%m-%d %H:%M:%S'), numbercall, job['id'])) + # TODO re-schedule the master thread to nextcall if its wake-up time is later than nextcall. + # TODO NOTIFY the 'ir_cron' channel. + finally: + cr.commit() + cr.close() + + def _poolJobs(self, db_name): + # TODO remove 'check' argument from addons/base_action_rule/base_action_rule.py try: db, pool = pooler.get_db_and_pool(db_name) except: return False cr = db.cursor() try: + jobs = {} # mapping job ids to jobs for all jobs being processed. if not pool._init: now = datetime.now() cr.execute('select * from ir_cron where numbercall<>0 and active and nextcall<=now() order by priority') for job in cr.dictfetchall(): - nextcall = datetime.strptime(job['nextcall'], '%Y-%m-%d %H:%M:%S') - numbercall = job['numbercall'] + task_cr = db.cursor() + task_job = None + try: + task_cr.execute('select * from ir_cron where id=%s for update nowait', (job['id'],), log_exceptions=False) + task_job = task_cr.dictfetchall()[0] + jobs[job['id']] = job + except psycopg2.OperationalError, e: + if e.pgcode == '55P03': + # Class 55: Object not in prerequisite state, 55P03: lock_not_available + continue + else: + raise + finally: + if not task_job: + task_cr.close() - ok = False - while nextcall < now and numbercall: - if numbercall > 0: - numbercall -= 1 - if not ok or job['doall']: - self._callback(cr, job['user_id'], job['model'], job['function'], job['args'], job['id']) - if numbercall: - nextcall += _intervalTypes[job['interval_type']](job['interval_number']) - ok = True - addsql = '' - if not numbercall: - addsql = ', active=False' - cr.execute("update ir_cron set nextcall=%s, numbercall=%s"+addsql+" where id=%s", (nextcall.strftime('%Y-%m-%d %H:%M:%S'), numbercall, job['id'])) - cr.commit() + task_thread = threading.Thread(target=self._run_job, name=task_job['name'], args=(task_cr, task_job, now)) + # force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default) + task_thread.setDaemon(False) + task_thread.start() - - cr.execute('select min(nextcall) as min_next_call from ir_cron where numbercall<>0 and active') + # Wake up time, without considering the currently processed jobs. + if jobs.keys(): + cr.execute('select min(nextcall) as min_next_call from ir_cron where numbercall<>0 and active and id not in %s', (tuple(jobs.keys()),)) + else: + cr.execute('select min(nextcall) as min_next_call from ir_cron where numbercall<>0 and active') next_call = cr.dictfetchone()['min_next_call'] if next_call: next_call = time.mktime(time.strptime(next_call, '%Y-%m-%d %H:%M:%S')) else: next_call = int(time.time()) + 3600 # if do not find active cron job from database, it will run again after 1 day - if not check: - self.setAlarm(self._poolJobs, next_call, db_name, db_name) + # Take the smallest nextcall value. + for job in jobs.values(): + nextcall = self._compute_nextcall(job, now) + if nextcall < next_call: + next_call = nextcall + + self.setAlarm(self._poolJobs, next_call, db_name, db_name) except Exception, ex: self._logger.warning('Exception in cron:', exc_info=True) @@ -156,6 +222,11 @@ class ir_cron(osv.osv, netsvc.Agent): cr.commit() cr.close() + def restart_all(self): + import openerp.models.registry + for dbname in openerp.models.registry.RegistryManager.registries: + self.restart(self, dbname) + def restart(self, dbname): self.cancel(dbname) # Reschedule cron processing job asap, but not in the current thread From 46f82438777b6a426a764c129c324289f6fd79c4 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 7 Jul 2011 15:58:43 +0200 Subject: [PATCH 031/526] [IMP] ir_cron: reschedule the main cron thread if a worker takes too long. bzr revid: vmt@openerp.com-20110707135843-z38f4r8s373ctnd2 --- openerp-server | 4 +- openerp/addons/base/ir/ir_cron.py | 75 ++++++++++++++++++++++++++----- openerp/netsvc.py | 12 +++++ 3 files changed, 78 insertions(+), 13 deletions(-) diff --git a/openerp-server b/openerp-server index 347cd9f6723..a2ec11ba996 100755 --- a/openerp-server +++ b/openerp-server @@ -108,8 +108,8 @@ if config['db_name']: openerp.tools.convert_yaml_import(cr, 'base', file(config["test_file"]), {}, 'test', True) cr.rollback() - pool.get('ir.cron')._poolJobs(db.dbname) - # pool.get('ir.cron').restart(db.dbname) # jobs will start to be processed later, when start_agent below is called. + # jobs will start to be processed later, when start_agent below is called. + pool.get('ir.cron').restart(db.dbname) cr.close() diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index ba25f59525d..9360442e922 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -76,6 +76,25 @@ class ir_cron(osv.osv, netsvc.Agent): 'doall' : lambda *a: 1 } + thread_count_lock = threading.Lock() + thread_count = 1 # maximum allowed number of thread. + + @classmethod + def get_thread_count(cls): + return cls.thread_count + + @classmethod + def inc_thread_count(cls): + cls.thread_count_lock.acquire() + cls.thread_count += 1 + cls.thread_count_lock.release() + + @classmethod + def dec_thread_count(cls): + cls.thread_count_lock.acquire() + cls.thread_count -= 1 + cls.thread_count_lock.release() + def f(a, b, c): print ">>> in f" @@ -125,7 +144,11 @@ class ir_cron(osv.osv, netsvc.Agent): self._handle_callback_exception(cr, uid, model, func, args, job_id, e) def _compute_nextcall(self, job, now): - """ Compute the nextcall for a job exactly as _run_job does. """ + """ Compute the nextcall for a job exactly as _run_job does. + + Return either the nextcall or None if it shouldn't be called. + + """ nextcall = datetime.strptime(job['nextcall'], '%Y-%m-%d %H:%M:%S') numbercall = job['numbercall'] @@ -135,10 +158,12 @@ class ir_cron(osv.osv, netsvc.Agent): if numbercall: nextcall += _intervalTypes[job['interval_type']](job['interval_number']) + if not numbercall: + return None return nextcall.strftime('%Y-%m-%d %H:%M:%S') def _run_job(self, cr, job, now): - """ Run a given job. """ + """ Run a given job taking care of the repetition. """ try: nextcall = datetime.strptime(job['nextcall'], '%Y-%m-%d %H:%M:%S') numbercall = job['numbercall'] @@ -156,18 +181,35 @@ class ir_cron(osv.osv, netsvc.Agent): if not numbercall: addsql = ', active=False' cr.execute("update ir_cron set nextcall=%s, numbercall=%s"+addsql+" where id=%s", (nextcall.strftime('%Y-%m-%d %H:%M:%S'), numbercall, job['id'])) - # TODO re-schedule the master thread to nextcall if its wake-up time is later than nextcall. - # TODO NOTIFY the 'ir_cron' channel. + + if numbercall: + # Reschedule our own main cron thread if necessary. + # This is really needed if this job run longer that its rescheduling period. + print ">>> advance at", nextcall + nextcall = time.mktime(time.strptime(nextcall.strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S')) + self.reschedule_in_advance(self._poolJobs, nextcall, cr.dbname, cr.dbname) finally: cr.commit() cr.close() def _poolJobs(self, db_name): + return self._run_jobs(db_name) + + def _run_jobs(self, db_name): # TODO remove 'check' argument from addons/base_action_rule/base_action_rule.py + """ Process the cron jobs by spawning worker threads. + + This selects in database all the jobs that should be processed. It then + try to lock each of them and, if it succeeds, spawn a thread to run the + cron job (if doesn't succeed, it means another the job was already + locked to be taken care of by another thread. + + """ try: db, pool = pooler.get_db_and_pool(db_name) except: return False + print ">>> _run_jobs" cr = db.cursor() try: jobs = {} # mapping job ids to jobs for all jobs being processed. @@ -177,13 +219,16 @@ class ir_cron(osv.osv, netsvc.Agent): for job in cr.dictfetchall(): task_cr = db.cursor() task_job = None + jobs[job['id']] = job try: + # Try to lock the job... task_cr.execute('select * from ir_cron where id=%s for update nowait', (job['id'],), log_exceptions=False) task_job = task_cr.dictfetchall()[0] - jobs[job['id']] = job except psycopg2.OperationalError, e: if e.pgcode == '55P03': # Class 55: Object not in prerequisite state, 55P03: lock_not_available + # ... and fail. + print ">>>", job['name'], " is already being processed" continue else: raise @@ -191,6 +236,8 @@ class ir_cron(osv.osv, netsvc.Agent): if not task_job: task_cr.close() + # ... and succeed. + print ">>> taking care of", job['name'] task_thread = threading.Thread(target=self._run_job, name=task_job['name'], args=(task_cr, task_job, now)) # force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default) task_thread.setDaemon(False) @@ -202,17 +249,23 @@ class ir_cron(osv.osv, netsvc.Agent): else: cr.execute('select min(nextcall) as min_next_call from ir_cron where numbercall<>0 and active') next_call = cr.dictfetchone()['min_next_call'] + print ">>> possibility at ", next_call + + # Wake up time, taking the smallest processed job nextcall value. + for job in jobs.values(): + nextcall = self._compute_nextcall(job, now) + print ">>> or at ", nextcall + if not nextcall: + continue + if not next_call or nextcall < next_call: + next_call = nextcall + print ">>> rescheduling at", next_call + if next_call: next_call = time.mktime(time.strptime(next_call, '%Y-%m-%d %H:%M:%S')) else: next_call = int(time.time()) + 3600 # if do not find active cron job from database, it will run again after 1 day - # Take the smallest nextcall value. - for job in jobs.values(): - nextcall = self._compute_nextcall(job, now) - if nextcall < next_call: - next_call = nextcall - self.setAlarm(self._poolJobs, next_call, db_name, db_name) except Exception, ex: diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 3a2add26421..26f7cbe7875 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -283,6 +283,18 @@ class Agent(object): for task in cls.__tasks_by_db[db_name]: task[0] = 0 + @classmethod + def reschedule_in_advance(cls, function, timestamp, db_name, *args, **kwargs): + # Cancel the previous task if any. + old_timestamp = None + if db_name in cls.__tasks_by_db: + for task in cls.__tasks_by_db[db_name]: + if task[2] == function and timestamp < task[0]: + old_timestamp = task[0] + task[0] = 0 + if not old_timestamp or timestamp < old_timestamp: + cls.setAlarm(function, timestamp, db_name, *args, **kwargs) + @classmethod def quit(cls): cls.cancel(None) From 70b65e0922b75901d594ac782b95464d0f0affa1 Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Mon, 11 Jul 2011 13:30:17 +0530 Subject: [PATCH 032/526] [IMP+FIX] edi_service: fixed wrong method call to get_db into import_edi_url service and return list of tuple (mobel,record_id) from load_edi function. it will be useful to redirect document after import successful bzr revid: hmo@tinyerp.com-20110711080017-chgth42d2409o81m --- openerp/addons/base/ir/ir_edi.py | 8 ++++---- openerp/service/web_services.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 0ee29e13a3a..b37abbb7747 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -122,13 +122,14 @@ class ir_edi_document(osv.osv): :param edi_dicts: list of edi_dict """ - + res = [] for edi_document in edi_documents: model = edi_document.get('__model') assert model, _('model should be provided in EDI Dict') model_obj = self.pool.get(model) - model_obj.edi_import(cr, uid, edi_document, context=context) - return True + record_id = model_obj.edi_import(cr, uid, edi_document, context=context) + res.append((model,record_id)) + return res def deserialize(self, edi_document_string): """ Deserialized the edi document string @@ -175,7 +176,6 @@ class ir_edi_document(osv.osv): if edi_url and not edi_document: edi_document = urllib2.urlopen(edi_url).read() - assert edi_document, _('EDI Document should be provided') edi_documents = self.deserialize(edi_document) return self.load_edi(cr, uid, edi_documents, context=context) diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index 116eefdd1a1..ec2a3c7507e 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -69,7 +69,7 @@ class edi(netsvc.ExportService): def exp_import_edi_url(self, db, uid, passwd, edi_url, context=None): res = None - cr = pooler.get_db_only(db).cursor() + cr = pooler.get_db(db).cursor() pool = pooler.get_pool(db) edi_pool = pool.get('ir.edi.document') try: From 841cc683c6d0fb91f2dde005cc7c42658bd12442 Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Mon, 11 Jul 2011 13:56:03 +0530 Subject: [PATCH 033/526] [IMP] edi: yml of edi document bzr revid: hmo@tinyerp.com-20110711082603-56r1dmtfc2h0lxgt --- openerp/addons/base/test/partner.pdf | 118 ++++++++++++++++++ .../addons/base/test/test_edi_documents.yml | 15 ++- 2 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 openerp/addons/base/test/partner.pdf diff --git a/openerp/addons/base/test/partner.pdf b/openerp/addons/base/test/partner.pdf new file mode 100644 index 00000000000..1b1c7f0daac --- /dev/null +++ b/openerp/addons/base/test/partner.pdf @@ -0,0 +1,118 @@ +%PDF-1.3 +%“Œ‹ž ReportLab Generated PDF document http://www.reportlab.com +% 'BasicFonts': class PDFDictionary +1 0 obj +% The standard fonts dictionary +<< /F1 2 0 R + /F2 3 0 R + /F3 4 0 R >> +endobj +% 'F1': class PDFType1Font +2 0 obj +% Font Helvetica +<< /BaseFont /Helvetica + /Encoding /WinAnsiEncoding + /Name /F1 + /Subtype /Type1 + /Type /Font >> +endobj +% 'F2': class PDFType1Font +3 0 obj +% Font Helvetica-Bold +<< /BaseFont /Helvetica-Bold + /Encoding /WinAnsiEncoding + /Name /F2 + /Subtype /Type1 + /Type /Font >> +endobj +% 'F3': class PDFType1Font +4 0 obj +% Font Times-Roman +<< /BaseFont /Times-Roman + /Encoding /WinAnsiEncoding + /Name /F3 + /Subtype /Type1 + /Type /Font >> +endobj +% 'Page1': class PDFPage +5 0 obj +% Page dictionary +<< /Contents 9 0 R + /MediaBox [ 0 + 0 + 841.8898 + 595.2756 ] + /Parent 8 0 R + /Resources << /Font 1 0 R + /ProcSet [ /PDF + /Text + /ImageB + /ImageC + /ImageI ] >> + /Rotate 0 + /Trans << >> + /Type /Page >> +endobj +% 'R6': class PDFCatalog +6 0 obj +% Document Root +<< /Outlines 10 0 R + /PageMode /UseNone + /Pages 8 0 R + /Type /Catalog >> +endobj +% 'R7': class PDFInfo +7 0 obj +<< /Author (Generated by OpenERP, Fabien Pinckaers) + /CreationDate (20100421145542) + /Keywords () + /Producer (ReportLab http://www.reportlab.com) + /Subject (\(unspecified\)) + /Title (Partner) >> +endobj +% 'R8': class PDFPages +8 0 obj +% page tree +<< /Count 1 + /Kids [ 5 0 R ] + /Type /Pages >> +endobj +% 'R9': class PDFStream +9 0 obj +% page stream +<< /Filter [ /ASCII85Decode + /FlateDecode ] + /Length 524 >> +stream +Gat=(?#P3ie[>S,B2ME\)S7:@oo6@nF8`I+h##OY3a-n#Qm!>5jiD.9pQ@E1#Xj$=)27)F3RmD%fbqZh,T`gJd0/,?A3`0:_VVm`"l`>oOf9p&HGF7>WCGVJoD2&_+c#BDb)/&0>>rL5@]KY$9YOAqZHTJq8k]!o+?4a5\-KbB#NYJC'`~>endstream + +endobj +% 'R10': class PDFOutlines +10 0 obj +<< /Count 0 + /Type /Outlines >> +endobj +xref +0 11 +0000000000 65535 f +0000000113 00000 n +0000000233 00000 n +0000000398 00000 n +0000000573 00000 n +0000000740 00000 n +0000001017 00000 n +0000001152 00000 n +0000001395 00000 n +0000001500 00000 n +0000002169 00000 n +trailer +<< /ID + % ReportLab generated PDF document -- digest (http://www.reportlab.com) + [(\210\344\006bE\230CUUi\010[\)Qif) (\210\344\006bE\230CUUi\010[\)Qif)] + + /Info 7 0 R + /Root 6 0 R + /Size 11 >> +startxref +2221 +%%EOF diff --git a/openerp/addons/base/test/test_edi_documents.yml b/openerp/addons/base/test/test_edi_documents.yml index 85a350bf3a9..03dbe623ede 100644 --- a/openerp/addons/base/test/test_edi_documents.yml +++ b/openerp/addons/base/test/test_edi_documents.yml @@ -1,5 +1,5 @@ - - create partner + I create partner - !record {model: res.partner, id: res_partner_test33}: name: Ivan @@ -17,7 +17,7 @@ company_id: base.main_company - - I am creating one Document to be stored in ". + I am creating one Document to be attached in partner ". - !record {model: 'ir.attachment', id: document_4}: name : "My partner document" @@ -25,7 +25,7 @@ type : binary - - Attach a pdf file in "My partner document" + I attach a pdf file in "My partner document" - !python {model: 'ir.attachment'}: | import addons @@ -34,15 +34,14 @@ self.write(cr, uid, [ref("document_4")], {'res_id':ref("res_partner_test33"), 'datas_fname': 'partner.pdf', 'datas' : base64.encodestring(pdf_file.read())}, context) - - Testing import and export + I am Tesing of EDI functionality. so first export partner and than import it again. - !python {model: ir.edi.document}: | partner_obj = self.pool.get('res.partner') partners = [partner_obj.browse(cr, uid, ref("res_partner_test33"))] tokens = self.export_edi(cr, uid, partners) - for token in tokens: - document = self.get_document(cr, uid, token, context=context) - print document - a = self.import_edi(cr, uid, edi_document = document) + assert tokens, 'Token is not generated.' + document = self.get_document(cr, uid, tokens[0], context=context) + a = self.import_edi(cr, uid, edi_document = document) From 6f5eb6b91e27f1c5aa83fd09badc3bb41fe88e3a Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 13 Jul 2011 15:49:33 +0200 Subject: [PATCH 034/526] [IMP] ir.cron: continued implementing multithreaded cron: - use a lock to protect the number of threads - the not task[0] condition in reschedule_in_advance is not really correct - but we have to remove the Agent in favor of a real cron master thread. bzr revid: vmt@openerp.com-20110713134933-gmfwddot50a3ib4k --- openerp/addons/base/ir/ir_cron.py | 61 ++++++++++++++++--------------- openerp/netsvc.py | 14 +++++-- 2 files changed, 42 insertions(+), 33 deletions(-) diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 9360442e922..0998b43c8b7 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -76,24 +76,18 @@ class ir_cron(osv.osv, netsvc.Agent): 'doall' : lambda *a: 1 } - thread_count_lock = threading.Lock() - thread_count = 1 # maximum allowed number of thread. + def __init__(self, pool, cr): + self.thread_count_lock = threading.Lock() + self.thread_count = 2 # maximum allowed number of thread. + super(osv.osv, self).__init__(pool, cr) - @classmethod - def get_thread_count(cls): - return cls.thread_count + def get_thread_count(self): + return self.thread_count - @classmethod - def inc_thread_count(cls): - cls.thread_count_lock.acquire() - cls.thread_count += 1 - cls.thread_count_lock.release() - - @classmethod - def dec_thread_count(cls): - cls.thread_count_lock.acquire() - cls.thread_count -= 1 - cls.thread_count_lock.release() + def dec_thread_count(self): + self.thread_count_lock.acquire() + self.thread_count -= 1 + self.thread_count_lock.release() def f(a, b, c): print ">>> in f" @@ -105,7 +99,7 @@ class ir_cron(osv.osv, netsvc.Agent): def expensive_2(a, b, c): print ">>> in expensive_2" - time.sleep(80) + time.sleep(30) print ">>> out expensive_2" def _check_args(self, cr, uid, ids, context=None): @@ -187,10 +181,16 @@ class ir_cron(osv.osv, netsvc.Agent): # This is really needed if this job run longer that its rescheduling period. print ">>> advance at", nextcall nextcall = time.mktime(time.strptime(nextcall.strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S')) - self.reschedule_in_advance(self._poolJobs, nextcall, cr.dbname, cr.dbname) + with self.thread_count_lock: + self.reschedule_in_advance(self._poolJobs, nextcall, cr.dbname, cr.dbname) finally: cr.commit() cr.close() + with self.thread_count_lock: + self.thread_count += 1 + # reschedule the master thread in advance, using its saved next_call value. + self.reschedule_in_advance(self._poolJobs, self.next_call, cr.dbname, cr.dbname) + self.next_call = None def _poolJobs(self, db_name): return self._run_jobs(db_name) @@ -210,6 +210,7 @@ class ir_cron(osv.osv, netsvc.Agent): except: return False print ">>> _run_jobs" + self.next_call = None cr = db.cursor() try: jobs = {} # mapping job ids to jobs for all jobs being processed. @@ -217,9 +218,13 @@ class ir_cron(osv.osv, netsvc.Agent): now = datetime.now() cr.execute('select * from ir_cron where numbercall<>0 and active and nextcall<=now() order by priority') for job in cr.dictfetchall(): + print ">>>", self.get_thread_count(), "threads" + if not self.get_thread_count(): + break task_cr = db.cursor() task_job = None jobs[job['id']] = job + try: # Try to lock the job... task_cr.execute('select * from ir_cron where id=%s for update nowait', (job['id'],), log_exceptions=False) @@ -241,6 +246,7 @@ class ir_cron(osv.osv, netsvc.Agent): task_thread = threading.Thread(target=self._run_job, name=task_job['name'], args=(task_cr, task_job, now)) # force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default) task_thread.setDaemon(False) + self.dec_thread_count() task_thread.start() # Wake up time, without considering the currently processed jobs. @@ -251,22 +257,19 @@ class ir_cron(osv.osv, netsvc.Agent): next_call = cr.dictfetchone()['min_next_call'] print ">>> possibility at ", next_call - # Wake up time, taking the smallest processed job nextcall value. - for job in jobs.values(): - nextcall = self._compute_nextcall(job, now) - print ">>> or at ", nextcall - if not nextcall: - continue - if not next_call or nextcall < next_call: - next_call = nextcall - print ">>> rescheduling at", next_call - if next_call: next_call = time.mktime(time.strptime(next_call, '%Y-%m-%d %H:%M:%S')) else: next_call = int(time.time()) + 3600 # if do not find active cron job from database, it will run again after 1 day - self.setAlarm(self._poolJobs, next_call, db_name, db_name) + # avoid race condition: the thread rescheduled the main thread, then the main thread puts +3600. + with self.thread_count_lock: + if not self.thread_count: + print ">>> no more threads" + self.next_call = next_call + next_call = int(time.time()) + 3600 # no available thread, it will run again after 1 day + + self.reschedule_in_advance(self._poolJobs, next_call, db_name, db_name) except Exception, ex: self._logger.warning('Exception in cron:', exc_info=True) diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 26f7cbe7875..5ec114f7ce1 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -285,14 +285,18 @@ class Agent(object): @classmethod def reschedule_in_advance(cls, function, timestamp, db_name, *args, **kwargs): + if not timestamp: + return # Cancel the previous task if any. - old_timestamp = None + old_timestamp = False if db_name in cls.__tasks_by_db: for task in cls.__tasks_by_db[db_name]: - if task[2] == function and timestamp < task[0]: - old_timestamp = task[0] + print ">>> function:", function + if task[2] == function and (not task[0] or timestamp < task[0]): + old_timestamp = True task[0] = 0 - if not old_timestamp or timestamp < old_timestamp: + if old_timestamp or db_name not in cls.__tasks_by_db or not cls.__tasks_by_db[db_name]: + print ">>> rescheduled earlier", timestamp cls.setAlarm(function, timestamp, db_name, *args, **kwargs) @classmethod @@ -306,6 +310,7 @@ class Agent(object): """ current_thread = threading.currentThread() while True: + print ">>>>> starting thread for" while cls.__tasks and cls.__tasks[0][0] < time.time(): task = heapq.heappop(cls.__tasks) timestamp, dbname, function, args, kwargs = task @@ -319,6 +324,7 @@ class Agent(object): task_thread = threading.Thread(target=function, name='netsvc.Agent.task', args=args, kwargs=kwargs) # force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default) task_thread.setDaemon(False) + print ">>>>> -", function.func_name task_thread.start() time.sleep(1) time.sleep(60) From b5daffc115f77d02b8e78ab6c437776f9d91e97c Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Wed, 13 Jul 2011 17:35:21 +0200 Subject: [PATCH 035/526] [IMP] registry: whene deleting a registry, also delete its cache and cron. bzr revid: vmt@openerp.com-20110713153521-isn9bllnggbxwi0z --- openerp-server | 5 +++-- openerp/modules/registry.py | 27 +++++++++++++++++++++++++-- openerp/pooler.py | 5 ----- openerp/service/web_services.py | 4 ++-- openerp/sql_db.py | 3 +-- 5 files changed, 31 insertions(+), 13 deletions(-) diff --git a/openerp-server b/openerp-server index a2ec11ba996..56aa278940a 100755 --- a/openerp-server +++ b/openerp-server @@ -100,7 +100,7 @@ if not ( config["stop_after_init"] or \ if config['db_name']: for dbname in config['db_name'].split(','): - db, pool = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False) + db, registry = openerp.pooler.get_db_and_pool(dbname, update_module=config['init'] or config['update'], pooljobs=False) cr = db.cursor() if config["test_file"]: @@ -109,7 +109,7 @@ if config['db_name']: cr.rollback() # jobs will start to be processed later, when start_agent below is called. - pool.get('ir.cron').restart(db.dbname) + registry.start_cron_thread() cr.close() @@ -218,6 +218,7 @@ def quit(): # and would present the forced shutdown thread.join(0.05) time.sleep(0.05) + openerp.modules.registry.RegistryManager.delete_all() sys.exit(0) if config['pidfile']: diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index 11f484a71a0..cb81284f6fa 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -25,6 +25,8 @@ import openerp.sql_db import openerp.osv.orm +import openerp.netsvc +import openerp.tools class Registry(object): @@ -82,6 +84,9 @@ class Registry(object): return res + def start_cron_thread(self): + self.get('ir.cron').restart(self.db.dbname) + class RegistryManager(object): """ Model registries manager. @@ -143,16 +148,34 @@ class RegistryManager(object): cr.close() if pooljobs: - registry.get('ir.cron').restart(registry.db.dbname) + registry.start_cron_thread() return registry @classmethod def delete(cls, db_name): - """ Delete the registry linked to a given database. """ + """ Delete the registry linked to a given database. + + This also cleans the associated caches. For good measure this also + cancels the associated cron job. But please note that the cron job can + be running and take some time before ending, and that you should not + remove a registry if it can still be used by some thread. So it might + be necessary to call yourself openerp.netsvc.Agent.cancel(db_name) and + and join (i.e. wait for) the thread. + + """ if db_name in cls.registries: del cls.registries[db_name] + openerp.tools.cache.clean_caches_for_db(db_name) + openerp.netsvc.Agent.cancel(db_name) + + + @classmethod + def delete_all(cls): + """ Delete all the registries. """ + for db_name in cls.registries.keys(): + cls.delete(db_name) # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/pooler.py b/openerp/pooler.py index c754385d6b3..88b60864b90 100644 --- a/openerp/pooler.py +++ b/openerp/pooler.py @@ -34,11 +34,6 @@ def get_db_and_pool(db_name, force_demo=False, status=None, update_module=False, return registry.db, registry -def delete_pool(db_name): - """Delete an existing registry.""" - RegistryManager.delete(db_name) - - def restart_pool(db_name, force_demo=False, status=None, update_module=False): """Delete an existing registry and return a database connection and a newly initialized registry.""" registry = RegistryManager.new(db_name, force_demo, status, update_module, True) diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index 40e7ccf1d69..e7757ec1e66 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -161,8 +161,8 @@ class db(netsvc.ExportService): raise Exception, e def exp_drop(self, db_name): + openerp.modules.registry.RegistryManager.delete(db_name) sql_db.close_db(db_name) - openerp.netsvc.Agent.cancel(db_name) logger = netsvc.Logger() db = sql_db.db_connect('template1') @@ -264,8 +264,8 @@ class db(netsvc.ExportService): return True def exp_rename(self, old_name, new_name): + openerp.modules.registry.RegistryManager.delete(old_name) sql_db.close_db(old_name) - openerp.netsvc.Agent.cancel(db_name) logger = netsvc.Logger() db = sql_db.db_connect('template1') diff --git a/openerp/sql_db.py b/openerp/sql_db.py index 43a508f9082..37cb8b224bb 100644 --- a/openerp/sql_db.py +++ b/openerp/sql_db.py @@ -429,9 +429,8 @@ def db_connect(db_name): return Connection(_Pool, db_name) def close_db(db_name): - """ You might want to call openerp.netsvc.Agent.cancel(db_name) along this function.""" + """ You might want to call openerp.modules.registry.RegistryManager.delete(db_name) along this function.""" _Pool.close_all(dsn(db_name)) - tools.cache.clean_caches_for_db(db_name) ct = currentThread() if hasattr(ct, 'dbname'): delattr(ct, 'dbname') From 32e830eb9963ff0fb276352ac87920ca8e2d870c Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 14 Jul 2011 13:08:09 +0200 Subject: [PATCH 036/526] [IMP] cron: removed unnecessary multi-tasks per db in Agent, some cleaning. bzr revid: vmt@openerp.com-20110714110809-sgsoev9i24589sn8 --- openerp-server | 5 +- openerp/addons/base/ir/ir_cron.py | 41 ++++----------- openerp/netsvc.py | 87 +++++++++++++------------------ 3 files changed, 48 insertions(+), 85 deletions(-) diff --git a/openerp-server b/openerp-server index 56aa278940a..d5876111c3b 100755 --- a/openerp-server +++ b/openerp-server @@ -199,7 +199,8 @@ if os.name == 'posix': signal.signal(signal.SIGQUIT, dumpstacks) def quit(): - openerp.netsvc.Agent.quit() + # stop scheduling new jobs; we will have to wait for the jobs to complete below + openerp.netsvc.Agent.cancel_all() openerp.netsvc.Server.quitAll() if config['pidfile']: os.unlink(config['pidfile']) @@ -215,7 +216,7 @@ def quit(): if thread != threading.currentThread() and not thread.isDaemon(): while thread.isAlive(): # need a busyloop here as thread.join() masks signals - # and would present the forced shutdown + # and would prevent the forced shutdown thread.join(0.05) time.sleep(0.05) openerp.modules.registry.RegistryManager.delete_all() diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 0998b43c8b7..61a8b6d1f52 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -76,10 +76,8 @@ class ir_cron(osv.osv, netsvc.Agent): 'doall' : lambda *a: 1 } - def __init__(self, pool, cr): - self.thread_count_lock = threading.Lock() - self.thread_count = 2 # maximum allowed number of thread. - super(osv.osv, self).__init__(pool, cr) + thread_count_lock = threading.Lock() + thread_count = 2 # maximum allowed number of thread. def get_thread_count(self): return self.thread_count @@ -137,25 +135,6 @@ class ir_cron(osv.osv, netsvc.Agent): except Exception, e: self._handle_callback_exception(cr, uid, model, func, args, job_id, e) - def _compute_nextcall(self, job, now): - """ Compute the nextcall for a job exactly as _run_job does. - - Return either the nextcall or None if it shouldn't be called. - - """ - nextcall = datetime.strptime(job['nextcall'], '%Y-%m-%d %H:%M:%S') - numbercall = job['numbercall'] - - while nextcall < now and numbercall: - if numbercall > 0: - numbercall -= 1 - if numbercall: - nextcall += _intervalTypes[job['interval_type']](job['interval_number']) - - if not numbercall: - return None - return nextcall.strftime('%Y-%m-%d %H:%M:%S') - def _run_job(self, cr, job, now): """ Run a given job taking care of the repetition. """ try: @@ -182,20 +161,17 @@ class ir_cron(osv.osv, netsvc.Agent): print ">>> advance at", nextcall nextcall = time.mktime(time.strptime(nextcall.strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S')) with self.thread_count_lock: - self.reschedule_in_advance(self._poolJobs, nextcall, cr.dbname, cr.dbname) + self.schedule_in_advance(nextcall, cr.dbname) finally: cr.commit() cr.close() with self.thread_count_lock: self.thread_count += 1 # reschedule the master thread in advance, using its saved next_call value. - self.reschedule_in_advance(self._poolJobs, self.next_call, cr.dbname, cr.dbname) + self.schedule_in_advance(self.next_call, cr.dbname) self.next_call = None - def _poolJobs(self, db_name): - return self._run_jobs(db_name) - - def _run_jobs(self, db_name): + def _run_jobs(self): # TODO remove 'check' argument from addons/base_action_rule/base_action_rule.py """ Process the cron jobs by spawning worker threads. @@ -205,8 +181,9 @@ class ir_cron(osv.osv, netsvc.Agent): locked to be taken care of by another thread. """ + db_name = self.pool.db.dbname try: - db, pool = pooler.get_db_and_pool(db_name) + db, pool = self.pool.db, self.pool except: return False print ">>> _run_jobs" @@ -269,7 +246,7 @@ class ir_cron(osv.osv, netsvc.Agent): self.next_call = next_call next_call = int(time.time()) + 3600 # no available thread, it will run again after 1 day - self.reschedule_in_advance(self._poolJobs, next_call, db_name, db_name) + self.schedule_in_advance(next_call, db_name) except Exception, ex: self._logger.warning('Exception in cron:', exc_info=True) @@ -286,7 +263,7 @@ class ir_cron(osv.osv, netsvc.Agent): def restart(self, dbname): self.cancel(dbname) # Reschedule cron processing job asap, but not in the current thread - self.setAlarm(self._poolJobs, time.time(), dbname, dbname) + self.schedule_in_advance(time.time(), dbname) def update_running_cron(self, cr): # Verify whether the server is already started and thus whether we need to commit diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 5ec114f7ce1..7ad5778244c 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -37,6 +37,7 @@ from pprint import pformat # TODO modules that import netsvc only for things from loglevels must be changed to use loglevels. from loglevels import * import tools +import openerp def close_socket(sock): """ Closes a socket instance cleanly @@ -252,81 +253,65 @@ class Agent(object): * a timestamp * the database on which the task run - * the function to call - * the arguments and keyword arguments to pass to the function + * a boolean attribute specifying if the task is canceled Implementation details: - Tasks are stored as list, allowing the cancellation by setting - the timestamp to 0. + the boolean to True. - A heapq is used to store tasks, so we don't need to sort tasks ourself. """ - __tasks = [] - __tasks_by_db = {} + _wakeups = [] + _wakeup_by_db = {} _logger = logging.getLogger('netsvc.agent') - @classmethod - def setAlarm(cls, function, timestamp, db_name, *args, **kwargs): - task = [timestamp, db_name, function, args, kwargs] - heapq.heappush(cls.__tasks, task) - cls.__tasks_by_db.setdefault(db_name, []).append(task) - @classmethod def cancel(cls, db_name): - """Cancel all tasks for a given database. If None is passed, all tasks are cancelled""" - cls._logger.debug("Cancel timers for %s db", db_name or 'all') - if db_name is None: - cls.__tasks, cls.__tasks_by_db = [], {} - else: - if db_name in cls.__tasks_by_db: - for task in cls.__tasks_by_db[db_name]: - task[0] = 0 + """ Cancel next wakeup for a given database. """ + cls._logger.debug("Cancel next wake-up for database '%s'.", db_name) + if db_name in cls._wakeup_by_db: + cls._wakeup_by_db[db_name][2] = True @classmethod - def reschedule_in_advance(cls, function, timestamp, db_name, *args, **kwargs): + def cancel_all(cls): + cls._wakeups = [] + cls._wakeup_by_db = {} + + @classmethod + def schedule_in_advance(cls, timestamp, db_name): if not timestamp: return - # Cancel the previous task if any. - old_timestamp = False - if db_name in cls.__tasks_by_db: - for task in cls.__tasks_by_db[db_name]: - print ">>> function:", function - if task[2] == function and (not task[0] or timestamp < task[0]): - old_timestamp = True - task[0] = 0 - if old_timestamp or db_name not in cls.__tasks_by_db or not cls.__tasks_by_db[db_name]: + # Cancel the previous wakeup if any. + add_wakeup = False + if db_name in cls._wakeup_by_db: + task = cls._wakeup_by_db[db_name] + if task[2] or timestamp < task[0]: + add_wakeup = True + task[2] = True + else: + add_wakeup = True + if add_wakeup: print ">>> rescheduled earlier", timestamp - cls.setAlarm(function, timestamp, db_name, *args, **kwargs) - - @classmethod - def quit(cls): - cls.cancel(None) + task = [timestamp, db_name, False] + heapq.heappush(cls._wakeups, task) + cls._wakeup_by_db[db_name] = task @classmethod def runner(cls): """Neverending function (intended to be ran in a dedicated thread) that checks every 60 seconds tasks to run. TODO: make configurable """ - current_thread = threading.currentThread() while True: - print ">>>>> starting thread for" - while cls.__tasks and cls.__tasks[0][0] < time.time(): - task = heapq.heappop(cls.__tasks) - timestamp, dbname, function, args, kwargs = task - cls.__tasks_by_db[dbname].remove(task) - if not timestamp: - # null timestamp -> cancelled task + print ">>>>> cron for" + while cls._wakeups and cls._wakeups[0][0] < time.time(): + task = heapq.heappop(cls._wakeups) + timestamp, db_name, canceled = task + del cls._wakeup_by_db[db_name] + if canceled: continue - current_thread.dbname = dbname # hack hack - cls._logger.debug("Run %s.%s(*%s, **%s)", function.im_class.__name__, function.func_name, args, kwargs) - delattr(current_thread, 'dbname') - task_thread = threading.Thread(target=function, name='netsvc.Agent.task', args=args, kwargs=kwargs) - # force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default) - task_thread.setDaemon(False) - print ">>>>> -", function.func_name - task_thread.start() - time.sleep(1) + ir_cron = openerp.pooler.get_pool(db_name).get('ir.cron') + ir_cron._run_jobs() time.sleep(60) def start_agent(): From 2f115c21aad68d952e7a37e17fda2b69e3824ab1 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 14 Jul 2011 16:32:09 +0200 Subject: [PATCH 037/526] [IMP] cron: moved netsvc.Agent to openerp.cron. bzr revid: vmt@openerp.com-20110714143209-bebn6xg91fcrxro9 --- openerp-server | 4 +- openerp/addons/base/ir/ir_cron.py | 108 ++++++++++++------------------ openerp/modules/registry.py | 6 +- openerp/netsvc.py | 78 --------------------- 4 files changed, 46 insertions(+), 150 deletions(-) diff --git a/openerp-server b/openerp-server index d5876111c3b..f06da60236b 100755 --- a/openerp-server +++ b/openerp-server @@ -153,7 +153,7 @@ if config["translate_in"]: if config["stop_after_init"]: sys.exit(0) -openerp.netsvc.start_agent() +openerp.cron.start_master_thread() #---------------------------------------------------------- # Launch Servers @@ -200,7 +200,7 @@ if os.name == 'posix': def quit(): # stop scheduling new jobs; we will have to wait for the jobs to complete below - openerp.netsvc.Agent.cancel_all() + openerp.cron.cancel_all() openerp.netsvc.Server.quitAll() if config['pidfile']: os.unlink(config['pidfile']) diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 61a8b6d1f52..f1f4547517b 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -30,6 +30,7 @@ import tools from tools.safe_eval import safe_eval as eval import pooler from osv import fields, osv +import openerp def str2tuple(s): return eval('tuple(%s)' % (s or '')) @@ -43,9 +44,8 @@ _intervalTypes = { 'minutes': lambda interval: relativedelta(minutes=interval), } -class ir_cron(osv.osv, netsvc.Agent): +class ir_cron(osv.osv): """ This is the ORM object that periodically executes actions. - Note that we use the netsvc.Agent()._logger member. """ _name = "ir.cron" _order = 'name' @@ -76,16 +76,7 @@ class ir_cron(osv.osv, netsvc.Agent): 'doall' : lambda *a: 1 } - thread_count_lock = threading.Lock() - thread_count = 2 # maximum allowed number of thread. - - def get_thread_count(self): - return self.thread_count - - def dec_thread_count(self): - self.thread_count_lock.acquire() - self.thread_count -= 1 - self.thread_count_lock.release() + _logger = logging.getLogger('cron') def f(a, b, c): print ">>> in f" @@ -160,16 +151,11 @@ class ir_cron(osv.osv, netsvc.Agent): # This is really needed if this job run longer that its rescheduling period. print ">>> advance at", nextcall nextcall = time.mktime(time.strptime(nextcall.strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S')) - with self.thread_count_lock: - self.schedule_in_advance(nextcall, cr.dbname) + openerp.cron.schedule_in_advance(nextcall, cr.dbname) finally: cr.commit() cr.close() - with self.thread_count_lock: - self.thread_count += 1 - # reschedule the master thread in advance, using its saved next_call value. - self.schedule_in_advance(self.next_call, cr.dbname) - self.next_call = None + openerp.cron.inc_thread_count() def _run_jobs(self): # TODO remove 'check' argument from addons/base_action_rule/base_action_rule.py @@ -181,50 +167,45 @@ class ir_cron(osv.osv, netsvc.Agent): locked to be taken care of by another thread. """ - db_name = self.pool.db.dbname - try: - db, pool = self.pool.db, self.pool - except: - return False print ">>> _run_jobs" - self.next_call = None + db = self.pool.db cr = db.cursor() + db_name = db.dbname try: jobs = {} # mapping job ids to jobs for all jobs being processed. - if not pool._init: - now = datetime.now() - cr.execute('select * from ir_cron where numbercall<>0 and active and nextcall<=now() order by priority') - for job in cr.dictfetchall(): - print ">>>", self.get_thread_count(), "threads" - if not self.get_thread_count(): - break - task_cr = db.cursor() - task_job = None - jobs[job['id']] = job + now = datetime.now() + cr.execute('select * from ir_cron where numbercall<>0 and active and nextcall<=now() order by priority') + for job in cr.dictfetchall(): + print ">>>", openerp.cron.get_thread_count(), "threads" + if not openerp.cron.get_thread_count(): + break + task_cr = db.cursor() + task_job = None + jobs[job['id']] = job - try: - # Try to lock the job... - task_cr.execute('select * from ir_cron where id=%s for update nowait', (job['id'],), log_exceptions=False) - task_job = task_cr.dictfetchall()[0] - except psycopg2.OperationalError, e: - if e.pgcode == '55P03': - # Class 55: Object not in prerequisite state, 55P03: lock_not_available - # ... and fail. - print ">>>", job['name'], " is already being processed" - continue - else: - raise - finally: - if not task_job: - task_cr.close() + try: + # Try to lock the job... + task_cr.execute('select * from ir_cron where id=%s for update nowait', (job['id'],), log_exceptions=False) + task_job = task_cr.dictfetchall()[0] + except psycopg2.OperationalError, e: + if e.pgcode == '55P03': + # Class 55: Object not in prerequisite state, 55P03: lock_not_available + # ... and fail. + print ">>>", job['name'], " is already being processed" + continue + else: + raise + finally: + if not task_job: + task_cr.close() - # ... and succeed. - print ">>> taking care of", job['name'] - task_thread = threading.Thread(target=self._run_job, name=task_job['name'], args=(task_cr, task_job, now)) - # force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default) - task_thread.setDaemon(False) - self.dec_thread_count() - task_thread.start() + # ... and succeed. + print ">>> taking care of", job['name'] + task_thread = threading.Thread(target=self._run_job, name=task_job['name'], args=(task_cr, task_job, now)) + # force non-daemon task threads (the runner thread must be daemon, and this property is inherited by default) + task_thread.setDaemon(False) + openerp.cron.dec_thread_count() + task_thread.start() # Wake up time, without considering the currently processed jobs. if jobs.keys(): @@ -239,14 +220,7 @@ class ir_cron(osv.osv, netsvc.Agent): else: next_call = int(time.time()) + 3600 # if do not find active cron job from database, it will run again after 1 day - # avoid race condition: the thread rescheduled the main thread, then the main thread puts +3600. - with self.thread_count_lock: - if not self.thread_count: - print ">>> no more threads" - self.next_call = next_call - next_call = int(time.time()) + 3600 # no available thread, it will run again after 1 day - - self.schedule_in_advance(next_call, db_name) + openerp.cron.schedule_in_advance(next_call, db_name) except Exception, ex: self._logger.warning('Exception in cron:', exc_info=True) @@ -261,9 +235,9 @@ class ir_cron(osv.osv, netsvc.Agent): self.restart(self, dbname) def restart(self, dbname): - self.cancel(dbname) + openerp.cron.cancel(dbname) # Reschedule cron processing job asap, but not in the current thread - self.schedule_in_advance(time.time(), dbname) + openerp.cron.schedule_in_advance(time.time(), dbname) def update_running_cron(self, cr): # Verify whether the server is already started and thus whether we need to commit diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index cb81284f6fa..965ba0079b4 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -25,7 +25,7 @@ import openerp.sql_db import openerp.osv.orm -import openerp.netsvc +import openerp.cron import openerp.tools @@ -161,14 +161,14 @@ class RegistryManager(object): cancels the associated cron job. But please note that the cron job can be running and take some time before ending, and that you should not remove a registry if it can still be used by some thread. So it might - be necessary to call yourself openerp.netsvc.Agent.cancel(db_name) and + be necessary to call yourself openerp.cron.Agent.cancel(db_name) and and join (i.e. wait for) the thread. """ if db_name in cls.registries: del cls.registries[db_name] openerp.tools.cache.clean_caches_for_db(db_name) - openerp.netsvc.Agent.cancel(db_name) + openerp.cron.cancel(db_name) @classmethod diff --git a/openerp/netsvc.py b/openerp/netsvc.py index 7ad5778244c..dc5de9a4354 100644 --- a/openerp/netsvc.py +++ b/openerp/netsvc.py @@ -21,7 +21,6 @@ ############################################################################## import errno -import heapq import logging import logging.handlers import os @@ -245,83 +244,6 @@ def init_alternative_logger(): logger.addHandler(handler) logger.setLevel(logging.ERROR) -class Agent(object): - """ Singleton that keeps track of cancellable tasks to run at a given - timestamp. - - The tasks are characterised by: - - * a timestamp - * the database on which the task run - * a boolean attribute specifying if the task is canceled - - Implementation details: - - - Tasks are stored as list, allowing the cancellation by setting - the boolean to True. - - A heapq is used to store tasks, so we don't need to sort - tasks ourself. - """ - _wakeups = [] - _wakeup_by_db = {} - _logger = logging.getLogger('netsvc.agent') - - @classmethod - def cancel(cls, db_name): - """ Cancel next wakeup for a given database. """ - cls._logger.debug("Cancel next wake-up for database '%s'.", db_name) - if db_name in cls._wakeup_by_db: - cls._wakeup_by_db[db_name][2] = True - - @classmethod - def cancel_all(cls): - cls._wakeups = [] - cls._wakeup_by_db = {} - - @classmethod - def schedule_in_advance(cls, timestamp, db_name): - if not timestamp: - return - # Cancel the previous wakeup if any. - add_wakeup = False - if db_name in cls._wakeup_by_db: - task = cls._wakeup_by_db[db_name] - if task[2] or timestamp < task[0]: - add_wakeup = True - task[2] = True - else: - add_wakeup = True - if add_wakeup: - print ">>> rescheduled earlier", timestamp - task = [timestamp, db_name, False] - heapq.heappush(cls._wakeups, task) - cls._wakeup_by_db[db_name] = task - - @classmethod - def runner(cls): - """Neverending function (intended to be ran in a dedicated thread) that - checks every 60 seconds tasks to run. TODO: make configurable - """ - while True: - print ">>>>> cron for" - while cls._wakeups and cls._wakeups[0][0] < time.time(): - task = heapq.heappop(cls._wakeups) - timestamp, db_name, canceled = task - del cls._wakeup_by_db[db_name] - if canceled: - continue - ir_cron = openerp.pooler.get_pool(db_name).get('ir.cron') - ir_cron._run_jobs() - time.sleep(60) - -def start_agent(): - agent_runner = threading.Thread(target=Agent.runner, name="netsvc.Agent.runner") - # the agent runner is a typical daemon thread, that will never quit and must be - # terminated when the main process exits - with no consequence (the processing - # threads it spawns are not marked daemon) - agent_runner.setDaemon(True) - agent_runner.start() - import traceback class Server: From 4b8708fb103b5580ff3466b774412654bd7eb7a5 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Thu, 14 Jul 2011 17:11:13 +0200 Subject: [PATCH 038/526] [IMP] cron: minor cleaning. bzr revid: vmt@openerp.com-20110714151113-v07yr2rneqafbnni --- openerp-server | 4 ++-- openerp/addons/base/ir/ir_cron.py | 15 ++------------- openerp/modules/registry.py | 18 +++++++++++------- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/openerp-server b/openerp-server index f06da60236b..9e3f36cf443 100755 --- a/openerp-server +++ b/openerp-server @@ -108,8 +108,8 @@ if config['db_name']: openerp.tools.convert_yaml_import(cr, 'base', file(config["test_file"]), {}, 'test', True) cr.rollback() - # jobs will start to be processed later, when start_agent below is called. - registry.start_cron_thread() + # jobs will start to be processed later, when openerp.cron.start_master_thread below is called. + registry.schedule_cron_jobs() cr.close() diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index f1f4547517b..a49e059d526 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -105,8 +105,7 @@ class ir_cron(osv.osv): def _handle_callback_exception(self, cr, uid, model, func, args, job_id, job_exception): cr.rollback() - logger=logging.getLogger('cron') - logger.exception("Call of self.pool.get('%s').%s(cr, uid, *%r) failed in Job %s" % (model, func, args, job_id)) + self._logger.exception("Call of self.pool.get('%s').%s(cr, uid, *%r) failed in Job %s" % (model, func, args, job_id)) def _callback(self, cr, uid, model, func, args, job_id): args = str2tuple(args) @@ -229,16 +228,6 @@ class ir_cron(osv.osv): cr.commit() cr.close() - def restart_all(self): - import openerp.models.registry - for dbname in openerp.models.registry.RegistryManager.registries: - self.restart(self, dbname) - - def restart(self, dbname): - openerp.cron.cancel(dbname) - # Reschedule cron processing job asap, but not in the current thread - openerp.cron.schedule_in_advance(time.time(), dbname) - def update_running_cron(self, cr): # Verify whether the server is already started and thus whether we need to commit # immediately our changes and restart the cron agent in order to apply the change @@ -249,7 +238,7 @@ class ir_cron(osv.osv): # when the server is only starting or loading modules (hence the test on pool._init). if not self.pool._init: cr.commit() - self.restart(cr.dbname) + openerp.cron.schedule_in_advance(1, self.pool.db.dbname) def create(self, cr, uid, vals, context=None): res = super(ir_cron, self).create(cr, uid, vals, context=context) diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index 965ba0079b4..9ed51044793 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -84,8 +84,16 @@ class Registry(object): return res - def start_cron_thread(self): - self.get('ir.cron').restart(self.db.dbname) + def schedule_cron_jobs(self): + """ Make the cron thread care about this registry/database jobs. + + This will initiate the cron thread to check for any pending jobs for + this registry/database as soon as possible. Then it will continously + monitors the ir.cron model for future jobs. See openerp.cron for + details. + + """ + openerp.cron.schedule_in_advance(1, self.db.dbname) class RegistryManager(object): @@ -100,7 +108,6 @@ class RegistryManager(object): # Accessed through the methods below. registries = {} - @classmethod def get(cls, db_name, force_demo=False, status=None, update_module=False, pooljobs=True): @@ -113,7 +120,6 @@ class RegistryManager(object): update_module, pooljobs) return registry - @classmethod def new(cls, db_name, force_demo=False, status=None, update_module=False, pooljobs=True): @@ -148,11 +154,10 @@ class RegistryManager(object): cr.close() if pooljobs: - registry.start_cron_thread() + registry.schedule_cron_jobs() return registry - @classmethod def delete(cls, db_name): """ Delete the registry linked to a given database. @@ -170,7 +175,6 @@ class RegistryManager(object): openerp.tools.cache.clean_caches_for_db(db_name) openerp.cron.cancel(db_name) - @classmethod def delete_all(cls): """ Delete all the registries. """ From db70049e4d4ed37c83d71f3cafc8d5722a27b6ad Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Fri, 15 Jul 2011 14:05:09 +0530 Subject: [PATCH 039/526] [ADD] base: add configuration parameter to store web.base.url bzr revid: hmo@tinyerp.com-20110715083509-j0dyh2dq1j9c9t0q --- openerp/addons/base/base_data.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openerp/addons/base/base_data.xml b/openerp/addons/base/base_data.xml index 3f8208b8ec4..5b9a1e07e7f 100644 --- a/openerp/addons/base/base_data.xml +++ b/openerp/addons/base/base_data.xml @@ -1,6 +1,10 @@ + + web.base.url + http://localhost:8002 + ir.ui.menu.tree ir.ui.menu From 045b42b03a65b6120b2457f8afd64980514fb230 Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Fri, 15 Jul 2011 14:05:56 +0530 Subject: [PATCH 040/526] [FIX] base.ir_edi: value of function fields should be exported on EDI Document bzr revid: hmo@tinyerp.com-20110715083556-u60zjo42ync4d47w --- openerp/addons/base/ir/ir_edi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index b37abbb7747..208f69dabac 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -356,9 +356,9 @@ class edi(object): record = getattr(row, field) if not record: continue - if _fields[field].has_key('function') or _fields[field].has_key('related_columns'): - # Do not Export Function Fields and related fields - continue + #if _fields[field].has_key('function') or _fields[field].has_key('related_columns'): + # # Do not Export Function Fields and related fields + # continue elif cols['type'] == 'many2one': value = self.edi_m2o(cr, uid, record, context=context) elif cols['type'] == 'many2many': From ed910b589822c5849873f0f7c43e5cb1514561bb Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 15 Jul 2011 11:40:51 +0200 Subject: [PATCH 041/526] [IMP] cron: added some code to test the new threaded jobs, should be moved elsewhere. bzr revid: vmt@openerp.com-20110715094051-7z5v2xu91uid0jtm --- openerp/addons/base/ir/ir_cron.py | 53 +++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index a49e059d526..6073489fa8a 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -44,6 +44,21 @@ _intervalTypes = { 'minutes': lambda interval: relativedelta(minutes=interval), } +JOB = { + 'function': u'f', + 'interval_type': u'minutes', + 'user_id': 1, + 'name': u'test', + 'args': False, + 'numbercall': 1, + 'nextcall': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'priority': 5, + 'doall': True, + 'active': True, + 'interval_number': 1, + 'model': u'ir.cron' +} + class ir_cron(osv.osv): """ This is the ORM object that periodically executes actions. """ @@ -240,6 +255,44 @@ class ir_cron(osv.osv): cr.commit() openerp.cron.schedule_in_advance(1, self.pool.db.dbname) + def _20_seconds(self, cr, uid): + print ">>> in _20_seconds" + time.sleep(20) + print ">>> out _20_seconds" + + def _80_seconds(self, cr, uid): + print ">>> in _80_seconds" + time.sleep(80) + print ">>> out _80_seconds" + + def test_0(self, cr, uid): + now = datetime.now() + t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') + t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S') + t3 = (now + relativedelta(minutes=1, seconds=10)).strftime('%Y-%m-%d %H:%M:%S') + self.create(cr, uid, dict(JOB, name='test_0 _20_seconds A', function='_20_seconds', nextcall=t1)) + self.create(cr, uid, dict(JOB, name='test_0 _20_seconds B', function='_20_seconds', nextcall=t2)) + self.create(cr, uid, dict(JOB, name='test_0 _20_seconds C', function='_20_seconds', nextcall=t3)) + + def test_1(self, cr, uid): + now = datetime.now() + t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') + self.create(cr, uid, dict(JOB, name='test_1 _20_seconds * 3', function='_20_seconds', nextcall=t1, numbercall=3)) + + def test_2(self, cr, uid): + now = datetime.now() + t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') + self.create(cr, uid, dict(JOB, name='test_2 _80_seconds * 2', function='_80_seconds', nextcall=t1, numbercall=2)) + + def test_3(self, cr, uid): + now = datetime.now() + t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') + t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S') + t3 = (now + relativedelta(minutes=1, seconds=10)).strftime('%Y-%m-%d %H:%M:%S') + self.create(cr, uid, dict(JOB, name='test_3 _80_seconds A', function='_80_seconds', nextcall=t1)) + self.create(cr, uid, dict(JOB, name='test_3 _20_seconds B', function='_20_seconds', nextcall=t2)) + self.create(cr, uid, dict(JOB, name='test_3 _20_seconds C', function='_20_seconds', nextcall=t3)) + def create(self, cr, uid, vals, context=None): res = super(ir_cron, self).create(cr, uid, vals, context=context) self.update_running_cron(cr) From 1d7eb0cdc25ce45a0d48881f5732f1b010d47ff1 Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Fri, 15 Jul 2011 15:20:59 +0530 Subject: [PATCH 042/526] [FIX] service: put autocommit true on EDI import service to store imported data bzr revid: hmo@tinyerp.com-20110715095059-96rrh2pu4acevzj4 --- openerp/addons/base/ir/ir_edi.py | 3 +-- openerp/service/web_services.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 208f69dabac..7347625c622 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -519,6 +519,5 @@ class edi(object): values[field] = one2many_ids else: values[field] = edi_field_value - - return model_data._update(cr, uid, self._name, edi_module, values, context=context) + return self.pool.get(self._name).create(cr, uid, values, context=context) # vim: ts=4 sts=4 sw=4 si et diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index ec2a3c7507e..58b31ede7ff 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -62,6 +62,7 @@ class edi(netsvc.ExportService): pool = pooler.get_pool(db) edi_pool = pool.get('ir.edi.document') try: + cr.autocommit(True) res = edi_pool.import_edi(cr, uid, edi_document=edi_document, context=context) finally: cr.close() @@ -73,6 +74,7 @@ class edi(netsvc.ExportService): pool = pooler.get_pool(db) edi_pool = pool.get('ir.edi.document') try: + cr.autocommit(True) res = edi_pool.import_edi(cr, uid, edi_url=edi_url, context=context) finally: cr.close() From f6b44ec779df0aee08837552be9d242ffa733c3c Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 15 Jul 2011 12:01:27 +0200 Subject: [PATCH 043/526] [IMP] cron: forgot to add the new openerp.cron module. bzr revid: vmt@openerp.com-20110715100127-8btlo3bluaju3em6 --- openerp/cron.py | 162 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 openerp/cron.py diff --git a/openerp/cron.py b/openerp/cron.py new file mode 100644 index 00000000000..3271e131f01 --- /dev/null +++ b/openerp/cron.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2011 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 . +# +############################################################################## + +""" Cron jobs scheduling + +Cron jobs are defined in the ir_cron table/model. This module deals with all +cron jobs, for all databases of a single OpenERP server instance. + +It defines a single master thread that will spawn (a bounded number of) +threads to process individual cron jobs. + +""" + +import heapq +import logging +import threading +import time + +import openerp + +""" Singleton that keeps track of cancellable tasks to run at a given + timestamp. + + The tasks are characterised by: + + * a timestamp + * the database on which the task run + * a boolean attribute specifying if the task is canceled + + Implementation details: + + - Tasks are stored as list, allowing the cancellation by setting + the boolean to True. + - A heapq is used to store tasks, so we don't need to sort + tasks ourself. +""" + +# Heapq of database wake-ups. Note that 'database wake-up' meaning is in +# the context of the cron management. This is not about loading a database +# or otherwise making anything about it. +_wakeups = [] # TODO protect this variable with a lock? + +# Mapping of database names to the wake-up defined in the heapq, +# so that we can cancel the wake-up without messing with the heapq +# internal structure. +_wakeup_by_db = {} + +_logger = logging.getLogger('cron') + +_thread_count_lock = threading.Lock() + +# Maximum number of threads allowed to process cron jobs concurrently. +_thread_count = 2 + + +def get_thread_count(): + return _thread_count + + +def inc_thread_count(): + global _thread_count + with _thread_count_lock: + _thread_count += 1 + + +def dec_thread_count(): + global _thread_count + with _thread_count_lock: + _thread_count -= 1 + + +def cancel(db_name): + """ Cancel the next wake-up of a given database, if any. """ + _logger.debug("Cancel next wake-up for database '%s'.", db_name) + if db_name in _wakeup_by_db: + _wakeup_by_db[db_name][2] = True + + +def cancel_all(): + """ Cancel all database wake-ups. """ + global _wakeups + global _wakeup_by_db + _wakeups = [] + _wakeup_by_db = {} + + +def schedule_in_advance(timestamp, db_name): + """ Schedule a wake-up for a new database. + + If an earlier wake-up is already defined, the new wake-up is discarded. + If another wake-up is defined, it is discarded. + + """ + if not timestamp: + return + # Cancel the previous wakeup if any. + add_wakeup = False + if db_name in _wakeup_by_db: + task = _wakeup_by_db[db_name] + if task[2] or timestamp < task[0]: + add_wakeup = True + task[2] = True + else: + add_wakeup = True + if add_wakeup: + task = [timestamp, db_name, False] + heapq.heappush(_wakeups, task) + _wakeup_by_db[db_name] = task + + +def runner(): + """Neverending function (intended to be ran in a dedicated thread) that + checks every 60 seconds the next database wake-up. TODO: make configurable + """ + while True: + while _wakeups and _wakeups[0][0] < time.time() and get_thread_count(): + task = heapq.heappop(_wakeups) + timestamp, db_name, canceled = task + if canceled: + continue + task[2] = True + registry = openerp.pooler.get_pool(db_name) + if not registry._init: + registry['ir.cron']._run_jobs() + if _wakeups and get_thread_count(): + time.sleep(min(60, _wakeups[0][0] - time.time())) + else: + time.sleep(60) + + +def start_master_thread(): + """ Start the above runner function in a daemon thread. + + The thread is a typical daemon thread: it will never quit and must be + terminated when the main process exits - with no consequence (the processing + threads it spawns are not marked daemon). + + """ + t = threading.Thread(target=runner, name="openerp.cron.master_thread") + t.setDaemon(True) + t.start() + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 69259d01711f41349f251cbc610087c9a3acfc0b Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Fri, 15 Jul 2011 16:27:39 +0530 Subject: [PATCH 044/526] [FIX] base.ir_edi: call _update method of ir.model.data on import bzr revid: hmo@tinyerp.com-20110715105739-6vvwjlmghh19pjsu --- openerp/addons/base/ir/ir_edi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 7347625c622..1d13b84aedc 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -519,5 +519,5 @@ class edi(object): values[field] = one2many_ids else: values[field] = edi_field_value - return self.pool.get(self._name).create(cr, uid, values, context=context) + return model_data._update(cr, uid, self._name, edi_module, values, context=context) # vim: ts=4 sts=4 sw=4 si et From faf2863a3595c9a655a5e7152f2a351e3965e479 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Fri, 15 Jul 2011 13:38:45 +0200 Subject: [PATCH 045/526] [IMP] cron: bracketed the jobs heap/dict with a lock. bzr revid: vmt@openerp.com-20110715113845-zokj6cf6z0adj6h4 --- openerp/addons/base/ir/ir_cron.py | 2 +- openerp/cron.py | 68 ++++++++++++++++++------------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 6073489fa8a..2cdb15b4990 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -164,7 +164,7 @@ class ir_cron(osv.osv): # Reschedule our own main cron thread if necessary. # This is really needed if this job run longer that its rescheduling period. print ">>> advance at", nextcall - nextcall = time.mktime(time.strptime(nextcall.strftime('%Y-%m-%d %H:%M:%S'), '%Y-%m-%d %H:%M:%S')) + nextcall = time.mktime(nextcall.timetuple()) openerp.cron.schedule_in_advance(nextcall, cr.dbname) finally: cr.commit() diff --git a/openerp/cron.py b/openerp/cron.py index 3271e131f01..79347fbe178 100644 --- a/openerp/cron.py +++ b/openerp/cron.py @@ -66,6 +66,11 @@ _wakeup_by_db = {} _logger = logging.getLogger('cron') +# We could use a simple (non-reentrant) lock if the runner function below +# was more fine-grained, but we are fine with the loop owning the lock +# while spawning a few threads. +_wakeups_lock = threading.RLock() + _thread_count_lock = threading.Lock() # Maximum number of threads allowed to process cron jobs concurrently. @@ -91,16 +96,18 @@ def dec_thread_count(): def cancel(db_name): """ Cancel the next wake-up of a given database, if any. """ _logger.debug("Cancel next wake-up for database '%s'.", db_name) - if db_name in _wakeup_by_db: - _wakeup_by_db[db_name][2] = True + with _wakeups_lock: + if db_name in _wakeup_by_db: + _wakeup_by_db[db_name][2] = True def cancel_all(): """ Cancel all database wake-ups. """ global _wakeups global _wakeup_by_db - _wakeups = [] - _wakeup_by_db = {} + with _wakeups_lock: + _wakeups = [] + _wakeup_by_db = {} def schedule_in_advance(timestamp, db_name): @@ -112,19 +119,20 @@ def schedule_in_advance(timestamp, db_name): """ if not timestamp: return - # Cancel the previous wakeup if any. - add_wakeup = False - if db_name in _wakeup_by_db: - task = _wakeup_by_db[db_name] - if task[2] or timestamp < task[0]: + with _wakeups_lock: + # Cancel the previous wakeup if any. + add_wakeup = False + if db_name in _wakeup_by_db: + task = _wakeup_by_db[db_name] + if task[2] or timestamp < task[0]: + add_wakeup = True + task[2] = True + else: add_wakeup = True - task[2] = True - else: - add_wakeup = True - if add_wakeup: - task = [timestamp, db_name, False] - heapq.heappush(_wakeups, task) - _wakeup_by_db[db_name] = task + if add_wakeup: + task = [timestamp, db_name, False] + heapq.heappush(_wakeups, task) + _wakeup_by_db[db_name] = task def runner(): @@ -132,19 +140,21 @@ def runner(): checks every 60 seconds the next database wake-up. TODO: make configurable """ while True: - while _wakeups and _wakeups[0][0] < time.time() and get_thread_count(): - task = heapq.heappop(_wakeups) - timestamp, db_name, canceled = task - if canceled: - continue - task[2] = True - registry = openerp.pooler.get_pool(db_name) - if not registry._init: - registry['ir.cron']._run_jobs() - if _wakeups and get_thread_count(): - time.sleep(min(60, _wakeups[0][0] - time.time())) - else: - time.sleep(60) + with _wakeups_lock: + while _wakeups and _wakeups[0][0] < time.time() and get_thread_count(): + task = heapq.heappop(_wakeups) + timestamp, db_name, canceled = task + if canceled: + continue + task[2] = True + registry = openerp.pooler.get_pool(db_name) + if not registry._init: + registry['ir.cron']._run_jobs() + amount = 60 + with _wakeups_lock: + if _wakeups and get_thread_count(): + amount = min(60, _wakeups[0][0] - time.time()) + time.sleep(amount) def start_master_thread(): From ed1b2a92cad8058a30fd8ba65ef9137b9cd23554 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 8 Aug 2011 15:05:02 +0200 Subject: [PATCH 046/526] [IMP] cron: comments and docstrings. bzr revid: vmt@openerp.com-20110808130502-htps768jts63m9sx --- openerp/addons/base/ir/ir_cron.py | 2 +- openerp/cron.py | 62 +++++++++++++++++-------------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 2cdb15b4990..04486cafdd0 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -162,7 +162,7 @@ class ir_cron(osv.osv): if numbercall: # Reschedule our own main cron thread if necessary. - # This is really needed if this job run longer that its rescheduling period. + # This is really needed if this job runs longer than its rescheduling period. print ">>> advance at", nextcall nextcall = time.mktime(nextcall.timetuple()) openerp.cron.schedule_in_advance(nextcall, cr.dbname) diff --git a/openerp/cron.py b/openerp/cron.py index 79347fbe178..f6e8a5d49a2 100644 --- a/openerp/cron.py +++ b/openerp/cron.py @@ -28,6 +28,12 @@ cron jobs, for all databases of a single OpenERP server instance. It defines a single master thread that will spawn (a bounded number of) threads to process individual cron jobs. +The thread runs forever, checking every 60 seconds for new +'database wake-ups'. It maintains a heapq of database wake-ups. At each +wake-up, it will call ir_cron._run_jobs() for the given database. _run_jobs +will check the jobs defined in the ir_cron table and spawn accordingly threads +to process them. + """ import heapq @@ -37,64 +43,61 @@ import time import openerp -""" Singleton that keeps track of cancellable tasks to run at a given - timestamp. - - The tasks are characterised by: - - * a timestamp - * the database on which the task run - * a boolean attribute specifying if the task is canceled - - Implementation details: - - - Tasks are stored as list, allowing the cancellation by setting - the boolean to True. - - A heapq is used to store tasks, so we don't need to sort - tasks ourself. -""" - # Heapq of database wake-ups. Note that 'database wake-up' meaning is in # the context of the cron management. This is not about loading a database # or otherwise making anything about it. -_wakeups = [] # TODO protect this variable with a lock? +# Each element is a triple (timestamp, database-name, boolean). The boolean +# specifies if the wake-up is canceled (so a wake-up can be canceled without +# relying on the heapq implementation detail; no need to remove the job from +# the heapq). +_wakeups = [] # Mapping of database names to the wake-up defined in the heapq, # so that we can cancel the wake-up without messing with the heapq -# internal structure. +# internal structure: lookup the wake-up by database-name, then set +# its third element to True. _wakeup_by_db = {} -_logger = logging.getLogger('cron') - +# Re-entrant lock to protect the above _wakeups and _wakeup_by_db variables. # We could use a simple (non-reentrant) lock if the runner function below # was more fine-grained, but we are fine with the loop owning the lock # while spawning a few threads. _wakeups_lock = threading.RLock() +# Maximum number of threads allowed to process cron jobs concurrently. +_thread_count = 2 # TODO make it configurable + +# A (non re-entrant) lock to protect the above _thread_count variable. _thread_count_lock = threading.Lock() -# Maximum number of threads allowed to process cron jobs concurrently. -_thread_count = 2 +_logger = logging.getLogger('cron') def get_thread_count(): + """ Return the number of available threads. """ return _thread_count def inc_thread_count(): + """ Increment by the number of available threads. """ global _thread_count with _thread_count_lock: _thread_count += 1 def dec_thread_count(): + """ Decrement by the number of available threads. """ global _thread_count with _thread_count_lock: _thread_count -= 1 def cancel(db_name): - """ Cancel the next wake-up of a given database, if any. """ + """ Cancel the next wake-up of a given database, if any. + + :param db_name: database name for which the wake-up is canceled. + + """ _logger.debug("Cancel next wake-up for database '%s'.", db_name) with _wakeups_lock: if db_name in _wakeup_by_db: @@ -111,16 +114,20 @@ def cancel_all(): def schedule_in_advance(timestamp, db_name): - """ Schedule a wake-up for a new database. + """ Schedule a new wake-up for a database. If an earlier wake-up is already defined, the new wake-up is discarded. - If another wake-up is defined, it is discarded. + If another wake-up is defined, that wake-up is discarded and the new one + is scheduled. + + :param db_name: database name for which a new wake-up is scheduled. + :param timestamp: when the wake-up is scheduled. """ if not timestamp: return with _wakeups_lock: - # Cancel the previous wakeup if any. + # Cancel the previous wake-up if any. add_wakeup = False if db_name in _wakeup_by_db: task = _wakeup_by_db[db_name] @@ -152,6 +159,7 @@ def runner(): registry['ir.cron']._run_jobs() amount = 60 with _wakeups_lock: + # Sleep less than 60s if the next known wake-up will happen before. if _wakeups and get_thread_count(): amount = min(60, _wakeups[0][0] - time.time()) time.sleep(amount) From d803d9192be06bc10a86ceb6054ab6ba2ce1dd4f Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 8 Aug 2011 15:54:53 +0200 Subject: [PATCH 047/526] [IMP] cron: the maximum number of cron threads is configurable. bzr revid: vmt@openerp.com-20110808135453-qdlhkyupb6803jln --- openerp/conf/__init__.py | 6 ++++++ openerp/cron.py | 14 +++++--------- openerp/tools/config.py | 8 +++++++- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/openerp/conf/__init__.py b/openerp/conf/__init__.py index 7de8070f9e2..bc8ee98ad43 100644 --- a/openerp/conf/__init__.py +++ b/openerp/conf/__init__.py @@ -32,4 +32,10 @@ of paths. import deprecation +# Maximum number of threads processing concurrently cron jobs. +# Access to this variable must be thread-safe; they have to be done +# through the functions in openerp.cron. +max_cron_threads = 4 # Actually the default value here is meaningless, + # look at tools.config for the default value. + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/cron.py b/openerp/cron.py index f6e8a5d49a2..8879c73ce62 100644 --- a/openerp/cron.py +++ b/openerp/cron.py @@ -64,10 +64,8 @@ _wakeup_by_db = {} # while spawning a few threads. _wakeups_lock = threading.RLock() -# Maximum number of threads allowed to process cron jobs concurrently. -_thread_count = 2 # TODO make it configurable - -# A (non re-entrant) lock to protect the above _thread_count variable. +# A (non re-entrant) lock to protect the openerp.conf.max_cron_threads +# variable. _thread_count_lock = threading.Lock() _logger = logging.getLogger('cron') @@ -75,21 +73,19 @@ _logger = logging.getLogger('cron') def get_thread_count(): """ Return the number of available threads. """ - return _thread_count + return openerp.conf.max_cron_threads def inc_thread_count(): """ Increment by the number of available threads. """ - global _thread_count with _thread_count_lock: - _thread_count += 1 + openerp.conf.max_cron_threads += 1 def dec_thread_count(): """ Decrement by the number of available threads. """ - global _thread_count with _thread_count_lock: - _thread_count -= 1 + openerp.conf.max_cron_threads -= 1 def cancel(db_name): diff --git a/openerp/tools/config.py b/openerp/tools/config.py index 18f40e0011f..af8ad8cdbc7 100644 --- a/openerp/tools/config.py +++ b/openerp/tools/config.py @@ -24,6 +24,7 @@ import optparse import os import sys import openerp +import openerp.conf import openerp.loglevels as loglevels import logging import openerp.release as release @@ -250,6 +251,9 @@ class configmanager(object): "osv_memory tables. This is a decimal value expressed in hours, " "and the default is 1 hour.", type="float") + group.add_option("--max-cron-threads", dest="max_cron_threads", my_default=4, + help="Maximum number of threads processing concurrently cron jobs.", + type="int") parser.add_option_group(group) # Copy all optparse options (i.e. MyOption) into self.options. @@ -335,7 +339,7 @@ class configmanager(object): 'stop_after_init', 'logrotate', 'without_demo', 'netrpc', 'xmlrpc', 'syslog', 'list_db', 'xmlrpcs', 'test_file', 'test_disable', 'test_commit', 'test_report_directory', - 'osv_memory_count_limit', 'osv_memory_age_limit', + 'osv_memory_count_limit', 'osv_memory_age_limit', 'max_cron_threads', ] for arg in keys: @@ -417,6 +421,8 @@ class configmanager(object): if opt.save: self.save() + openerp.conf.max_cron_threads = self.options['max_cron_threads'] + def _generate_pgpassfile(self): """ Generate the pgpass file with the parameters from the command line (db_host, db_user, From f23ec137ca93456a2975582a30ccba792cebbcf5 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 8 Aug 2011 16:05:24 +0200 Subject: [PATCH 048/526] [IMP] cron: the thread-safe variable is located inside openerp.cron; the configuration variable is just read once. bzr revid: vmt@openerp.com-20110808140524-xj8sdm43upp4jr64 --- openerp/conf/__init__.py | 2 -- openerp/cron.py | 20 +++++++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/openerp/conf/__init__.py b/openerp/conf/__init__.py index bc8ee98ad43..6ff832b6551 100644 --- a/openerp/conf/__init__.py +++ b/openerp/conf/__init__.py @@ -33,8 +33,6 @@ of paths. import deprecation # Maximum number of threads processing concurrently cron jobs. -# Access to this variable must be thread-safe; they have to be done -# through the functions in openerp.cron. max_cron_threads = 4 # Actually the default value here is meaningless, # look at tools.config for the default value. diff --git a/openerp/cron.py b/openerp/cron.py index 8879c73ce62..7f96a7d499d 100644 --- a/openerp/cron.py +++ b/openerp/cron.py @@ -34,6 +34,9 @@ wake-up, it will call ir_cron._run_jobs() for the given database. _run_jobs will check the jobs defined in the ir_cron table and spawn accordingly threads to process them. +This module behavior depends on the following configuration variable: +openerp.conf.max_cron_threads. + """ import heapq @@ -64,8 +67,11 @@ _wakeup_by_db = {} # while spawning a few threads. _wakeups_lock = threading.RLock() -# A (non re-entrant) lock to protect the openerp.conf.max_cron_threads -# variable. +# Maximum number of threads allowed to process cron jobs concurrently. This +# variable is set by start_master_thread using openerp.conf.max_cron_threads. +_thread_count = None + +# A (non re-entrant) lock to protect the above _thread_count variable. _thread_count_lock = threading.Lock() _logger = logging.getLogger('cron') @@ -73,19 +79,21 @@ _logger = logging.getLogger('cron') def get_thread_count(): """ Return the number of available threads. """ - return openerp.conf.max_cron_threads + return _thread_count def inc_thread_count(): """ Increment by the number of available threads. """ + global _thread_count with _thread_count_lock: - openerp.conf.max_cron_threads += 1 + _thread_count += 1 def dec_thread_count(): """ Decrement by the number of available threads. """ + global _thread_count with _thread_count_lock: - openerp.conf.max_cron_threads -= 1 + _thread_count -= 1 def cancel(db_name): @@ -169,6 +177,8 @@ def start_master_thread(): threads it spawns are not marked daemon). """ + global _thread_count + _thread_count = openerp.conf.max_cron_threads t = threading.Thread(target=runner, name="openerp.cron.master_thread") t.setDaemon(True) t.start() From 711554a4db33ee726ca820b5b7d7917d44cc3711 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 8 Aug 2011 16:59:31 +0200 Subject: [PATCH 049/526] [IMP] cron: comments, docstrings, help messages. bzr revid: vmt@openerp.com-20110808145931-ms0vpvg8uj72kyd0 --- openerp/addons/base/ir/ir_cron.py | 76 ++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 22 deletions(-) diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 04486cafdd0..84c9c5e837c 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -60,7 +60,7 @@ JOB = { } class ir_cron(osv.osv): - """ This is the ORM object that periodically executes actions. + """ Model describing cron jobs (also called actions or tasks). """ _name = "ir.cron" _order = 'name' @@ -71,13 +71,13 @@ class ir_cron(osv.osv): 'interval_number': fields.integer('Interval Number',help="Repeat every x."), 'interval_type': fields.selection( [('minutes', 'Minutes'), ('hours', 'Hours'), ('work_days','Work Days'), ('days', 'Days'),('weeks', 'Weeks'), ('months', 'Months')], 'Interval Unit'), - 'numbercall': fields.integer('Number of Calls', help='Number of time the function is called,\na negative number indicates no limit'), - 'doall' : fields.boolean('Repeat Missed', help="Enable this if you want to execute missed occurences as soon as the server restarts."), - 'nextcall' : fields.datetime('Next Execution Date', required=True, help="Next planned execution date for this scheduler"), - 'model': fields.char('Object', size=64, help="Name of object whose function will be called when this scheduler will run. e.g. 'res.partener'"), - 'function': fields.char('Function', size=64, help="Name of the method to be called on the object when this scheduler is executed."), - 'args': fields.text('Arguments', help="Arguments to be passed to the method. e.g. (uid,)"), - 'priority': fields.integer('Priority', help='0=Very Urgent\n10=Not urgent') + 'numbercall': fields.integer('Number of Calls', help='How many times the method is called,\na negative number indicates no limit.'), + 'doall' : fields.boolean('Repeat Missed', help="Specify if missed occurrences should be executed when the server restarts."), + 'nextcall' : fields.datetime('Next Execution Date', required=True, help="Next planned execution date for this job."), + 'model': fields.char('Object', size=64, help="Model name on which the method to be called is located, e.g. 'res.partner'."), + 'function': fields.char('Method', size=64, help="Name of the method to be called when this job is processed."), + 'args': fields.text('Arguments', help="Arguments to be passed to the method, e.g. (uid,)."), + 'priority': fields.integer('Priority', help='The priority of the job, as an integer: 0 means higher priority, 10 means lower priority.') } _defaults = { @@ -118,30 +118,58 @@ class ir_cron(osv.osv): (_check_args, 'Invalid arguments', ['args']), ] - def _handle_callback_exception(self, cr, uid, model, func, args, job_id, job_exception): - cr.rollback() - self._logger.exception("Call of self.pool.get('%s').%s(cr, uid, *%r) failed in Job %s" % (model, func, args, job_id)) + def _handle_callback_exception(self, cr, uid, model_name, method_name, args, job_id, job_exception): + """ Method called when an exception is raised by a job. - def _callback(self, cr, uid, model, func, args, job_id): + Simply logs the exception and rollback the transaction. + + :param model_name: model name on which the job method is located. + :param method_name: name of the method to call when this job is processed. + :param args: arguments of the method (without the usual self, cr, uid). + :param job_id: job id. + :param job_exception: exception raised by the job. + + """ + cr.rollback() + self._logger.exception("Call of self.pool.get('%s').%s(cr, uid, *%r) failed in Job %s" % (model_name, method_name, args, job_id)) + + def _callback(self, cr, uid, model_name, method_name, args, job_id): + """ Run the method associated to a given job + + It takes care of logging and exception handling. + + :param model_name: model name on which the job method is located. + :param method_name: name of the method to call when this job is processed. + :param args: arguments of the method (without the usual self, cr, uid). + :param job_id: job id. + """ args = str2tuple(args) - m = self.pool.get(model) - if m and hasattr(m, func): - f = getattr(m, func) + model = self.pool.get(model_name) + if model and hasattr(model, method_name): + method = getattr(model, method_name) try: - netsvc.log('cron', (cr.dbname,uid,'*',model,func)+tuple(args), channel=logging.DEBUG, + netsvc.log('cron', (cr.dbname,uid,'*',model_name,method_name)+tuple(args), channel=logging.DEBUG, depth=(None if self._logger.isEnabledFor(logging.DEBUG_RPC_ANSWER) else 1), fn='object.execute') logger = logging.getLogger('execution time') if logger.isEnabledFor(logging.DEBUG): start_time = time.time() - f(cr, uid, *args) + method(cr, uid, *args) if logger.isEnabledFor(logging.DEBUG): end_time = time.time() - logger.log(logging.DEBUG, '%.3fs (%s, %s)' % (end_time - start_time, model, func)) + logger.log(logging.DEBUG, '%.3fs (%s, %s)' % (end_time - start_time, model_name, method_name)) except Exception, e: - self._handle_callback_exception(cr, uid, model, func, args, job_id, e) + self._handle_callback_exception(cr, uid, model_name, method_name, args, job_id, e) def _run_job(self, cr, job, now): - """ Run a given job taking care of the repetition. """ + """ Run a given job taking care of the repetition. + + The cursor has a lock on the job (aquired by _run_jobs()) and this + method is run in a worker thread (spawned by _run_jobs())). + + :param job: job to be run (as a dictionary). + :param now: timestamp (result of datetime.now(), no need to call it multiple time). + + """ try: nextcall = datetime.strptime(job['nextcall'], '%Y-%m-%d %H:%M:%S') numbercall = job['numbercall'] @@ -176,10 +204,13 @@ class ir_cron(osv.osv): """ Process the cron jobs by spawning worker threads. This selects in database all the jobs that should be processed. It then - try to lock each of them and, if it succeeds, spawn a thread to run the - cron job (if doesn't succeed, it means another the job was already + tries to lock each of them and, if it succeeds, spawns a thread to run + the cron job (if it doesn't succeed, it means the job was already locked to be taken care of by another thread. + The cursor used to lock the job in database is given to the worker + thread (which has to close it itself). + """ print ">>> _run_jobs" db = self.pool.db @@ -244,6 +275,7 @@ class ir_cron(osv.osv): cr.close() def update_running_cron(self, cr): + """ Schedule as soon as possible a wake-up for this database. """ # Verify whether the server is already started and thus whether we need to commit # immediately our changes and restart the cron agent in order to apply the change # immediately. The commit() is needed because as soon as the cron is (re)started it From a7e6a5bd4885366b89a42384e87746568d38a464 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 8 Aug 2011 17:20:24 +0200 Subject: [PATCH 050/526] [IMP] cron: moved testing code to some other file. That code is useful for manual testing but is not called by the normal testing framework. Its import is commented out. bzr revid: vmt@openerp.com-20110808152024-23plfx82d35ok8ua --- openerp/addons/base/__init__.py | 1 + openerp/addons/base/ir/ir_cron.py | 67 ------------------ openerp/addons/base/test/__init__.py | 25 +++++++ openerp/addons/base/test/test_ir_cron.py | 87 ++++++++++++++++++++++++ 4 files changed, 113 insertions(+), 67 deletions(-) create mode 100644 openerp/addons/base/test/__init__.py create mode 100644 openerp/addons/base/test/test_ir_cron.py diff --git a/openerp/addons/base/__init__.py b/openerp/addons/base/__init__.py index 847bef71f8c..16a61521fb4 100644 --- a/openerp/addons/base/__init__.py +++ b/openerp/addons/base/__init__.py @@ -24,6 +24,7 @@ import module import res import publisher_warranty import report +import test # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 84c9c5e837c..25393d50146 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -44,21 +44,6 @@ _intervalTypes = { 'minutes': lambda interval: relativedelta(minutes=interval), } -JOB = { - 'function': u'f', - 'interval_type': u'minutes', - 'user_id': 1, - 'name': u'test', - 'args': False, - 'numbercall': 1, - 'nextcall': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), - 'priority': 5, - 'doall': True, - 'active': True, - 'interval_number': 1, - 'model': u'ir.cron' -} - class ir_cron(osv.osv): """ Model describing cron jobs (also called actions or tasks). """ @@ -93,19 +78,6 @@ class ir_cron(osv.osv): _logger = logging.getLogger('cron') - def f(a, b, c): - print ">>> in f" - - def expensive(a, b, c): - print ">>> in expensive" - time.sleep(80) - print ">>> out expensive" - - def expensive_2(a, b, c): - print ">>> in expensive_2" - time.sleep(30) - print ">>> out expensive_2" - def _check_args(self, cr, uid, ids, context=None): try: for this in self.browse(cr, uid, ids, context): @@ -287,44 +259,6 @@ class ir_cron(osv.osv): cr.commit() openerp.cron.schedule_in_advance(1, self.pool.db.dbname) - def _20_seconds(self, cr, uid): - print ">>> in _20_seconds" - time.sleep(20) - print ">>> out _20_seconds" - - def _80_seconds(self, cr, uid): - print ">>> in _80_seconds" - time.sleep(80) - print ">>> out _80_seconds" - - def test_0(self, cr, uid): - now = datetime.now() - t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') - t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S') - t3 = (now + relativedelta(minutes=1, seconds=10)).strftime('%Y-%m-%d %H:%M:%S') - self.create(cr, uid, dict(JOB, name='test_0 _20_seconds A', function='_20_seconds', nextcall=t1)) - self.create(cr, uid, dict(JOB, name='test_0 _20_seconds B', function='_20_seconds', nextcall=t2)) - self.create(cr, uid, dict(JOB, name='test_0 _20_seconds C', function='_20_seconds', nextcall=t3)) - - def test_1(self, cr, uid): - now = datetime.now() - t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') - self.create(cr, uid, dict(JOB, name='test_1 _20_seconds * 3', function='_20_seconds', nextcall=t1, numbercall=3)) - - def test_2(self, cr, uid): - now = datetime.now() - t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') - self.create(cr, uid, dict(JOB, name='test_2 _80_seconds * 2', function='_80_seconds', nextcall=t1, numbercall=2)) - - def test_3(self, cr, uid): - now = datetime.now() - t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') - t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S') - t3 = (now + relativedelta(minutes=1, seconds=10)).strftime('%Y-%m-%d %H:%M:%S') - self.create(cr, uid, dict(JOB, name='test_3 _80_seconds A', function='_80_seconds', nextcall=t1)) - self.create(cr, uid, dict(JOB, name='test_3 _20_seconds B', function='_20_seconds', nextcall=t2)) - self.create(cr, uid, dict(JOB, name='test_3 _20_seconds C', function='_20_seconds', nextcall=t3)) - def create(self, cr, uid, vals, context=None): res = super(ir_cron, self).create(cr, uid, vals, context=context) self.update_running_cron(cr) @@ -342,4 +276,3 @@ class ir_cron(osv.osv): ir_cron() # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: - diff --git a/openerp/addons/base/test/__init__.py b/openerp/addons/base/test/__init__.py new file mode 100644 index 00000000000..2be9ed76bb4 --- /dev/null +++ b/openerp/addons/base/test/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2011-TODAY 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 +# 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 . +# +############################################################################## + +# Useful for manual testing of cron jobs scheduling. +# import test_ir_cron + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/test/test_ir_cron.py b/openerp/addons/base/test/test_ir_cron.py new file mode 100644 index 00000000000..2fe28e7c2ea --- /dev/null +++ b/openerp/addons/base/test/test_ir_cron.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2011-TODAY 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 +# 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 datetime import datetime + +import openerp + +JOB = { + 'function': u'f', + 'interval_type': u'minutes', + 'user_id': 1, + 'name': u'test', + 'args': False, + 'numbercall': 1, + 'nextcall': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'priority': 5, + 'doall': True, + 'active': True, + 'interval_number': 1, + 'model': u'ir.cron' +} + +class test_ir_cron(openerp.osv.osv.osv): + """ Add a few handy methods to test cron jobs scheduling. """ + _inherit = "ir.cron" + + def _0_seconds(a, b, c): + print ">>> _0_seconds" + + def _20_seconds(self, cr, uid): + print ">>> in _20_seconds" + time.sleep(20) + print ">>> out _20_seconds" + + def _80_seconds(self, cr, uid): + print ">>> in _80_seconds" + time.sleep(80) + print ">>> out _80_seconds" + + def test_0(self, cr, uid): + now = datetime.now() + t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') + t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S') + t3 = (now + relativedelta(minutes=1, seconds=10)).strftime('%Y-%m-%d %H:%M:%S') + self.create(cr, uid, dict(JOB, name='test_0 _20_seconds A', function='_20_seconds', nextcall=t1)) + self.create(cr, uid, dict(JOB, name='test_0 _20_seconds B', function='_20_seconds', nextcall=t2)) + self.create(cr, uid, dict(JOB, name='test_0 _20_seconds C', function='_20_seconds', nextcall=t3)) + + def test_1(self, cr, uid): + now = datetime.now() + t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') + self.create(cr, uid, dict(JOB, name='test_1 _20_seconds * 3', function='_20_seconds', nextcall=t1, numbercall=3)) + + def test_2(self, cr, uid): + now = datetime.now() + t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') + self.create(cr, uid, dict(JOB, name='test_2 _80_seconds * 2', function='_80_seconds', nextcall=t1, numbercall=2)) + + def test_3(self, cr, uid): + now = datetime.now() + t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') + t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S') + t3 = (now + relativedelta(minutes=1, seconds=10)).strftime('%Y-%m-%d %H:%M:%S') + self.create(cr, uid, dict(JOB, name='test_3 _80_seconds A', function='_80_seconds', nextcall=t1)) + self.create(cr, uid, dict(JOB, name='test_3 _20_seconds B', function='_20_seconds', nextcall=t2)) + self.create(cr, uid, dict(JOB, name='test_3 _20_seconds C', function='_20_seconds', nextcall=t3)) + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + From ade53b64c7f69843e481262858935916bd922f47 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Mon, 8 Aug 2011 17:29:08 +0200 Subject: [PATCH 051/526] [IMP] cron: minor typo and comments. bzr revid: vmt@openerp.com-20110808152908-1v12zc7elj03mtnf --- openerp/addons/base/ir/ir_cron.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/ir/ir_cron.py b/openerp/addons/base/ir/ir_cron.py index 25393d50146..fdfb6adc26a 100644 --- a/openerp/addons/base/ir/ir_cron.py +++ b/openerp/addons/base/ir/ir_cron.py @@ -178,7 +178,7 @@ class ir_cron(osv.osv): This selects in database all the jobs that should be processed. It then tries to lock each of them and, if it succeeds, spawns a thread to run the cron job (if it doesn't succeed, it means the job was already - locked to be taken care of by another thread. + locked to be taken care of by another thread). The cursor used to lock the job in database is given to the worker thread (which has to close it itself). @@ -206,11 +206,12 @@ class ir_cron(osv.osv): task_job = task_cr.dictfetchall()[0] except psycopg2.OperationalError, e: if e.pgcode == '55P03': - # Class 55: Object not in prerequisite state, 55P03: lock_not_available - # ... and fail. + # Class 55: Object not in prerequisite state; 55P03: lock_not_available + # ... and fail (in a good way for our purpose). print ">>>", job['name'], " is already being processed" continue else: + # ... and fail (badly). raise finally: if not task_job: From e93d018a394ed09c1e8420052b24c5c6c15dd568 Mon Sep 17 00:00:00 2001 From: Vo Minh Thu Date: Tue, 9 Aug 2011 13:10:08 +0200 Subject: [PATCH 052/526] [IMP] cron: added yaml test. The test should probably be a standalone program as the YAML infrastructure isnt really suited for this purpose. bzr revid: vmt@openerp.com-20110809111008-vxh0bm08n3drw1o2 --- openerp/addons/base/__openerp__.py | 4 ++ openerp/addons/base/test/__init__.py | 2 + openerp/addons/base/test/test_ir_cron.py | 31 +++++++++++- openerp/addons/base/test/test_ir_cron.yml | 61 +++++++++++++++++++++++ openerp/cron.py | 35 +++++++------ openerp/modules/registry.py | 4 +- 6 files changed, 118 insertions(+), 19 deletions(-) create mode 100644 openerp/addons/base/test/test_ir_cron.yml diff --git a/openerp/addons/base/__openerp__.py b/openerp/addons/base/__openerp__.py index 275b8317c0a..8e936a1566a 100644 --- a/openerp/addons/base/__openerp__.py +++ b/openerp/addons/base/__openerp__.py @@ -95,6 +95,10 @@ 'test/test_osv_expression.yml', 'test/test_ir_rule.yml', # <-- These tests modify/add/delete ir_rules. 'test/test_ir_values.yml', + # Commented because this takes some time. + # This must be (un)commented with the corresponding import statement + # in test/__init__.py. + # 'test/test_ir_cron.yml', # <-- These tests perform a roolback. ], 'installable': True, 'active': True, diff --git a/openerp/addons/base/test/__init__.py b/openerp/addons/base/test/__init__.py index 2be9ed76bb4..c0cc8f7cb78 100644 --- a/openerp/addons/base/test/__init__.py +++ b/openerp/addons/base/test/__init__.py @@ -20,6 +20,8 @@ ############################################################################## # Useful for manual testing of cron jobs scheduling. +# This must be (un)commented with the corresponding yml file +# in ../__openerp__.py. # import test_ir_cron # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/test/test_ir_cron.py b/openerp/addons/base/test/test_ir_cron.py index 2fe28e7c2ea..6ef79ac3a78 100644 --- a/openerp/addons/base/test/test_ir_cron.py +++ b/openerp/addons/base/test/test_ir_cron.py @@ -19,12 +19,14 @@ # ############################################################################## +import time from datetime import datetime +from dateutil.relativedelta import relativedelta import openerp JOB = { - 'function': u'f', + 'function': u'_0_seconds', 'interval_type': u'minutes', 'user_id': 1, 'name': u'test', @@ -83,5 +85,32 @@ class test_ir_cron(openerp.osv.osv.osv): self.create(cr, uid, dict(JOB, name='test_3 _20_seconds B', function='_20_seconds', nextcall=t2)) self.create(cr, uid, dict(JOB, name='test_3 _20_seconds C', function='_20_seconds', nextcall=t3)) + # This test assumes 4 cron threads. + def test_00(self, cr, uid): + self.test_00_set = set() + now = datetime.now() + t1 = (now + relativedelta(minutes=1)).strftime('%Y-%m-%d %H:%M:%S') + t2 = (now + relativedelta(minutes=1, seconds=5)).strftime('%Y-%m-%d %H:%M:%S') + t3 = (now + relativedelta(minutes=1, seconds=10)).strftime('%Y-%m-%d %H:%M:%S') + self.create(cr, uid, dict(JOB, name='test_00 _20_seconds_A', function='_20_seconds_A', nextcall=t1)) + self.create(cr, uid, dict(JOB, name='test_00 _20_seconds_B', function='_20_seconds_B', nextcall=t2)) + self.create(cr, uid, dict(JOB, name='test_00 _20_seconds_C', function='_20_seconds_C', nextcall=t3)) + + def _expect(self, cr, uid, to_add, to_sleep, to_expect_in, to_expect_out): + assert self.test_00_set == to_expect_in + self.test_00_set.add(to_add) + time.sleep(to_sleep) + self.test_00_set.discard(to_add) + assert self.test_00_set == to_expect_out + + def _20_seconds_A(self, cr, uid): + self._expect(cr, uid, 'A', 20, set(), set(['B', 'C'])) + + def _20_seconds_B(self, cr, uid): + self._expect(cr, uid, 'B', 20, set('A'), set('C')) + + def _20_seconds_C(self, cr, uid): + self._expect(cr, uid, 'C', 20, set(['A', 'B']), set()) + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/openerp/addons/base/test/test_ir_cron.yml b/openerp/addons/base/test/test_ir_cron.yml new file mode 100644 index 00000000000..5b6e36ae4ae --- /dev/null +++ b/openerp/addons/base/test/test_ir_cron.yml @@ -0,0 +1,61 @@ +- + Test the cron jobs scheduling. +- + Disable the existing cron jobs if any during the tests. +- + !python {model: ir.cron }: | + # For this test to work, as it involves multiple database cursors, + # we have to commit changes. But YAML tests must be rollbacked, so + # the final database state is left untouched. So we have to be a bit + # ugly here: use our own cursor, commit, and clean after ourselves. + # We also pass around some ids using setattr/delattr, and we have to + # rollback the previous tests otherwise we won't be able to touch the + # db. + # Well, this should probably be a standalone, or regular unit test, + # instead of using the YAML infrastructure. + cr.rollback() + our_cr = self.pool.db.cursor() + try: + ids = self.search(our_cr, uid, [], {}) + setattr(self, 'saved_ids', ids) + self.write(our_cr, uid, ids, {'active': False}, {}) + our_cr.commit() + finally: + our_cr.close() +- + Three concurrent jobs started with a slight time gap. Assume 4 cron threads. + This will take about 2 minutes. +- + !python {model: ir.cron }: | + # Pretend initialization is already done. We the use a try/finally + # to reset _init correctly. + self.pool._init = False + our_cr = self.pool.db.cursor() + try: + self.test_00(our_cr, uid) # this will commit using the passed cursor + import openerp.cron + openerp.cron._thread_count = 4 + # Wake up this db as soon as the master cron thread starts. + openerp.cron.schedule_in_advance(1, self.pool.db.dbname) + # Pretend to be the master thread, for 4 iterations. + openerp.cron.runner_body() + openerp.cron.runner_body() + openerp.cron.runner_body() + openerp.cron.runner_body() + finally: + self.pool._init = True + our_cr.close() +- + Clean after ourselves. +- + !python {model: ir.cron }: | + our_cr = self.pool.db.cursor() + try: + ids = [x for x in self.search(our_cr, uid, ['|', ('active', '=', True), ('active', '=', False)], {}) if x not in self.saved_ids] + self.unlink(our_cr, uid, ids, {}) + ids = self.saved_ids + delattr(self, 'saved_ids') + self.write(our_cr, uid, ids, {'active': True}, {}) + our_cr.commit() + finally: + our_cr.close() diff --git a/openerp/cron.py b/openerp/cron.py index 7f96a7d499d..64edf04208d 100644 --- a/openerp/cron.py +++ b/openerp/cron.py @@ -151,22 +151,25 @@ def runner(): checks every 60 seconds the next database wake-up. TODO: make configurable """ while True: - with _wakeups_lock: - while _wakeups and _wakeups[0][0] < time.time() and get_thread_count(): - task = heapq.heappop(_wakeups) - timestamp, db_name, canceled = task - if canceled: - continue - task[2] = True - registry = openerp.pooler.get_pool(db_name) - if not registry._init: - registry['ir.cron']._run_jobs() - amount = 60 - with _wakeups_lock: - # Sleep less than 60s if the next known wake-up will happen before. - if _wakeups and get_thread_count(): - amount = min(60, _wakeups[0][0] - time.time()) - time.sleep(amount) + runner_body() + +def runner_body(): + with _wakeups_lock: + while _wakeups and _wakeups[0][0] < time.time() and get_thread_count(): + task = heapq.heappop(_wakeups) + timestamp, db_name, canceled = task + if canceled: + continue + task[2] = True + registry = openerp.pooler.get_pool(db_name) + if not registry._init: + registry['ir.cron']._run_jobs() + amount = 60 + with _wakeups_lock: + # Sleep less than 60s if the next known wake-up will happen before. + if _wakeups and get_thread_count(): + amount = min(60, _wakeups[0][0] - time.time()) + time.sleep(amount) def start_master_thread(): diff --git a/openerp/modules/registry.py b/openerp/modules/registry.py index 9ed51044793..ae6495dba5f 100644 --- a/openerp/modules/registry.py +++ b/openerp/modules/registry.py @@ -88,8 +88,8 @@ class Registry(object): """ Make the cron thread care about this registry/database jobs. This will initiate the cron thread to check for any pending jobs for - this registry/database as soon as possible. Then it will continously - monitors the ir.cron model for future jobs. See openerp.cron for + this registry/database as soon as possible. Then it will continuously + monitor the ir.cron model for future jobs. See openerp.cron for details. """ From 7417f09da100f46f77dc78d75af3ef5b69d5ff1e Mon Sep 17 00:00:00 2001 From: "Hardik Ansodariy (OpenERP)" Date: Fri, 19 Aug 2011 18:54:22 +0530 Subject: [PATCH 053/526] [IMP]: made changes and removed unwanted code bzr revid: han@tinyerp.com-20110819132422-5frpi82rdhczpykb --- openerp/addons/base/ir/ir_edi.py | 18 +++++++++++++----- openerp/addons/base/res/partner/partner.py | 4 ++-- openerp/service/web_services.py | 6 ++---- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 225df8d9944..44d6d8f4303 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -26,6 +26,8 @@ import time import base64 import urllib2 import openerp.release as release +from tools.translate import _ +import netsvc edi_module = 'edi_import' @@ -122,13 +124,19 @@ class ir_edi_document(osv.osv): :param edi_dicts: list of edi_dict """ + module_obj =self.pool.get('ir.module.module') res = [] for edi_document in edi_documents: - model = edi_document.get('__model') - assert model, _('model should be provided in EDI Dict') - model_obj = self.pool.get(model) - record_id = model_obj.edi_import(cr, uid, edi_document, context=context) - res.append((model,record_id)) + module = edi_document.get('__module') + module_id = module_obj.search(cr, uid, [('name','=',module),('state','=','installed')]) + if module_id: + model = edi_document.get('__model') + assert model, _('model should be provided in EDI Dict') + model_obj = self.pool.get(model) + record_id = model_obj.edi_import(cr, uid, edi_document, context=context) + res.append((model,record_id)) + else: + raise osv.except_osv(_('Invalid action !'), _('Module is not Installed')) return res def deserialize(self, edi_document_string): diff --git a/openerp/addons/base/res/partner/partner.py b/openerp/addons/base/res/partner/partner.py index de8fce3c5e9..e1e71f4996d 100644 --- a/openerp/addons/base/res/partner/partner.py +++ b/openerp/addons/base/res/partner/partner.py @@ -111,7 +111,7 @@ class res_partner(osv.osv): _order = "name" _columns = { 'name': fields.char('Name', size=128, required=True, select=True), - 'opt_out':fields.boolean('Opt-out'), + 'opt_out':fields.boolean('Opt-out', help='After enable opt-out, Automated Mail will not be sent to provided email address of partner'), 'date': fields.date('Date', select=1), 'title': fields.many2one('res.partner.title','Partner Firm'), 'parent_id': fields.many2one('res.partner','Parent Partner'), @@ -150,7 +150,7 @@ class res_partner(osv.osv): 'customer': lambda *a: 1, 'category_id': _default_category, 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'res.partner', context=c), - 'opt_out' : False + 'opt_out' : True } def copy(self, cr, uid, id, default={}, context={}): name = self.read(cr, uid, [id], ['name'])[0]['name'] diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index 58b31ede7ff..0e8acc6bb7a 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -40,10 +40,8 @@ from cStringIO import StringIO import threading class edi(netsvc.ExportService): - def exp_get_edi_document(self, edi_token, db_name=None): + def exp_get_edi_document(self, edi_token, db_name): res = None - if not db_name: - db_name = getattr(threading.currentThread(), 'dbname', None) if db_name: cr = pooler.get_db(db_name).cursor() else: @@ -74,8 +72,8 @@ class edi(netsvc.ExportService): pool = pooler.get_pool(db) edi_pool = pool.get('ir.edi.document') try: - cr.autocommit(True) res = edi_pool.import_edi(cr, uid, edi_url=edi_url, context=context) + cr.commit() finally: cr.close() return res From fc5bf7ae26b6e7cea45cae8e072bc78ee1dd28c3 Mon Sep 17 00:00:00 2001 From: "Hardik Ansodariy (OpenERP)" Date: Tue, 23 Aug 2011 17:06:32 +0530 Subject: [PATCH 054/526] [IMP] bzr revid: han@tinyerp.com-20110823113632-3k859oxwq5k7kwzw --- openerp/addons/base/ir/ir_edi.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 44d6d8f4303..443a1d12ec6 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -129,14 +129,14 @@ class ir_edi_document(osv.osv): for edi_document in edi_documents: module = edi_document.get('__module') module_id = module_obj.search(cr, uid, [('name','=',module),('state','=','installed')]) - if module_id: + if not module_id: + raise Exception("The document you are trying to import requires the OpenERP" +module+ "application") + else: model = edi_document.get('__model') assert model, _('model should be provided in EDI Dict') model_obj = self.pool.get(model) record_id = model_obj.edi_import(cr, uid, edi_document, context=context) res.append((model,record_id)) - else: - raise osv.except_osv(_('Invalid action !'), _('Module is not Installed')) return res def deserialize(self, edi_document_string): @@ -323,7 +323,7 @@ class edi(object): dict_list = [] for record in records: - dict_list.append(self.edi_o2m(cr, uid, [record], context=None)) + dict_list.append(self.edi_m2o(cr, uid, [record], context=None)) return dict_list From 2b0109060c081d9a2b80d6317b0d2fb8a925083b Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Mon, 29 Aug 2011 13:17:32 +0530 Subject: [PATCH 055/526] [IMP] ir.edi: improve edi_import_relation and edi_xml_id with better xml_id, uuid bzr revid: hmo@tinyerp.com-20110829074732-lq2q9k2wh2ywnooy --- openerp/addons/base/ir/ir_edi.py | 117 +++++++++++++++---------------- openerp/service/web_services.py | 2 + 2 files changed, 58 insertions(+), 61 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 9af5bb325f8..2fd11338ffb 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -29,8 +29,6 @@ import openerp.release as release from tools.translate import _ import netsvc -edi_module = 'edi_import' - def safe_unique_id(database_id, model, record_id): """Generate a unique string to represent a (database id,model,record_id) pair without being too long, without revealing the database id, and @@ -42,7 +40,7 @@ def safe_unique_id(database_id, model, record_id): # finally, use the b64-encoded folded digest as ID part of the unique ID: digest = base64.urlsafe_b64encode(digest) - return '%s-%s' % (model,digest) + return '%s-%s' % (model.replace('.','_'), digest) class ir_edi_document(osv.osv): _name = 'ir.edi.document' @@ -185,6 +183,15 @@ class edi(object): ``edi_import()`` and ``edi_export()`` methods to implement their specific behavior, based on the primitives provided by this superclass.""" + def record_xml_id(self, cr, uid, record, context=None): + model_data_pool = self.pool.get('ir.model.data') + data_ids = model_data_pool.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)]) + if not data_ids: + return False + xml_record_id = data_ids[0] + xml_record = model_data_pool.browse(cr, uid, xml_record_id, context=context) + return xml_record.module, xml_record.name + def edi_xml_id(self, cr, uid, record, context=None): """ Generate a unique string to represent a pair of (the database's UUID, the XML ID). @@ -206,20 +213,21 @@ class edi(object): model_data_pool = self.pool.get('ir.model.data') db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid') - data_ids = model_data_pool.search(cr, uid, [('res_id','=',record.id),('model','=',record._name)]) - if len(data_ids): - xml_record_id = data_ids[0] - xml_record = model_data_pool.browse(cr, uid, xml_record_id, context=context) - uuid = '%s:%s' % (db_uuid, xml_record.name) + _record_xml = self.record_xml_id(cr, uid, record, context=context) + if _record_xml: + module, xml_id = _record_xml + xml_id = '%s:%s.%s' % (db_uuid, module, xml_id) else: - xml_id = safe_unique_id(db_uuid, record._name, record.id) - uuid = '%s:%s' % (db_uuid, xml_id) + uuid = safe_unique_id(db_uuid, record._name, record.id) + xml_id = '%s:%s.%s' % (db_uuid, record._module, uuid) + assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id) + module, xml_id2 = xml_id.split('.') xml_record_id = model_data_pool.create(cr, uid, { - 'name': xml_id, + 'name': xml_id2, 'model': record._name, - 'module': uuid, + 'module': module, 'res_id': record.id}, context=context) - return uuid + return xml_id def edi_metadata(self, cr, uid, records, context=None): """Return a list representing the boilerplate EDI structure for @@ -385,14 +393,29 @@ class edi(object): edi_dict_list.append(edi_dict) return edi_dict_list - def edi_import_relation(self, cr, uid, relation_model, relation_value, values={}, context=None): + def edi_import_relation(self, cr, uid, relation_model, relation_value, xml_id=None, context=None): + relation_id = False + model_data = self.pool.get('ir.model.data') relation_object = self.pool.get(relation_model) relation_ids = relation_object.name_search(cr, uid, relation_value, context=context) if relation_ids and len(relation_ids) == 1: relation_id = relation_ids[0][0] - else: - values.update({'name': relation_value}) + values = {relation_object._rec_name: relation_value} + if not relation_id: relation_id = relation_object.create(cr, uid, values, context=context) + + relation_record = relation_object.browse(cr, uid, relation_id, context=context) + record_xml = self.record_xml_id(cr, uid, relation_record, context=context) + if record_xml: + module, xml_id = record_xml + xml_id = '%s.%s' % (module, xml_id) + if not xml_id: + xml_id = self.edi_xml_id(cr, uid, relation_record, context=context) + if xml_id: + assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id) + module, xml_id2 = xml_id.split('.') + #TODO: update record from values + #relation_id = model_data._update(cr, uid, relation_model, module, values, xml_id=xml_id, context=context) return relation_id def edi_import(self, cr, uid, edi_document, context=None): @@ -443,24 +466,28 @@ class edi(object): connect it to the parent record via a write value like (4, db_id). """ # generic implementation! - fields = edi_document.keys() fields_to_import = [] data_line = [] model_data = self.pool.get('ir.model.data') _fields = self.fields_get(cr, uid, context=context) values = {} + xml_id = edi_document['__id'] + assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id) + module, xml_id2 = xml_id.split('.') for field in edi_document.keys(): if not field.startswith('__'): fields_to_import.append(field) edi_field_value = edi_document[field] + field_type = _fields[field]['type'] + relation_model = _fields[field].get('relation') if not edi_field_value: continue if _fields[field].has_key('function') or _fields[field].has_key('related_columns'): # DO NOT IMPORT FUNCTION FIELD AND RELATED FIELD continue - elif _fields[field]['type'] in ('many2one', 'many2many'): - if _fields[field]['type'] == 'many2one': + elif field_type in ('many2one', 'many2many'): + if field_type == 'many2one': edi_parent_documents = [edi_field_value] else: edi_parent_documents = edi_field_value @@ -468,41 +495,19 @@ class edi(object): parent_lines = [] for edi_parent_document in edi_parent_documents: - #Look in ir.model.data for a record that matches the db_id. - #If found, replace the m2o value with the correct database ID and stop. - #If not found, continue to next step - xml_id = edi_parent_document[0] - data_ids = model_data.name_search(cr, uid, xml_id) - if data_ids: - for data in model_data.browse(cr, uid, [data_ids[0][0]], context=context): - parent_lines.append(data.res_id) - else: - #Perform name_search(name) to look for a record that matches the - #given m2o name. If only one record is found, create the missing - #ir.model.data record to link it to the db_id, and the replace the m2o - #value with the correct database ID, then stop. If zero result or - #multiple results are found, go to next step. - #Create the new record using the only field value that is known: the - #name, and create the ir.model.data entry to map to it. - relation_model = _fields[field]['relation'] - relation_id = self.edi_import_relation(cr, uid, relation_model, edi_parent_document[1], context=context) - relation_object = self.pool.get(relation_model) - relation_record = relation_object.browse(cr, uid, relation_id, context=context) - self.edi_xml_id(cr, uid, relation_record, context=context) - - parent_lines.append(relation_id) - + relation_id = self.edi_import_relation(cr, uid, relation_model, edi_parent_document[1], edi_parent_document[0], context=context) + parent_lines.append(relation_id) if len(parent_lines): - if _fields[field]['type'] == 'many2one': + if field_type == 'many2one': values[field] = parent_lines[0] else: many2many_ids = [] for m2m_id in parent_lines: - many2many_ids.append((4,m2m_id)) + many2many_ids.append((4, m2m_id)) values[field] = many2many_ids - elif _fields[field]['type'] == 'one2many': + elif field_type == 'one2many': #Look for a record that matches the db_id provided in the __id field. If #found, keep the corresponding database id, and connect it to the parent #using a write value like (4,db_id). @@ -511,22 +516,12 @@ class edi(object): #and use it to connect to the parent via a write value like (4, db_id). relations = [] - relation_object = self.pool.get(_fields[field]['relation']) + relation_object = self.pool.get(relation_model) for edi_relation_document in edi_field_value: - if edi_relation_document['__id'].find(':') != -1: - db_uuid, xml_id = tuple(edi_relation_document['__id'].split(':')) - data_ids = model_data.name_search(cr, uid, xml_id) - if data_ids: - for data in model_data.browse(cr,uid,[data_ids[0][0]]): - relations.append(data.res_id) - else: - r = relation_object.edi_import(cr, uid, edi_relation_document, context=context) - relations.append(r) - one2many_ids = [] - for o2m_id in relations: - one2many_ids.append((4,o2m_id)) - values[field] = one2many_ids + relation_id = relation_object.edi_import(cr, uid, edi_relation_document, context=context) + relations.append((4, relation_id)) + values[field] = relations else: values[field] = edi_field_value - return model_data._update(cr, uid, self._name, edi_module, values, context=context) + return model_data._update(cr, uid, self._name, module, values, xml_id=xml_id, context=context) # vim: ts=4 sts=4 sw=4 si et diff --git a/openerp/service/web_services.py b/openerp/service/web_services.py index f6b4ff23df0..5be6629bb49 100644 --- a/openerp/service/web_services.py +++ b/openerp/service/web_services.py @@ -63,6 +63,7 @@ class edi(netsvc.ExportService): res = edi_pool.import_edi(cr, uid, edi_document=edi_document, context=context) cr.commit() except: + print traceback.format_exc() cr.rollback() finally: cr.close() @@ -77,6 +78,7 @@ class edi(netsvc.ExportService): res = edi_pool.import_edi(cr, uid, edi_url=edi_url, context=context) cr.commit() except: + print traceback.format_exc() cr.rollback() finally: cr.close() From 75e0749542e51ce76fa29a9d2f4b6a02cb4067d1 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 1 Sep 2011 20:12:54 +0200 Subject: [PATCH 056/526] [IMP] ir.values: separated defaults and actions API, added different views This should prepare for a future split of the two parts currently composing ir.values entries: user defaults and action bindings to a model's UI. bzr revid: odo@openerp.com-20110901181254-g14lyeogs0wv23bz --- openerp/addons/base/ir/ir.xml | 130 +++-- openerp/addons/base/ir/ir_values.py | 511 ++++++++++++++------ openerp/addons/base/test/test_ir_values.yml | 3 + 3 files changed, 415 insertions(+), 229 deletions(-) diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index 011ad437cca..eda044336df 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -7,44 +7,50 @@ ir.values.form.action ir.values form - 20 - - + + - + + - + - + + + + + + - - + + ir.values.form.defaults + ir.values + form + +
+ + + + + + + - - - - + + + - - - - - - - - - - - +
@@ -53,11 +59,11 @@ ir.values tree - + - + @@ -73,7 +79,7 @@ - + @@ -81,23 +87,21 @@ - Client Events + Action Bindings ir.actions.act_window ir.values form tree,form [('key','=','action')] - {'read':'default','default_object':1} + {'default_object':1,'default_key':'action'} - tree - form @@ -105,54 +109,27 @@ - - - - - - - ir.values.form - ir.values - form - -
- - - - - - - - - - - - -
- - - ir.values.tree - ir.values - tree - - - - - - - - - - - - - - Client Actions Connections + + User-defined Defaults ir.actions.act_window ir.values form - - {'read':'default'} + tree,form + + [('key','=','default')] + {'default_object':0,'default_key':'default','default_key2':''} + + + + tree + + + + + + form + + @@ -342,6 +319,7 @@ + diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py index c2a59ada55e..c75831f9c2f 100644 --- a/openerp/addons/base/ir/ir_values.py +++ b/openerp/addons/base/ir/ir_values.py @@ -28,7 +28,76 @@ EXCLUDED_FIELDS = set(( 'report_sxw_content', 'report_rml_content', 'report_sxw', 'report_rml', 'report_sxw_content_data', 'report_rml_content_data', 'search_view', )) +#: Possible slots to bind an action to with :meth:`~.set_action` +ACTION_SLOTS = [ + "client_action_multi", # sidebar wizard action + "client_print_multi", # sidebar report printing button + "client_action_relate", # sidebar related link + "tree_but_open", # double-click on item in tree view + "tree_but_action", # deprecated: same as tree_but_open + ] + + class ir_values(osv.osv): + """Holds internal model-specific action bindings and user-defined default + field values. definitions. This is a legacy internal model, mixing + two different concepts, and will likely be updated or replaced in a + future version by cleaner, separate models. You should not depend + explicitly on it. + + The purpose of each ``ir.values`` entry depends on its type, defined + by the ``key`` column: + + * 'default': user-defined default values, used when creating new + records of this model: + * 'action': binding of an action to a particular *action slot* of + this model, making the action easily available in the user + interface for this model. + + The ``key2`` column acts as a qualifier, further refining the type + of the entry. The possible values are: + + * for 'default' entries: an optional condition restricting the + cases where this particular default value will be applicable, + or ``False`` for no condition + * for 'action' entries: the ``key2`` qualifier is one of the available + action slots, defining how this action can be invoked: + + * ``'client_print_multi'`` for report printing actions that will + be available on views displaying items from this model + * ``'client_action_multi'`` for assistants (wizards) actions + that will be available in views displaying objects of this model + * ``'client_action_relate'`` for links towards related documents + that should be available in views displaying objects of this model + * ``'tree_but_open'`` for actions that will be triggered when + double-clicking an item from this model in a hierarchical tree view + + Each entry is specific to a model (``model`` column), and for ``'actions'`` + type, may even be made specific to a given record of that model when the + ``res_id`` column contains a record ID (``False`` means it's global for + all records). + + The content of the entry is defined by the ``value`` column, which may either + contain an arbitrary value (for default values - when the ``object`` column + is False), or a reference string defining the action that should be executed. + + .. rubric:: Usage: default values + + The ``'default'`` entries are usually defined manually by the + users, and set by their UI clients calling :meth:`~.set_default`. + These default values are then automatically used by the + ORM every time a new record is about to be created, i.e. when + :meth:`~openerp.osv.osv.osv.default_get` + or :meth:`~openerp.osv.osv.osv.create` are called. + + .. rubric:: Usage: action bindings + + Business applications will usually bind their actions during + installation, and OpenERP UI clients will apply them as defined, + based on the list of actions included in the result of + :meth:`~openerp.osv.osv.osv.fields_view_get`, + or directly returned by explicit calls to :meth:`~.get_actions`. + """ _name = 'ir.values' def _value_unpickle(self, cursor, user, ids, name, arg, context=None): @@ -38,7 +107,7 @@ class ir_values(osv.osv): if not report.object and value: try: value = str(pickle.loads(value)) - except: + except Exception: pass res[report.id] = value return res @@ -53,44 +122,69 @@ class ir_values(osv.osv): value = pickle.dumps(value) self.write(cursor, user, id, {name[:-9]: value}, context=ctx) - def onchange_object_id(self, cr, uid, ids, object_id, context={}): + def onchange_object_id(self, cr, uid, ids, object_id, context=None): if not object_id: return {} act = self.pool.get('ir.model').browse(cr, uid, object_id, context=context) return { 'value': {'model': act.model} } - def onchange_action_id(self, cr, uid, ids, action_id, context={}): + def onchange_action_id(self, cr, uid, ids, action_id, context=None): if not action_id: return {} act = self.pool.get('ir.actions.actions').browse(cr, uid, action_id, context=context) return { 'value': {'value_unpickle': act.type+','+str(act.id)} } + def onchange_key(self, cr, uid, ids, key, context=None): + return { + 'value': {'object': key == 'action'} + } + _columns = { - 'name': fields.char('Name', size=128), - 'model_id': fields.many2one('ir.model', 'Object', size=128, - help="This field is not used, it only helps you to select a good model."), - 'model': fields.char('Object Name', size=128, select=True), - 'action_id': fields.many2one('ir.actions.actions', 'Action', - help="This field is not used, it only helps you to select the right action."), - 'value': fields.text('Value'), + 'name': fields.char('Name', size=128, required=True), + 'model': fields.char('Model Name', size=128, select=True, required=True, + help="Model to which this entry applies"), + + # TODO: model_id and action_id should be read-write function fields + 'model_id': fields.many2one('ir.model', 'Model (change only)', size=128, + help="Model to which this entry applies - " + "helper field for setting a model, will " + "automatically set the correct model name"), + 'action_id': fields.many2one('ir.actions.actions', 'Action (change only)', + help="Action bound to this entry - " + "helper field for binding an action, will " + "automatically set the correct reference"), + + 'value': fields.text('Value', help="Default value (pickled) or reference to an action"), 'value_unpickle': fields.function(_value_unpickle, fnct_inv=_value_pickle, - method=True, type='text', string='Value'), - 'object': fields.boolean('Is Object'), - 'key': fields.selection([('action','Action'),('default','Default')], 'Type', size=128, select=True), - 'key2' : fields.char('Event Type',help="The kind of action or button in the client side that will trigger the action.", size=128, select=True), - 'meta': fields.text('Meta Datas'), - 'meta_unpickle': fields.function(_value_unpickle, fnct_inv=_value_pickle, - method=True, type='text', string='Metadata'), - 'res_id': fields.integer('Object ID', help="Keep 0 if the action must appear on all resources.", select=True), - 'user_id': fields.many2one('res.users', 'User', ondelete='cascade', select=True), - 'company_id': fields.many2one('res.company', 'Company', select=True) + type='text', + string='Default value or action reference'), + 'object': fields.boolean('No value serialization', help="Should be enabled when Type is Action"), + 'key': fields.selection([('action','Action'),('default','Default')], + 'Action Type', size=128, select=True, required=True, + help="- Action: an action attached to one slot of the given model\n" + "- Default: a default value for a model field"), + 'key2' : fields.char('Action Qualifier', size=128, select=True, + help="For actions, one of the possible action slots: \n" + " - client_action_multi\n" + " - client_print_multi\n" + " - client_action_relate\n" + " - tree_but_open\n" + "For defaults, an optional condition" + ,), + 'res_id': fields.integer('Record ID', select=True, + help="Database identifier of the record to which this applies." + "0 = for all records"), + 'user_id': fields.many2one('res.users', 'User', ondelete='cascade', select=True, + help="If set, action binding only applies for this user."), + 'company_id': fields.many2one('res.company', 'Company', ondelete='cascade', select=True, + help="If set, action binding only applies for this company") } _defaults = { - 'key': lambda *a: 'action', - 'key2': lambda *a: 'tree_but_open', - 'company_id': lambda *a: False + 'key': 'action', + 'key2': 'tree_but_open', + 'object': True, } def _auto_init(self, cr, context=None): @@ -99,140 +193,251 @@ class ir_values(osv.osv): if not cr.fetchone(): cr.execute('CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)') - def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False): + def set_default(self, cr, uid, model, field_name, value, for_all_users=True, company_id=False, condition=False): + """Defines a default value for the given model and field_name. Any previous + default will be replaced and lost in the process. + + :param string model: model name + :param string field_name: field name to which the default applies + :param value: the default field value to set + :type value: any serializable Python value + :param bool for_all_users: whether the default should apply to everybody or only + the user calling the method + :param int company_id: optional ID of the company to which the default should + apply. If omitted, the default will be global. If True + is passed, the current user's company will be used. + :param string condition: optional condition specification that can be used to + restrict the applicability of the default values + (e.g. based on another field's value) + :return: id of the newly created ir.values entry + """ if isinstance(value, unicode): value = value.encode('utf8') - if not isobject: - value = pickle.dumps(value) - if meta: - meta = pickle.dumps(meta) - assert isinstance(models, (list, tuple)), models - ids_res = [] - for model in models: - if isinstance(model, (list, tuple)): - model,res_id = model - else: - res_id = False - if replace: - search_criteria = [ - ('key', '=', key), - ('key2', '=', key2), - ('model', '=', model), - ('res_id', '=', res_id), - ('user_id', '=', preserve_user and uid) - ] - if key in ('meta', 'default'): - search_criteria.append(('name', '=', name)) - else: - search_criteria.append(('value', '=', value)) + if company_id is True: + # should be company-specific, need to get company id + user = self.pool.get('res.users').browse(cr, uid, uid) + company_id = user.company_id.id - self.unlink(cr, uid, self.search(cr, uid, search_criteria)) - vals = { - 'name': name, - 'value': value, - 'model': model, - 'object': isobject, - 'key': key, - 'key2': key2 and key2[:200], - 'meta': meta, - 'user_id': preserve_user and uid, - } - if company: - cid = self.pool.get('res.users').browse(cr, uid, uid, context={}).company_id.id - vals['company_id']=cid - if res_id: - vals['res_id']= res_id - ids_res.append(self.create(cr, uid, vals)) - return ids_res + # remove existing defaults for the same scope + search_criteria = [ + ('key', '=', 'default'), + ('key2', '=', condition), + ('model', '=', model), + ('name', '=', field_name), + ('user_id', '=', False if for_all_users else uid), + ('company_id','=', company_id) + ] + self.unlink(cr, uid, self.search(cr, uid, search_criteria)) + + return self.create(cr, uid, { + 'name': field_name, + 'value': pickle.dumps(value), + 'model': model, + 'object': False, + 'key': 'default', + 'key2': condition and condition[:200], + 'user_id': False if for_all_users else uid, + 'company_id': company_id, + }) + + def get_defaults(self, cr, uid, model, condition=False): + """Returns any user-defined values registered for the given model. + This is not field-specific, but an optional ``condition`` can be + provided to query for default values that should be applied only + when the given condition is met (usually another field's value). + Defaults that have been set for the user herself will have higher + priority that those that have been set for everyone + (see :meth:`~.set_default`). + + :param string model: model name + :param string condition: optional condition specification that can be used to + restrict the applicability of the default values + (e.g. based on another field's value) + :return: list of default values tuples of the form ``(id, field_name, value)`` + (``id`` is the ID of the default entry, usually irrelevant) + """ + # use a direct SQL query for performance reasons, + # this is called very often + query = """SELECT v.id, v.name, v.value FROM ir_values v + LEFT JOIN res_users u ON (v.user_id = u.id) + WHERE v.key = %%s AND v.model = %%s + AND (v.user_id = %%s OR v.user_id IS NULL) + AND (u.company_id IS NULL OR + u.company_id = + (SELECT company_id from res_users where id = %%s) + ) + %s + ORDER BY v.user_id, u.company_id""" + query = query % ('AND v.key2 = %s' if condition else '') + params = ('default', model, uid, uid) + if condition: + params += (condition,) + cr.execute(query, params) + + # keep only the highest priority default for each field + defaults = {} + for row in cr.dictfetchall(): + defaults.setdefault(row['name'], + (row['id'], row['name'], pickle.loads(row['value'].encode('utf-8')))) + return defaults.values() + + def set_action(self, cr, uid, name, action_slot, model, action, res_id=False): + """Binds an the given action to the given model's action slot - for later + retrieval via :meth:`~.get_actions`. Any existing binding of the same action + to the same slot is first removed, allowing an update of the action's name. + See the class description for more details about the various action + slots: :class:`~ir_values`. + + :param string name: action label, usually displayed by UI client + :param string action_slot: the action slot to which the action should be + bound to - one of ``client_action_multi``, + ``client_print_multi``, ``client_action_relate``, + ``tree_but_open``. + :param string model: model name + :param string action: action reference, in the form ``'model,id'`` + :param int res_id: optional record id - will bind the action only to a + specific record of the model, not all records. + :return: id of the newly created ir.values entry + """ + assert isinstance(action, basestring) and ',' in action, \ + 'Action definition must be an action reference, e.g. "ir.actions.act_window,42"' + assert action_slot in ACTION_SLOTS, \ + 'Action slot (%s) must be one of: %r' % (action_slot, ACTION_SLOTS) + + # remove existing action definition of same slot and value + search_criteria = [ + ('key', '=', 'action'), + ('key2', '=', action_slot), + ('model', '=', model), + ('res_id', '=', res_id), + ('value', '=', action), + ] + self.unlink(cr, uid, self.search(cr, uid, search_criteria)) + + return self.create(cr, uid, { + 'key': 'action', + 'key2': action_slot, + 'object': True, + 'model': model, + 'res_id': res_id or False, + 'name': name, + 'value': action, + }) + + def get_actions(self, cr, uid, action_slot, model, res_id=False, context=None): + """Retrieves the list of actions bound to the given model's action slot. + See the class description for more details about the various action + slots: :class:`~.ir_values`. + + :param string action_slot: the action slot to which the actions should be + bound to - one of ``client_action_multi``, + ``client_print_multi``, ``client_action_relate``, + ``tree_but_open``. + :param string model: model name + :param int res_id: optional record id - will bind the action only to a + specific record of the model, not all records. + :return: list of action tuples of the form ``(id, name, action_def)``, + where ``id`` is the ID of the default entry, ``name`` is the + action label, and ``action_def`` is a dict containing the + action definition as obtained by calling + :meth:`~openerp.osv.osv.osv.read` on the action record. + """ + assert action_slot in ACTION_SLOTS, 'Illegal action slot value: %s' % action_slot + # use a direct SQL query for performance reasons, + # this is called very often + query = """SELECT v.id, v.name, v.value FROM ir_values v + WHERE v.key = %s AND v.key2 = %s + AND v.model = %s + AND (v.res_id = %s + OR v.res_id IS NULL + OR v.res_id = 0) + ORDER BY v.id""" + cr.execute(query, ('action', action_slot, model, res_id or None)) + results = {} + for action in cr.dictfetchall(): + action_model,id = action['value'].split(',') + fields = [ + field + for field in self.pool.get(action_model)._all_columns + if field not in EXCLUDED_FIELDS] + # FIXME: needs cleanup + try: + action_def = self.pool.get(action_model).read(cr, uid, int(id), fields, context) + if action_def: + if action_model in ('ir.actions.report.xml','ir.actions.act_window', + 'ir.actions.wizard'): + groups = action_def.get('groups_id') + if groups: + cr.execute('SELECT 1 FROM res_groups_users_rel WHERE gid IN %s AND uid=%s', + (tuple(groups), uid)) + if not cr.fetchone(): + if action['name'] == 'Menuitem': + raise osv.except_osv('Error !', + 'You do not have the permission to perform this operation !!!') + continue + # keep only the first action registered for each action name + results[action['name']] = (action['id'], action['name'], action_def) + except except_orm, e: + continue + return results.values() + + def _map_legacy_model_list(self, model_list, map_fn): + """Apply map_fn to the various models passed, according to + legacy way to specify models/records. + + :param model_list: list of models/records reference in the form ``[model,..]`` + or ``[(model,res_id), ...]`` + :param map_fn: the map function to apply - should expect 2 arguments: + ``model`` and ``res_id``. + """ + assert isinstance(model_list, (list, tuple)), \ + "model_list should be in the form [model,..] or [(model,res_id), ..]" + results = [] + for model in model_list: + res_id = False + if isinstance(model, (list, tuple)): + model, res_id = model + result = map_fn(model, res_id) + # some of the functions return one result at a time (tuple or id) + # and some return a list of many of them - care for both + if isinstance(result,list): + results.extend(result) + else: + results.append(result) + return results + + # Backards-compatibility adapter layer to retrofit into split API + def set(self, cr, uid, key, key2, name, models, value, replace=True, isobject=False, meta=False, preserve_user=False, company=False): + """Deprecated legacy method to set default values and bind actions to models' action slots. + Now dispatches to the newer API methods according to the value of ``key``: :meth:`~.set_default` + (``key=='default'``) or :meth:`~.set_action` (``key == 'action'``). + + :deprecated: As of v6.1, ``set_default()`` or ``set_action()`` should be used directly. + """ + assert key in ['default', 'action'], "ir.values entry keys must be in ['default','action']" + if key == 'default': + def do_set(model,res_id): + return self.set_default(cr, uid, model, field_name=name, value=value, + for_all_users=(not preserve_user), company_id=company, + condition=key2) + elif key == 'action': + def do_set(model,res_id): + return self.set_action(cr, uid, name, action_slot=key2, model=model, action=value, res_id=res_id) + return self._map_legacy_model_list(models, do_set) def get(self, cr, uid, key, key2, models, meta=False, context=None, res_id_req=False, without_user=True, key2_req=True): - if context is None: - context = {} - result = [] - assert isinstance(models, (list, tuple)), models + """Deprecated legacy method to get the list of default values or actions bound to models' action slots. + Now dispatches to the newer API methods according to the value of ``key``: :meth:`~.get_defaults` + (``key=='default'``) or :meth:`~.get_actions` (``key == 'action'``) - for m in models: - if isinstance(m, (list, tuple)): - m, res_id = m - else: - res_id = False + :deprecated: As of v6.1, ``get_defaults()`` or ``get_actions()`` should be used directly. - where = ['key=%s','model=%s'] - params = [key, str(m)] - if key2: - where.append('key2=%s') - params.append(key2[:200]) - elif key2_req and not meta: - where.append('key2 is null') - if res_id_req and (models[-1][0] == m): - if res_id: - where.append('res_id=%s') - params.append(res_id) - else: - where.append('(res_id is NULL)') - elif res_id: - if (models[-1][0]==m): - where.append('(res_id=%s or (res_id is null))') - params.append(res_id) - else: - where.append('res_id=%s') - params.append(res_id) - order = 'id' - if key == 'default': - # Make sure we get first the values for specific users, then - # the global values. The map/filter below will retain the first - # value for any given name. The 'order by' will put the null - # values last; this may be postgres specific (it is the - # behavior in postgres at least since 8.2). - order = 'user_id' - where.append('(user_id=%s or (user_id IS NULL)) order by '+ order) - params.append(uid) - clause = ' and '.join(where) - cr.execute('select id,name,value,object,meta, key from ir_values where ' + clause, params) - result = cr.fetchall() - if result: - break - - if not result: - return [] - - def _result_get(x, keys): - if x[1] in keys: - return False - keys.append(x[1]) - if x[3]: - model,id = x[2].split(',') - # FIXME: It might be a good idea to opt-in that kind of stuff - # FIXME: instead of arbitrarily removing random fields - fields = [ - field - for field in self.pool.get(model).fields_get_keys(cr, uid) - if field not in EXCLUDED_FIELDS] - - try: - datas = self.pool.get(model).read(cr, uid, [int(id)], fields, context) - except except_orm, e: - return False - datas = datas and datas[0] - if not datas: - return False - else: - datas = pickle.loads(x[2].encode('utf-8')) - if meta: - return (x[0], x[1], datas, pickle.loads(x[4])) - return (x[0], x[1], datas) - keys = [] - res = filter(None, map(lambda x: _result_get(x, keys), result)) - res2 = res[:] - for r in res: - if isinstance(r[2], dict) and r[2].get('type') in ('ir.actions.report.xml','ir.actions.act_window','ir.actions.wizard'): - groups = r[2].get('groups_id') - if groups: - cr.execute('SELECT COUNT(1) FROM res_groups_users_rel WHERE gid IN %s AND uid=%s',(tuple(groups), uid)) - cnt = cr.fetchone()[0] - if not cnt: - res2.remove(r) - if r[1] == 'Menuitem' and not res2: - raise osv.except_osv('Error !','You do not have the permission to perform this operation !!!') - return res2 -ir_values() + """ + assert key in ['default', 'action'], "ir.values entry keys must be in ['default','action']" + if key == 'default': + def do_get(model,res_id): + return self.get_defaults(cr, uid, model, condition=key2) + elif key == 'action': + def do_get(model,res_id): + return self.get_actions(cr, uid, action_slot=key2, model=model, res_id=res_id, context=context) + return self._map_legacy_model_list(models, do_get) diff --git a/openerp/addons/base/test/test_ir_values.yml b/openerp/addons/base/test/test_ir_values.yml index b4c08db31ff..da26b9837b5 100644 --- a/openerp/addons/base/test/test_ir_values.yml +++ b/openerp/addons/base/test/test_ir_values.yml @@ -21,5 +21,8 @@ - !python {model: ir.values }: | d = self.get(cr, uid, 'default', False, ['unexisting_model']) + assert len(d) == 1, "Only one default must be returned per field" assert d[0][1] == u'my_test_ir_value', "Can't retrieve the created default value." assert d[0][2] == 'specific value', "Can't retrieve the created default value." + +# TODO: ADD tests for action bindings too, the other half of ir_values. \ No newline at end of file From 38ddb76960c21af0bf097112abd1eca78316bea1 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 2 Sep 2011 12:15:30 +0200 Subject: [PATCH 057/526] [IMP] ir.values: minor cleanup/view improvements bzr revid: odo@openerp.com-20110902101530-13r328uwhwdpudn5 --- openerp/addons/base/ir/ir.xml | 3 +-- openerp/addons/base/ir/ir_values.py | 17 ++++++----------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index eda044336df..3f9c48b467f 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -42,7 +42,7 @@
- + @@ -63,7 +63,6 @@ - diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py index c75831f9c2f..f3faacfd4d4 100644 --- a/openerp/addons/base/ir/ir_values.py +++ b/openerp/addons/base/ir/ir_values.py @@ -162,10 +162,10 @@ class ir_values(osv.osv): string='Default value or action reference'), 'object': fields.boolean('No value serialization', help="Should be enabled when Type is Action"), 'key': fields.selection([('action','Action'),('default','Default')], - 'Action Type', size=128, select=True, required=True, + 'Type', size=128, select=True, required=True, help="- Action: an action attached to one slot of the given model\n" "- Default: a default value for a model field"), - 'key2' : fields.char('Action Qualifier', size=128, select=True, + 'key2' : fields.char('Qualifier', size=128, select=True, help="For actions, one of the possible action slots: \n" " - client_action_multi\n" " - client_print_multi\n" @@ -174,7 +174,7 @@ class ir_values(osv.osv): "For defaults, an optional condition" ,), 'res_id': fields.integer('Record ID', select=True, - help="Database identifier of the record to which this applies." + help="Database identifier of the record to which this applies. " "0 = for all records"), 'user_id': fields.many2one('res.users', 'User', ondelete='cascade', select=True, help="If set, action binding only applies for this user."), @@ -381,14 +381,9 @@ class ir_values(osv.osv): continue return results.values() - def _map_legacy_model_list(self, model_list, map_fn): + def _map_legacy_model_list(self, model_list, map_fn, merge_results=False): """Apply map_fn to the various models passed, according to legacy way to specify models/records. - - :param model_list: list of models/records reference in the form ``[model,..]`` - or ``[(model,res_id), ...]`` - :param map_fn: the map function to apply - should expect 2 arguments: - ``model`` and ``res_id``. """ assert isinstance(model_list, (list, tuple)), \ "model_list should be in the form [model,..] or [(model,res_id), ..]" @@ -400,7 +395,7 @@ class ir_values(osv.osv): result = map_fn(model, res_id) # some of the functions return one result at a time (tuple or id) # and some return a list of many of them - care for both - if isinstance(result,list): + if merge_results: results.extend(result) else: results.append(result) @@ -440,4 +435,4 @@ class ir_values(osv.osv): elif key == 'action': def do_get(model,res_id): return self.get_actions(cr, uid, action_slot=key2, model=model, res_id=res_id, context=context) - return self._map_legacy_model_list(models, do_get) + return self._map_legacy_model_list(models, do_get, merge_results=True) From e0624cbc954b946bb962fc8da252eb1ffed80cc8 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 2 Sep 2011 14:40:38 +0200 Subject: [PATCH 058/526] [IMP] ir.values: smoke test for action bindings bzr revid: odo@openerp.com-20110902124038-36ip41ccytr2f9vt --- openerp/addons/base/test/test_ir_values.yml | 54 +++++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/openerp/addons/base/test/test_ir_values.yml b/openerp/addons/base/test/test_ir_values.yml index da26b9837b5..a8912e0ad3b 100644 --- a/openerp/addons/base/test/test_ir_values.yml +++ b/openerp/addons/base/test/test_ir_values.yml @@ -2,27 +2,71 @@ Create some default value for some (non-existing) model, for all users. - !python {model: ir.values }: | - self.set(cr, uid, 'default', False, 'my_test_ir_value',['unexisting_model'], 'global value') + self.set(cr, uid, 'default', False, 'my_test_field',['unexisting_model'], 'global value') - Retrieve it. - !python {model: ir.values }: | # d is a list of triple (id, name, value) d = self.get(cr, uid, 'default', False, ['unexisting_model']) - assert d[0][1] == u'my_test_ir_value', "Can't retrieve the created default value." + assert d[0][1] == 'my_test_field', "Can't retrieve the created default value." assert d[0][2] == 'global value', "Can't retrieve the created default value." - Do it again but for a specific user. - !python {model: ir.values }: | - self.set(cr, uid, 'default', False, 'my_test_ir_value',['unexisting_model'], 'specific value', preserve_user=True) + self.set(cr, uid, 'default', False, 'my_test_field',['unexisting_model'], 'specific value', preserve_user=True) - Retrieve it and check it is the one for the current user. - !python {model: ir.values }: | d = self.get(cr, uid, 'default', False, ['unexisting_model']) assert len(d) == 1, "Only one default must be returned per field" - assert d[0][1] == u'my_test_ir_value', "Can't retrieve the created default value." + assert d[0][1] == 'my_test_field', "Can't retrieve the created default value." assert d[0][2] == 'specific value', "Can't retrieve the created default value." +- + Create some action bindings for a non-existing model +- + !python {model: ir.values }: | + self.set(cr, uid, 'action', 'tree_but_open', 'OnDblClick Action', ['unexisting_model'], 'ir.actions.act_window,10', isobject=True) + self.set(cr, uid, 'action', 'tree_but_open', 'OnDblClick Action 2', ['unexisting_model'], 'ir.actions.act_window,11', isobject=True) + self.set(cr, uid, 'action', 'client_action_multi', 'Side Wizard', ['unexisting_model'], 'ir.actions.act_window,12', isobject=True) + self.set(cr, uid, 'action', 'client_print_multi', 'Nice Report', ['unexisting_model'], 'ir.actions.report.xml,2', isobject=True) + self.set(cr, uid, 'action', 'client_action_relate', 'Related Stuff', ['unexisting_model'], 'ir.actions.act_window,14', isobject=True) +- + Replace one action binding to set a new name +- + !python {model: ir.values }: | + self.set(cr, uid, 'action', 'tree_but_open', 'OnDblClick Action New', ['unexisting_model'], 'ir.actions.act_window,10', isobject=True) +- + Retrieve the action bindings and check they're correct +- + !python {model: ir.values }: | + actions = self.get(cr, uid, 'action', 'tree_but_open', ['unexisting_model']) + assert len(actions) == 2, "Mismatching number of bound actions" + #first action + assert len(actions[0]) == 3, "Malformed action definition" + assert actions[0][1] == 'OnDblClick Action 2', 'Bound action does not match definition' + assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 11, 'Bound action does not match definition' + #second action - this ones comes last because it was re-created with a different name + assert len(actions[1]) == 3, "Malformed action definition" + assert actions[1][1] == 'OnDblClick Action New', 'Re-Registering an action should replace it' + assert isinstance(actions[1][2], dict) and actions[1][2]['id'] == 10, 'Bound action does not match definition' -# TODO: ADD tests for action bindings too, the other half of ir_values. \ No newline at end of file + actions = self.get(cr, uid, 'action', 'client_action_multi', ['unexisting_model']) + assert len(actions) == 1, "Mismatching number of bound actions" + assert len(actions[0]) == 3, "Malformed action definition" + assert actions[0][1] == 'Side Wizard', 'Bound action does not match definition' + assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 12, 'Bound action does not match definition' + + actions = self.get(cr, uid, 'action', 'client_print_multi', ['unexisting_model']) + assert len(actions) == 1, "Mismatching number of bound actions" + assert len(actions[0]) == 3, "Malformed action definition" + assert actions[0][1] == 'Nice Report', 'Bound action does not match definition' + assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 2, 'Bound action does not match definition' + + actions = self.get(cr, uid, 'action', 'client_action_relate', ['unexisting_model']) + assert len(actions) == 1, "Mismatching number of bound actions" + assert len(actions[0]) == 3, "Malformed action definition" + assert actions[0][1] == 'Related Stuff', 'Bound action does not match definition' + assert isinstance(actions[0][2], dict) and actions[0][2]['id'] == 14, 'Bound action does not match definition' From 32020e13531267301abb0e58711bed70c65ff8e7 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 2 Sep 2011 14:56:48 +0200 Subject: [PATCH 059/526] [FIX] ir.values: re-binding of actions should properly delete the previous one bzr revid: odo@openerp.com-20110902125648-x0cir5m7818h9ba6 --- openerp/addons/base/ir/ir_values.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py index f3faacfd4d4..acb0da72159 100644 --- a/openerp/addons/base/ir/ir_values.py +++ b/openerp/addons/base/ir/ir_values.py @@ -309,7 +309,7 @@ class ir_values(osv.osv): ('key', '=', 'action'), ('key2', '=', action_slot), ('model', '=', model), - ('res_id', '=', res_id), + ('res_id', '=', res_id or 0), # int field -> NULL == 0 ('value', '=', action), ] self.unlink(cr, uid, self.search(cr, uid, search_criteria)) @@ -319,7 +319,7 @@ class ir_values(osv.osv): 'key2': action_slot, 'object': True, 'model': model, - 'res_id': res_id or False, + 'res_id': res_id, 'name': name, 'value': action, }) From d0af2f830c933a5dab635170d45b79a8278b99aa Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Fri, 2 Sep 2011 17:14:42 +0200 Subject: [PATCH 060/526] [FIX] ir.values: get_defaults() should cut the condition at 200 chars to match the conditions that were set bzr revid: odo@openerp.com-20110902151442-rqrh62z67g8k921o --- openerp/addons/base/ir/ir_values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py index acb0da72159..ca2973f948e 100644 --- a/openerp/addons/base/ir/ir_values.py +++ b/openerp/addons/base/ir/ir_values.py @@ -271,7 +271,7 @@ class ir_values(osv.osv): query = query % ('AND v.key2 = %s' if condition else '') params = ('default', model, uid, uid) if condition: - params += (condition,) + params += (condition[:200],) cr.execute(query, params) # keep only the highest priority default for each field From 083691b392b1f9556dd64787f46e7536e7d2f7c3 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 5 Sep 2011 14:37:56 +0200 Subject: [PATCH 061/526] [IMP] ir.values: improve security: users can only write to their personal defaults Administrator access is required to set defaults for everybody, as well as to alter the action bindings. bzr revid: odo@openerp.com-20110905123756-oqum5k2pnbyoa11r --- openerp/addons/base/security/base_security.xml | 7 +++++++ openerp/addons/base/security/ir.model.access.csv | 3 +-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/security/base_security.xml b/openerp/addons/base/security/base_security.xml index 00aca6613d6..fe3a10fdaed 100644 --- a/openerp/addons/base/security/base_security.xml +++ b/openerp/addons/base/security/base_security.xml @@ -65,6 +65,13 @@ [('company_id','child_of',[user.company_id.id])] + + Defaults: alter personal values only + + [('key','=','default'),('user_id','=',user.id)] + + + diff --git a/openerp/addons/base/security/ir.model.access.csv b/openerp/addons/base/security/ir.model.access.csv index 8949052f89b..6460c06d1f7 100644 --- a/openerp/addons/base/security/ir.model.access.csv +++ b/openerp/addons/base/security/ir.model.access.csv @@ -38,8 +38,7 @@ "access_ir_ui_view_custom_group_user","ir_ui_view_custom_group_user","model_ir_ui_view_custom",,1,0,0,0 "access_ir_ui_view_custom_group_system","ir_ui_view_custom_group_system","model_ir_ui_view_custom","group_system",1,1,1,1 "access_ir_ui_view_sc_group_user","ir_ui_view_sc group_user","model_ir_ui_view_sc",,1,1,1,1 -"access_ir_values_group_erp_manager","ir_values group_erp_manager","model_ir_values","group_erp_manager",1,1,1,1 -"access_ir_values_group_all","ir_values group_all","model_ir_values",,1,0,1,0 +"access_ir_values_group_all","ir_values group_all","model_ir_values",,1,1,1,1 "access_res_company_group_erp_manager","res_company group_erp_manager","model_res_company","group_erp_manager",1,1,1,1 "access_res_company_group_user","res_company group_user","model_res_company",,1,0,0,0 "access_res_country_group_all","res_country group_user_all","model_res_country",,1,0,0,0 From e333a2eb2e81763cb532b3cd514a80892f185fa1 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Mon, 5 Sep 2011 16:10:58 +0200 Subject: [PATCH 062/526] [IMP] ir.values: improved defaults doc + removed `object` column completely The `object` column actually directly depended on the value of the `key` column, so it can be totally removed with no side-effects. Docstrings updated following review comments. bzr revid: odo@openerp.com-20110905141058-xa01o77l1rto6hg9 --- openerp/addons/base/ir/ir.xml | 4 +- openerp/addons/base/ir/ir_values.py | 73 +++++++++++++++++++---------- 2 files changed, 49 insertions(+), 28 deletions(-) diff --git a/openerp/addons/base/ir/ir.xml b/openerp/addons/base/ir/ir.xml index 3f9c48b467f..3ab0bb3f167 100644 --- a/openerp/addons/base/ir/ir.xml +++ b/openerp/addons/base/ir/ir.xml @@ -93,7 +93,7 @@ tree,form [('key','=','action')] - {'default_object':1,'default_key':'action'} + {'default_key':'action'} @@ -116,7 +116,7 @@ tree,form [('key','=','default')] - {'default_object':0,'default_key':'default','default_key2':''} + {'default_key':'default','default_key2':''} diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py index ca2973f948e..2eb7b97d18a 100644 --- a/openerp/addons/base/ir/ir_values.py +++ b/openerp/addons/base/ir/ir_values.py @@ -78,8 +78,8 @@ class ir_values(osv.osv): all records). The content of the entry is defined by the ``value`` column, which may either - contain an arbitrary value (for default values - when the ``object`` column - is False), or a reference string defining the action that should be executed. + contain an arbitrary value, or a reference string defining the action that + should be executed. .. rubric:: Usage: default values @@ -102,14 +102,15 @@ class ir_values(osv.osv): def _value_unpickle(self, cursor, user, ids, name, arg, context=None): res = {} - for report in self.browse(cursor, user, ids, context=context): - value = report[name[:-9]] - if not report.object and value: + for record in self.browse(cursor, user, ids, context=context): + value = record[name[:-9]] + if record.key == 'default' and value: + # default values are pickled on the fly try: value = str(pickle.loads(value)) except Exception: pass - res[report.id] = value + res[record.id] = value return res def _value_pickle(self, cursor, user, id, name, value, arg, context=None): @@ -118,7 +119,9 @@ class ir_values(osv.osv): ctx = context.copy() if self.CONCURRENCY_CHECK_FIELD in ctx: del ctx[self.CONCURRENCY_CHECK_FIELD] - if not self.browse(cursor, user, id, context=context).object: + record = self.browse(cursor, user, id, context=context) + if record.key == 'default': + # default values are pickled on the fly value = pickle.dumps(value) self.write(cursor, user, id, {name[:-9]: value}, context=ctx) @@ -136,11 +139,6 @@ class ir_values(osv.osv): 'value': {'value_unpickle': act.type+','+str(act.id)} } - def onchange_key(self, cr, uid, ids, key, context=None): - return { - 'value': {'object': key == 'action'} - } - _columns = { 'name': fields.char('Name', size=128, required=True), 'model': fields.char('Model Name', size=128, select=True, required=True, @@ -160,7 +158,6 @@ class ir_values(osv.osv): 'value_unpickle': fields.function(_value_unpickle, fnct_inv=_value_pickle, type='text', string='Default value or action reference'), - 'object': fields.boolean('No value serialization', help="Should be enabled when Type is Action"), 'key': fields.selection([('action','Action'),('default','Default')], 'Type', size=128, select=True, required=True, help="- Action: an action attached to one slot of the given model\n" @@ -184,7 +181,6 @@ class ir_values(osv.osv): _defaults = { 'key': 'action', 'key2': 'tree_but_open', - 'object': True, } def _auto_init(self, cr, context=None): @@ -195,7 +191,17 @@ class ir_values(osv.osv): def set_default(self, cr, uid, model, field_name, value, for_all_users=True, company_id=False, condition=False): """Defines a default value for the given model and field_name. Any previous - default will be replaced and lost in the process. + default for the same scope (model, field_name, value, for_all_users, company_id, condition) + will be replaced and lost in the process. + + Defaults can be later retrieved via :meth:`~.get_defaults`, which will return + the highest priority default for any given field. Defaults that are more specific + have a higher priority, in the following order (highest to lowest): + + * specific to user and company + * specific to user only + * specific to company only + * global to everyone :param string model: model name :param string field_name: field name to which the default applies @@ -208,7 +214,13 @@ class ir_values(osv.osv): is passed, the current user's company will be used. :param string condition: optional condition specification that can be used to restrict the applicability of the default values - (e.g. based on another field's value) + (e.g. based on another field's value). This is an + opaque string as far as the API is concerned, but client + stacks typically use single-field conditions in the + form ``'key=stringified_value'``. + (Currently, the condition is trimmed to 200 characters, + so values that share the same first 200 characters always + match) :return: id of the newly created ir.values entry """ if isinstance(value, unicode): @@ -233,7 +245,6 @@ class ir_values(osv.osv): 'name': field_name, 'value': pickle.dumps(value), 'model': model, - 'object': False, 'key': 'default', 'key2': condition and condition[:200], 'user_id': False if for_all_users else uid, @@ -241,18 +252,29 @@ class ir_values(osv.osv): }) def get_defaults(self, cr, uid, model, condition=False): - """Returns any user-defined values registered for the given model. - This is not field-specific, but an optional ``condition`` can be - provided to query for default values that should be applied only - when the given condition is met (usually another field's value). - Defaults that have been set for the user herself will have higher - priority that those that have been set for everyone - (see :meth:`~.set_default`). + """Returns any default values that are defined for the current model and user, + (and match ``condition``, if specified), previously registered via + :meth:`~.set_default`. + + Defaults are global to a model, not field-specific, but an optional + ``condition`` can be provided to restrict matching default values + to those that were defined for the same condition (usually based + on another field's value). + + Default values also have priorities depending on whom they apply + to: only the highest priority value will be returned for any + field. See :meth:`~.set_default` for more details. :param string model: model name :param string condition: optional condition specification that can be used to restrict the applicability of the default values - (e.g. based on another field's value) + (e.g. based on another field's value). This is an + opaque string as far as the API is concerned, but client + stacks typically use single-field conditions in the + form ``'key=stringified_value'``. + (Currently, the condition is trimmed to 200 characters, + so values that share the same first 200 characters always + match) :return: list of default values tuples of the form ``(id, field_name, value)`` (``id`` is the ID of the default entry, usually irrelevant) """ @@ -317,7 +339,6 @@ class ir_values(osv.osv): return self.create(cr, uid, { 'key': 'action', 'key2': action_slot, - 'object': True, 'model': model, 'res_id': res_id, 'name': name, From 3ba75e5d50b95f6ba01400afcf1c31ce3ee0f10e Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Thu, 8 Sep 2011 15:15:46 +0530 Subject: [PATCH 063/526] [IMP] base.ir_edi: dont use fields_get or _columns to get the list of fields, use the new "self._all_columns" (map of column_info records) which also contains inherited fields and is already cached (see in ORM for the definition of the column_info object) bzr revid: hmo@tinyerp.com-20110908094546-4zy2dbqk8zyrzr26 --- openerp/addons/base/ir/ir_edi.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 2fd11338ffb..5588e4d041f 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -366,26 +366,27 @@ class edi(object): context = {} if edi_struct is None: edi_struct = {} - _fields = self.fields_get(cr, uid, context=context) - fields_to_export = edi_struct and edi_struct.keys() or _fields.keys() + _columns = self._all_columns + fields_to_export = edi_struct and edi_struct.keys() or _columns.keys() edi_dict_list = [] value = None for row in records: edi_dict = {} edi_dict.update(self.edi_metadata(cr, uid, [row], context=context)[0]) for field in fields_to_export: - cols = _fields[field] + _column = _columns[field].column + _column_dict = fields.field_to_dict(self, cr, uid, context, _column) record = getattr(row, field) if not record: continue #if _fields[field].has_key('function') or _fields[field].has_key('related_columns'): # # Do not Export Function Fields and related fields # continue - elif cols['type'] == 'many2one': + elif _column_dict['type'] == 'many2one': value = self.edi_m2o(cr, uid, record, context=context) - elif cols['type'] == 'many2many': + elif _column_dict['type'] == 'many2many': value = self.edi_m2m(cr, uid, record, context=context) - elif cols['type'] == 'one2many': + elif _column_dict['type'] == 'one2many': value = self.edi_o2m(cr, uid, record, edi_struct=edi_struct.get(field, {}), context=context ) else: value = record @@ -466,24 +467,26 @@ class edi(object): connect it to the parent record via a write value like (4, db_id). """ # generic implementation! - fields = edi_document.keys() fields_to_import = [] data_line = [] model_data = self.pool.get('ir.model.data') - _fields = self.fields_get(cr, uid, context=context) + _columns = self._all_columns values = {} xml_id = edi_document['__id'] assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id) module, xml_id2 = xml_id.split('.') for field in edi_document.keys(): if not field.startswith('__'): + _column = _columns[field].column + _column_dict = fields.field_to_dict(self, cr, uid, context, _column) + fields_to_import.append(field) edi_field_value = edi_document[field] - field_type = _fields[field]['type'] - relation_model = _fields[field].get('relation') + field_type = _column_dict['type'] + relation_model = _column_dict.get('relation') if not edi_field_value: continue - if _fields[field].has_key('function') or _fields[field].has_key('related_columns'): + if _column_dict.has_key('function') or _column_dict.has_key('related_columns'): # DO NOT IMPORT FUNCTION FIELD AND RELATED FIELD continue elif field_type in ('many2one', 'many2many'): From fbb6e5e56aaf36ff652edbec0181a0fcb95a3601 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Thu, 8 Sep 2011 13:38:33 +0200 Subject: [PATCH 064/526] [FIX] ir.values.get_defaults: typo in SQL query - spotted by Naresh, thanks! bzr revid: odo@openerp.com-20110908113833-z2cgkj093940usys --- openerp/addons/base/ir/ir_values.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/ir/ir_values.py b/openerp/addons/base/ir/ir_values.py index 2eb7b97d18a..d908c2096b3 100644 --- a/openerp/addons/base/ir/ir_values.py +++ b/openerp/addons/base/ir/ir_values.py @@ -284,8 +284,8 @@ class ir_values(osv.osv): LEFT JOIN res_users u ON (v.user_id = u.id) WHERE v.key = %%s AND v.model = %%s AND (v.user_id = %%s OR v.user_id IS NULL) - AND (u.company_id IS NULL OR - u.company_id = + AND (v.company_id IS NULL OR + v.company_id = (SELECT company_id from res_users where id = %%s) ) %s From 09b63ae6ea0dfe2e173218097dde865d791ac529 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Sep 2011 16:15:41 +0200 Subject: [PATCH 065/526] [FIX] don't *eval* field names to get the field type associated with a function field bzr revid: xmo@openerp.com-20110909141541-p172s7jqu597sjc9 --- openerp/osv/orm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index b87672c012d..341f14e00e2 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -514,9 +514,9 @@ def get_pg_type(f): f_type = ('int4', 'INTEGER') else: f_type = ('varchar', 'VARCHAR(%d)' % f_size) - elif isinstance(f, fields.function) and eval('fields.'+(f._type), globals()) in type_dict: - t = eval('fields.'+(f._type), globals()) - f_type = (type_dict[t], type_dict[t]) + elif isinstance(f, fields.function) and getattr(fields, f._type, None) in type_dict: + t = type_dict[getattr(fields, f._type)] + f_type = (t, t) elif isinstance(f, fields.function) and f._type == 'float': if f.digits: f_type = ('numeric', 'NUMERIC') From cf44b49c939c7209111fea41b9a506d21b96dbee Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Sep 2011 16:33:49 +0200 Subject: [PATCH 066/526] [IMP] extract VARCHAR typing, if no size is provided (or the size is 0) don't put a limit on the varchar bzr revid: xmo@openerp.com-20110909143349-5sykc4lpsn3rxf5w --- openerp/osv/orm.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 341f14e00e2..8041628626d 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -475,6 +475,18 @@ class browse_record(object): __repr__ = __str__ +def pg_varchar(size=0): + """ Returns the VARCHAR declaration for the provided size: + + * If no size (or an empty size is provided) return an 'infinite' VARCHAR + * Otherwise return a VARCHAR(n) + + :type int size: varchar size, optional + :rtype: str + """ + if size: + return 'VARCHAR(%d)' % size + return 'VARCHAR' def get_pg_type(f): """ @@ -501,19 +513,19 @@ def get_pg_type(f): else: f_type = ('float8', 'DOUBLE PRECISION') elif isinstance(f, (fields.char, fields.reference)): - f_type = ('varchar', 'VARCHAR(%d)' % (f.size,)) + f_type = ('varchar', pg_varchar(f.size)) elif isinstance(f, fields.selection): if isinstance(f.selection, list) and isinstance(f.selection[0][0], (str, unicode)): - f_size = reduce(lambda x, y: max(x, len(y[0])), f.selection, f.size or 16) + f_size = reduce(lambda x, y: max(x, len(y[0])), f.selection, f.size) elif isinstance(f.selection, list) and isinstance(f.selection[0][0], int): f_size = -1 else: - f_size = getattr(f, 'size', None) or 16 + f_size = getattr(f, 'size', None) if f_size == -1: f_type = ('int4', 'INTEGER') else: - f_type = ('varchar', 'VARCHAR(%d)' % f_size) + f_type = ('varchar', pg_varchar(f_size)) elif isinstance(f, fields.function) and getattr(fields, f._type, None) in type_dict: t = type_dict[getattr(fields, f._type)] f_type = (t, t) @@ -525,7 +537,7 @@ def get_pg_type(f): elif isinstance(f, fields.function) and f._type == 'selection': f_type = ('text', 'text') elif isinstance(f, fields.function) and f._type == 'char': - f_type = ('varchar', 'VARCHAR(%d)' % (f.size)) + f_type = ('varchar', pg_varchar(f.size)) else: logger = netsvc.Logger() logger.notifyChannel("init", netsvc.LOG_WARNING, '%s type not supported!' % (type(f))) @@ -2838,7 +2850,7 @@ class orm(orm_template): if f_obj_type: ok = False casts = [ - ('text', 'char', 'VARCHAR(%d)' % (f.size or 0,), '::VARCHAR(%d)'%(f.size or 0,)), + ('text', 'char', pg_varchar(f.size), '::%s' % pg_varchar(f.size), ('varchar', 'text', 'TEXT', ''), ('int4', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]), ('date', 'datetime', 'TIMESTAMP', '::TIMESTAMP'), @@ -2848,8 +2860,8 @@ class orm(orm_template): ] if f_pg_type == 'varchar' and f._type == 'char' and f_pg_size < f.size: cr.execute('ALTER TABLE "%s" RENAME COLUMN "%s" TO temp_change_size' % (self._table, k)) - cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" VARCHAR(%d)' % (self._table, k, f.size)) - cr.execute('UPDATE "%s" SET "%s"=temp_change_size::VARCHAR(%d)' % (self._table, k, f.size)) + cr.execute('ALTER TABLE "%s" ADD COLUMN "%s" %s' % (self._table, k, pg_varchar(f.size))) + cr.execute('UPDATE "%s" SET "%s"=temp_change_size::%s' % (self._table, k, pg_varchar(f.size))) cr.execute('ALTER TABLE "%s" DROP COLUMN temp_change_size CASCADE' % (self._table,)) cr.commit() self.__schema.debug("Table '%s': column '%s' (type varchar) changed size from %s to %s", From 8668ad7ed3d9d2b406c179f1e111b2d63c875ad6 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Sep 2011 16:52:13 +0200 Subject: [PATCH 067/526] [IMP] have function fields delegate their pg_type discovery to get_pg_type * add a type override to get_pg_type, so the function field can pass itself as another field type * special-case the 'selection' role, not sure it's correct but it was pretty special last time around (I guess since we don't have the field's values available we can't just do the type/size inference of get_pg_type) bzr revid: xmo@openerp.com-20110909145213-y172z8eyfnond43w --- openerp/osv/orm.py | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 8041628626d..97c4ef89c60 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -488,11 +488,14 @@ def pg_varchar(size=0): return 'VARCHAR(%d)' % size return 'VARCHAR' -def get_pg_type(f): +def get_pg_type(f, type_override=None): """ - returns a tuple - (type returned by postgres when the column was created, type expression to create the column) + :param fields._column f: field to get a Postgres type for + :param type type_override: use the provided type for dispatching instead of the field's own type + :returns: (postgres_identification_type, postgres_type_specification) + :rtype: (str, str) """ + field_type = type_override or type(f) type_dict = { fields.boolean: 'bool', @@ -505,8 +508,8 @@ def get_pg_type(f): fields.binary: 'bytea', fields.many2one: 'int4', } - if type(f) in type_dict: - f_type = (type_dict[type(f)], type_dict[type(f)]) + if field_type in type_dict: + f_type = (type_dict[field_type], type_dict[field_type]) elif isinstance(f, fields.float): if f.digits: f_type = ('numeric', 'NUMERIC') @@ -526,21 +529,15 @@ def get_pg_type(f): f_type = ('int4', 'INTEGER') else: f_type = ('varchar', pg_varchar(f_size)) - elif isinstance(f, fields.function) and getattr(fields, f._type, None) in type_dict: - t = type_dict[getattr(fields, f._type)] - f_type = (t, t) - elif isinstance(f, fields.function) and f._type == 'float': - if f.digits: - f_type = ('numeric', 'NUMERIC') + + elif isinstance(f, fields.function): + if f._type = 'selection': + f_type = ('varchar', pg_varchar()) else: - f_type = ('float8', 'DOUBLE PRECISION') - elif isinstance(f, fields.function) and f._type == 'selection': - f_type = ('text', 'text') - elif isinstance(f, fields.function) and f._type == 'char': - f_type = ('varchar', pg_varchar(f.size)) + f_type = get_pg_type(f, getattr(fields, f._type)) else: logger = netsvc.Logger() - logger.notifyChannel("init", netsvc.LOG_WARNING, '%s type not supported!' % (type(f))) + logger.notifyChannel("init", netsvc.LOG_WARNING, '%s type not supported!' % (field_type)) f_type = None return f_type From 7a3960f736436bb395e237153207c54214aa8dd2 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Sep 2011 17:23:07 +0200 Subject: [PATCH 068/526] [IMP] remove size inference for selection lists with str|unicode value choices, simplify selection pg_type selection * if the first element of the choices list is an integer, use int4 * if the field has a size, use that size * else use an unrestricted varchar bzr revid: xmo@openerp.com-20110909152307-ralzt1g8l1b3e46x --- openerp/osv/orm.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 97c4ef89c60..2892a32c440 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -518,18 +518,10 @@ def get_pg_type(f, type_override=None): elif isinstance(f, (fields.char, fields.reference)): f_type = ('varchar', pg_varchar(f.size)) elif isinstance(f, fields.selection): - if isinstance(f.selection, list) and isinstance(f.selection[0][0], (str, unicode)): - f_size = reduce(lambda x, y: max(x, len(y[0])), f.selection, f.size) - elif isinstance(f.selection, list) and isinstance(f.selection[0][0], int): - f_size = -1 - else: - f_size = getattr(f, 'size', None) - - if f_size == -1: + if isinstance(f.selection, list) and isinstance(f.selection[0][0], int): f_type = ('int4', 'INTEGER') else: - f_type = ('varchar', pg_varchar(f_size)) - + f_type = ('varchar', pg_varchar(getattr(f, 'size', None))) elif isinstance(f, fields.function): if f._type = 'selection': f_type = ('varchar', pg_varchar()) From b9ed013504639a536b0d5c419507985e9bf9ded4 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Sep 2011 17:24:45 +0200 Subject: [PATCH 069/526] [IMP] use logging API in get_pg_types bzr revid: xmo@openerp.com-20110909152445-pol8ko3ckzc6p7s0 --- openerp/osv/orm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 2892a32c440..692368cd44d 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -528,8 +528,7 @@ def get_pg_type(f, type_override=None): else: f_type = get_pg_type(f, getattr(fields, f._type)) else: - logger = netsvc.Logger() - logger.notifyChannel("init", netsvc.LOG_WARNING, '%s type not supported!' % (field_type)) + logging.getLogger('orm').warn('%s type not supported!', field_type) f_type = None return f_type From f74c55182a5371c88f64af326cbbb3f56958fbb0 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Sep 2011 17:26:22 +0200 Subject: [PATCH 070/526] [IMP] lift mapping from field types to PGTYPES outside get_pg_type bzr revid: xmo@openerp.com-20110909152622-114y4401x6gw6a79 --- openerp/osv/orm.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 692368cd44d..f20b759f6f8 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -488,6 +488,17 @@ def pg_varchar(size=0): return 'VARCHAR(%d)' % size return 'VARCHAR' +FIELDS_TO_PGTYPES = { + fields.boolean: 'bool', + fields.integer: 'int4', + fields.integer_big: 'int8', + fields.text: 'text', + fields.date: 'date', + fields.time: 'time', + fields.datetime: 'timestamp', + fields.binary: 'bytea', + fields.many2one: 'int4', +} def get_pg_type(f, type_override=None): """ :param fields._column f: field to get a Postgres type for @@ -497,19 +508,8 @@ def get_pg_type(f, type_override=None): """ field_type = type_override or type(f) - type_dict = { - fields.boolean: 'bool', - fields.integer: 'int4', - fields.integer_big: 'int8', - fields.text: 'text', - fields.date: 'date', - fields.time: 'time', - fields.datetime: 'timestamp', - fields.binary: 'bytea', - fields.many2one: 'int4', - } - if field_type in type_dict: - f_type = (type_dict[field_type], type_dict[field_type]) + if field_type in FIELDS_TO_PGTYPES: + f_type = (FIELDS_TO_PGTYPES[field_type], FIELDS_TO_PGTYPES[field_type]) elif isinstance(f, fields.float): if f.digits: f_type = ('numeric', 'NUMERIC') From 4b8be3a051cc20f2079e146f583919b77e662e88 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Sep 2011 17:39:35 +0200 Subject: [PATCH 071/526] [IMP] use early returns instead of a single return point bzr revid: xmo@openerp.com-20110909153935-uk4cggyv78nfdjqt --- openerp/osv/orm.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index f20b759f6f8..977c3c3deb4 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -509,28 +509,23 @@ def get_pg_type(f, type_override=None): field_type = type_override or type(f) if field_type in FIELDS_TO_PGTYPES: - f_type = (FIELDS_TO_PGTYPES[field_type], FIELDS_TO_PGTYPES[field_type]) + return (FIELDS_TO_PGTYPES[field_type], FIELDS_TO_PGTYPES[field_type]) elif isinstance(f, fields.float): if f.digits: - f_type = ('numeric', 'NUMERIC') - else: - f_type = ('float8', 'DOUBLE PRECISION') + return ('numeric', 'NUMERIC') + return ('float8', 'DOUBLE PRECISION') elif isinstance(f, (fields.char, fields.reference)): - f_type = ('varchar', pg_varchar(f.size)) + return ('varchar', pg_varchar(f.size)) elif isinstance(f, fields.selection): if isinstance(f.selection, list) and isinstance(f.selection[0][0], int): - f_type = ('int4', 'INTEGER') - else: - f_type = ('varchar', pg_varchar(getattr(f, 'size', None))) + return ('int4', 'INTEGER') + return ('varchar', pg_varchar(getattr(f, 'size', None))) elif isinstance(f, fields.function): if f._type = 'selection': - f_type = ('varchar', pg_varchar()) - else: - f_type = get_pg_type(f, getattr(fields, f._type)) - else: - logging.getLogger('orm').warn('%s type not supported!', field_type) - f_type = None - return f_type + return ('varchar', pg_varchar()) + return get_pg_type(f, getattr(fields, f._type)) + logging.getLogger('orm').warn('%s type not supported!', field_type) + return class MetaModel(type): From cbaa36c8ccf31d960d497834f72a2a79b0edb014 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Sep 2011 17:42:42 +0200 Subject: [PATCH 072/526] [FIX] typo, might want to launch the software from time to time while changing stuff bzr revid: xmo@openerp.com-20110909154242-gve3dgsbmg6crkya --- openerp/osv/orm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 977c3c3deb4..4c592a53342 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -521,7 +521,7 @@ def get_pg_type(f, type_override=None): return ('int4', 'INTEGER') return ('varchar', pg_varchar(getattr(f, 'size', None))) elif isinstance(f, fields.function): - if f._type = 'selection': + if f._type == 'selection': return ('varchar', pg_varchar()) return get_pg_type(f, getattr(fields, f._type)) logging.getLogger('orm').warn('%s type not supported!', field_type) @@ -2833,7 +2833,7 @@ class orm(orm_template): if f_obj_type: ok = False casts = [ - ('text', 'char', pg_varchar(f.size), '::%s' % pg_varchar(f.size), + ('text', 'char', pg_varchar(f.size), '::%s' % pg_varchar(f.size)), ('varchar', 'text', 'TEXT', ''), ('int4', 'float', get_pg_type(f)[1], '::'+get_pg_type(f)[1]), ('date', 'datetime', 'TIMESTAMP', '::TIMESTAMP'), From 0d965c2fca76a73bc301da6769d34e1360a9e418 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Sep 2011 17:58:48 +0200 Subject: [PATCH 073/526] [FIX] if types can be overridden, type-checking against the overriden type instead of the non-overridden instance might be a good idea as well bzr revid: xmo@openerp.com-20110909155848-5no3pvtfugqmhm8q --- openerp/osv/orm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 4c592a53342..92a22a29f9a 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -510,17 +510,17 @@ def get_pg_type(f, type_override=None): if field_type in FIELDS_TO_PGTYPES: return (FIELDS_TO_PGTYPES[field_type], FIELDS_TO_PGTYPES[field_type]) - elif isinstance(f, fields.float): + elif issubclass(field_type, fields.float): if f.digits: return ('numeric', 'NUMERIC') return ('float8', 'DOUBLE PRECISION') - elif isinstance(f, (fields.char, fields.reference)): + elif issubclass(field_type, (fields.char, fields.reference)): return ('varchar', pg_varchar(f.size)) - elif isinstance(f, fields.selection): + elif issubclass(field_type, fields.selection): if isinstance(f.selection, list) and isinstance(f.selection[0][0], int): return ('int4', 'INTEGER') return ('varchar', pg_varchar(getattr(f, 'size', None))) - elif isinstance(f, fields.function): + elif issubclass(field_type, fields.function): if f._type == 'selection': return ('varchar', pg_varchar()) return get_pg_type(f, getattr(fields, f._type)) From 9c83238f18dd651e0df0bde54c050cfc4a08a694 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Sep 2011 18:16:23 +0200 Subject: [PATCH 074/526] [FIX] reimplement quality selection field API a size attribute of -1 means the selection field is an int4, even if the selection is a callable bzr revid: xmo@openerp.com-20110909161623-nab218ea48ucj4qt --- openerp/osv/orm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 92a22a29f9a..6f794469529 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -517,7 +517,8 @@ def get_pg_type(f, type_override=None): elif issubclass(field_type, (fields.char, fields.reference)): return ('varchar', pg_varchar(f.size)) elif issubclass(field_type, fields.selection): - if isinstance(f.selection, list) and isinstance(f.selection[0][0], int): + if (isinstance(f.selection, list) and isinstance(f.selection[0][0], int))\ + or getattr(f, 'size', None) == -1: return ('int4', 'INTEGER') return ('varchar', pg_varchar(getattr(f, 'size', None))) elif issubclass(field_type, fields.function): From e575211116c099e24ae294e0e5c08f617ae3a9fb Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 9 Sep 2011 18:29:29 +0200 Subject: [PATCH 075/526] [IMP] add a pair of assertions on the parameter to VARCHAR, just in case bzr revid: xmo@openerp.com-20110909162929-w1l328oh14ccn7f8 --- openerp/osv/orm.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 6f794469529..9492e3d812b 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -485,6 +485,12 @@ def pg_varchar(size=0): :rtype: str """ if size: + if not isinstance(size, int): + raise TypeError("VARCHAR parameter should be an int, got %s" + % type(size)) + if size < 0: + raise ValueError("VARCHAR parameter can not be negative, got %d" + % size) return 'VARCHAR(%d)' % size return 'VARCHAR' From 7ce7230c4010cf5e7099bbd7c34b4cba19b54388 Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Tue, 13 Sep 2011 18:24:56 +0530 Subject: [PATCH 076/526] [IMP] base.ir.edi: improve edi import process, split in different methods [ADD] base.res.edi_company: introduce new methods in res.company to import company as parter and export company address bzr revid: hmo@tinyerp.com-20110913125456-j9ipisqu5lf6tdyq --- openerp/addons/base/ir/ir_edi.py | 188 ++++++++++++--------- openerp/addons/base/res/__init__.py | 2 + openerp/addons/base/res/edi_res_company.py | 103 +++++++++++ 3 files changed, 214 insertions(+), 79 deletions(-) create mode 100644 openerp/addons/base/res/edi_res_company.py diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 5588e4d041f..1d36a879036 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -91,7 +91,7 @@ class ir_edi_document(osv.osv): returns the string serialization that is in the database (column: document) for the given edi_token or raise. """ - records = self.name_search(cr, uid, edi_token, context=context) + records = self.name_search(cr, uid, edi_token, operator='=', context=context) if records: record = records[0][0] edi = self.browse(cr, uid, record, context=context) @@ -192,7 +192,7 @@ class edi(object): xml_record = model_data_pool.browse(cr, uid, xml_record_id, context=context) return xml_record.module, xml_record.name - def edi_xml_id(self, cr, uid, record, context=None): + def edi_xml_id(self, cr, uid, record, xml_id=None, context=None): """ Generate a unique string to represent a pair of (the database's UUID, the XML ID). Each EDI record and each relationship value are represented using a unique @@ -218,8 +218,9 @@ class edi(object): module, xml_id = _record_xml xml_id = '%s:%s.%s' % (db_uuid, module, xml_id) else: - uuid = safe_unique_id(db_uuid, record._name, record.id) - xml_id = '%s:%s.%s' % (db_uuid, record._module, uuid) + if not xml_id: + uuid = safe_unique_id(db_uuid, record._name, record.id) + xml_id = '%s:%s.%s' % (db_uuid, record._module, uuid) assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id) module, xml_id2 = xml_id.split('.') xml_record_id = model_data_pool.create(cr, uid, { @@ -337,7 +338,7 @@ class edi(object): dict_list = [] for record in records: - dict_list.append(self.edi_m2o(cr, uid, [record], context=None)) + dict_list.append(self.edi_m2o(cr, uid, record, context=None)) return dict_list @@ -394,30 +395,51 @@ class edi(object): edi_dict_list.append(edi_dict) return edi_dict_list - def edi_import_relation(self, cr, uid, relation_model, relation_value, xml_id=None, context=None): - relation_id = False - model_data = self.pool.get('ir.model.data') - relation_object = self.pool.get(relation_model) - relation_ids = relation_object.name_search(cr, uid, relation_value, context=context) - if relation_ids and len(relation_ids) == 1: - relation_id = relation_ids[0][0] - values = {relation_object._rec_name: relation_value} - if not relation_id: - relation_id = relation_object.create(cr, uid, values, context=context) - relation_record = relation_object.browse(cr, uid, relation_id, context=context) - record_xml = self.record_xml_id(cr, uid, relation_record, context=context) + def edi_get_object_by_name(self, cr, uid, value, model_name, context=None): + openobject = False + model = self.pool.get(model_name) + object_ids = model.name_search(cr, uid, value, operator='=', context=context) + if object_ids and len(object_ids) == 1: + object_id = object_ids[0][0] + openobject = model.browse(cr, uid, object_id, context=context) + return openobject + + def edi_get_object(self, cr, uid, xml_id, model, context=None): + model_data = self.pool.get('ir.model.data') + assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id) + module, xml_id2 = xml_id.split('.') + openobject = False + data_ids = model_data.search(cr, uid, [('model','=', model), ('name','=', xml_id2)]) + if data_ids: + model = self.pool.get(model) + data = model_data.browse(cr, uid, data_ids[0], context=context) + openobject = model.browse(cr, uid, data.res_id, context=context) + return openobject + + def edi_import_relation(self, cr, uid, relation_model, relation_value, xml_id=None, context=None): + relation = False + if xml_id: + relation = self.edi_get_object(cr, uid, xml_id, relation_model, context=context) + if not relation: + relation = self.edi_get_object_by_name(cr, uid, relation_value, relation_model, context=context) + if not relation: + relation_model = self.pool.get(relation_model) + values = {} #relation_model.default_get(cr, uid, fields, context=context) + values[relation_model._rec_name] = relation_value + relation_id = relation_model.create(cr, uid, values, context=context) + relation = relation_model.browse(cr, uid, relation_id, context=context) + + record_xml = self.record_xml_id(cr, uid, relation, context=context) if record_xml: module, xml_id = record_xml xml_id = '%s.%s' % (module, xml_id) if not xml_id: - xml_id = self.edi_xml_id(cr, uid, relation_record, context=context) - if xml_id: - assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id) - module, xml_id2 = xml_id.split('.') - #TODO: update record from values + xml_id = self.edi_xml_id(cr, uid, relation, context=context) + + #TODO: update record from values ? #relation_id = model_data._update(cr, uid, relation_model, module, values, xml_id=xml_id, context=context) - return relation_id + return relation and relation.id or False def edi_import(self, cr, uid, edi_document, context=None): @@ -467,64 +489,72 @@ class edi(object): connect it to the parent record via a write value like (4, db_id). """ # generic implementation! - fields_to_import = [] - data_line = [] model_data = self.pool.get('ir.model.data') - _columns = self._all_columns - values = {} - xml_id = edi_document['__id'] - assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id) - module, xml_id2 = xml_id.split('.') - for field in edi_document.keys(): - if not field.startswith('__'): - _column = _columns[field].column - _column_dict = fields.field_to_dict(self, cr, uid, context, _column) - - fields_to_import.append(field) - edi_field_value = edi_document[field] - field_type = _column_dict['type'] - relation_model = _column_dict.get('relation') - if not edi_field_value: + assert self._name == edi_document['__model'], 'EDI Document could not import. Model of EDI Document and current model are does not match' + def process(datas, model_name): + values = {} + model_pool = self.pool.get(model_name) + xml_id = datas['__id'] + assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id) + module, xml_id2 = xml_id.split('.') + _columns = model_pool._all_columns + for field in datas.keys(): + if field not in _columns: continue - if _column_dict.has_key('function') or _column_dict.has_key('related_columns'): - # DO NOT IMPORT FUNCTION FIELD AND RELATED FIELD - continue - elif field_type in ('many2one', 'many2many'): - if field_type == 'many2one': - edi_parent_documents = [edi_field_value] - else: - edi_parent_documents = edi_field_value - - parent_lines = [] - - for edi_parent_document in edi_parent_documents: - relation_id = self.edi_import_relation(cr, uid, relation_model, edi_parent_document[1], edi_parent_document[0], context=context) - parent_lines.append(relation_id) - - if len(parent_lines): + if not field.startswith('__'): + _column = _columns[field].column + _column_dict = fields.field_to_dict(self, cr, uid, context, _column) + + edi_field_value = datas[field] + field_type = _column_dict['type'] + relation_model = _column_dict.get('relation') + #print '???????', field, edi_field_value, relation_model, field_type + if not edi_field_value: + continue + if _column_dict.has_key('function') or _column_dict.has_key('related_columns'): + # DO NOT IMPORT FUNCTION FIELD AND RELATED FIELD + continue + elif field_type == 'one2many': + # recursive call for getting children and returning [(0,0,{})] or [(1,ID,{})] + relations = [] + relation_object = self.pool.get(relation_model) + for edi_relation_document in edi_field_value: + relation = self.edi_get_object(cr, uid, edi_relation_document['__id'], relation_model, context=context) + if not relation and edi_relation_document.get('name'): + relation = self.edi_get_object_by_name(cr, uid, edi_relation_document['name'], relation_model, context=context) + if relation: + self.edi_xml_id(cr, uid, relation, \ + xml_id=edi_relation_document['__id'], context=context) + relations.append((4, relation.id)) + res_module, res_xml_id, newrow = process(edi_relation_document, relation_model) + relations.append( (relation and 1 or 0, relation and relation.id or 0, newrow)) + values[field] = relations + elif field_type in ('many2one', 'many2many'): if field_type == 'many2one': - values[field] = parent_lines[0] - + edi_parent_documents = [edi_field_value] else: - many2many_ids = [] - for m2m_id in parent_lines: - many2many_ids.append((4, m2m_id)) - values[field] = many2many_ids - elif field_type == 'one2many': - #Look for a record that matches the db_id provided in the __id field. If - #found, keep the corresponding database id, and connect it to the parent - #using a write value like (4,db_id). - #If not found via db_id, create a new entry using the same method that - #imports a full EDI record (recursive call!), grab the resulting db id, - #and use it to connect to the parent via a write value like (4, db_id). - - relations = [] - relation_object = self.pool.get(relation_model) - for edi_relation_document in edi_field_value: - relation_id = relation_object.edi_import(cr, uid, edi_relation_document, context=context) - relations.append((4, relation_id)) - values[field] = relations - else: - values[field] = edi_field_value - return model_data._update(cr, uid, self._name, module, values, xml_id=xml_id, context=context) + edi_parent_documents = edi_field_value + + parent_lines = [] + + for edi_parent_document in edi_parent_documents: + relation_id = self.edi_import_relation(cr, uid, relation_model, edi_parent_document[1], edi_parent_document[0], context=context) + parent_lines.append(relation_id) + + if len(parent_lines): + if field_type == 'many2one': + values[field] = parent_lines[0] + + else: + many2many_ids = [] + for m2m_id in parent_lines: + many2many_ids.append((4, m2m_id)) + values[field] = many2many_ids + + else: + values[field] = edi_field_value + return module, xml_id2, values + + module, xml_id, data_values = process(edi_document, self._name) + return model_data._update(cr, uid, self._name, module, data_values, xml_id=xml_id, context=context) # vim: ts=4 sts=4 sw=4 si et diff --git a/openerp/addons/base/res/__init__.py b/openerp/addons/base/res/__init__.py index de00fc54153..6f20b5c4f0e 100644 --- a/openerp/addons/base/res/__init__.py +++ b/openerp/addons/base/res/__init__.py @@ -34,3 +34,5 @@ import res_log import ir_property import res_widget +import edi_res_company + diff --git a/openerp/addons/base/res/edi_res_company.py b/openerp/addons/base/res/edi_res_company.py new file mode 100644 index 00000000000..0eabd39781e --- /dev/null +++ b/openerp/addons/base/res/edi_res_company.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2011 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 . +# +############################################################################## + +from osv import osv +from osv import fields +from tools.translate import _ + +class res_company(osv.osv): + _inherit = "res.company" + + def edi_import_as_partner(self, cr, uid, edi_document, values=None, context=None): + """ + import company as a new partner + company_address data used to add address to new partner + + edi_document is a dict to have company datas + edi_document = { + 'company_address': { + 'street': True, + 'street2': True, + 'zip': True, + 'city': True, + 'state_id': True, + 'country_id': True, + 'email': True, + 'phone': True, + + }, + 'company_id': True, + } + values is a dict to have other datas of partner which are need to import of partner record + values = { + 'customer': True, + 'supplier': True, + } + """ + if values is None: + values = {} + partner_pool = self.pool.get('res.partner') + edi_document_partner = { + '__model': 'res.partner', + '__id' : edi_document['company_id'][0], + 'name' : edi_document['company_id'][1], + 'address': [edi_document['company_address']] + } + edi_document_partner.update(values) + return partner_pool.edi_import(cr, uid, edi_document_partner, context=context) + + def edi_export_address(self, cr, uid, records, edi_address_struct=None, context=None): + if context is None: + context = {} + partner_pool = self.pool.get('res.partner') + partner_address_pool = self.pool.get('res.partner.address') + if edi_address_struct is None: + edi_address_struct = { + 'street': True, + 'street2': True, + 'zip': True, + 'city': True, + 'state_id': True, + 'country_id': True, + 'email': True, + 'phone': True, + + } + edi_company_dict = {} + for company in records: + edi_doc = {} + company_address = False + res = partner_pool.address_get(cr, uid, [company.partner_id.id], ['contact', 'invoice']) + addr_id = res['invoice'] or res['contact'] + if addr_id: + address = partner_address_pool.browse(cr, uid, addr_id, context=context) + ctx = context.copy() + ctx.update({'o2m_export':True}) + edi_address_dict_list = partner_address_pool.edi_export(cr, uid, [address], edi_struct=edi_address_struct, context=ctx) + if edi_address_dict_list: + company_address = edi_address_dict_list[0] + edi_doc.update({ + 'company_address': company_address, + #'company_logo': company.logo,#TODO + }) + edi_company_dict[company.id] = edi_doc + return edi_company_dict +res_company() From 701d20fa258ce98476ff75b1afdd2b06638c46e9 Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Tue, 13 Sep 2011 18:54:16 +0530 Subject: [PATCH 077/526] [IMP] base.ir.edi: introduce new method: split_xml_id to split module and xml_id from uuid of edi_document bzr revid: hmo@tinyerp.com-20110913132416-1putaa2y3rxzqunc --- openerp/addons/base/ir/ir_edi.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 1d36a879036..062517b1f87 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -29,6 +29,11 @@ import openerp.release as release from tools.translate import _ import netsvc +def split_xml_id(xml_id): + assert len(xml_id.split('.'))==2, \ + _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id) + return xml_id.split('.') + def safe_unique_id(database_id, model, record_id): """Generate a unique string to represent a (database id,model,record_id) pair without being too long, without revealing the database id, and @@ -221,8 +226,7 @@ class edi(object): if not xml_id: uuid = safe_unique_id(db_uuid, record._name, record.id) xml_id = '%s:%s.%s' % (db_uuid, record._module, uuid) - assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id) - module, xml_id2 = xml_id.split('.') + module, xml_id2 = split_xml_id(xml_id) xml_record_id = model_data_pool.create(cr, uid, { 'name': xml_id2, 'model': record._name, @@ -407,8 +411,7 @@ class edi(object): def edi_get_object(self, cr, uid, xml_id, model, context=None): model_data = self.pool.get('ir.model.data') - assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id) - module, xml_id2 = xml_id.split('.') + module, xml_id2 = split_xml_id(xml_id) openobject = False data_ids = model_data.search(cr, uid, [('model','=', model), ('name','=', xml_id2)]) if data_ids: @@ -495,8 +498,7 @@ class edi(object): values = {} model_pool = self.pool.get(model_name) xml_id = datas['__id'] - assert len(xml_id.split('.'))==2, _("'%s' contains too many dots. XML ids should not contain dots ! These are used to refer to other modules data, as in module.reference_id") % (xml_id) - module, xml_id2 = xml_id.split('.') + module, xml_id2 = split_xml_id(xml_id) _columns = model_pool._all_columns for field in datas.keys(): if field not in _columns: @@ -508,7 +510,6 @@ class edi(object): edi_field_value = datas[field] field_type = _column_dict['type'] relation_model = _column_dict.get('relation') - #print '???????', field, edi_field_value, relation_model, field_type if not edi_field_value: continue if _column_dict.has_key('function') or _column_dict.has_key('related_columns'): From 0e1765a4d5ac181bb351d8adc3cc40999eab22a5 Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Wed, 14 Sep 2011 15:55:05 +0530 Subject: [PATCH 078/526] [FIX] workflow: pass active_model in context on run server action bzr revid: hmo@tinyerp.com-20110914102505-03tzr0ncfsngnqnu --- openerp/workflow/wkf_expr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/workflow/wkf_expr.py b/openerp/workflow/wkf_expr.py index 468e5225212..608af34d118 100644 --- a/openerp/workflow/wkf_expr.py +++ b/openerp/workflow/wkf_expr.py @@ -60,7 +60,7 @@ def _eval_expr(cr, ident, workitem, action): def execute_action(cr, ident, workitem, activity): obj = pooler.get_pool(cr.dbname).get('ir.actions.server') - ctx = {'active_id':ident[2], 'active_ids':[ident[2]]} + ctx = {'active_model':ident[1], 'active_id':ident[2], 'active_ids':[ident[2]]} result = obj.run(cr, ident[0], [activity['action_id']], ctx) return result From 8a9c0dbad7323509b07cda605aed84bf51584753 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 13:12:13 +0200 Subject: [PATCH 079/526] [IMP] bulk-update fields_view_get result via dict.update instead of a bunch of __setitem__ bzr revid: xmo@openerp.com-20110915111213-zigij077k9ss9p6e --- openerp/osv/orm.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 7efe2272754..315d2bce0de 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1954,14 +1954,13 @@ class orm_template(object): # if a view was found if sql_res: - result['type'] = sql_res['type'] - result['view_id'] = sql_res['id'] - source = etree.fromstring(encode(sql_res['arch'])) - result['arch'] = apply_view_inheritance(source, result['view_id']) - - result['name'] = sql_res['name'] - result['field_parent'] = sql_res['field_parent'] or False + result.update( + arch=apply_view_inheritance(source, sql_res['id']), + type=sql_res['type'], + view_id=sql_res['id'], + name=sql_res['name'], + field_parent=sql_res['field_parent'] or False) else: # otherwise, build some kind of default view @@ -1994,10 +1993,11 @@ class orm_template(object): else: # what happens here, graph case? raise except_orm(_('Invalid Architecture!'), _("There is no view of type '%s' defined for the structure!") % view_type) - result['arch'] = etree.fromstring(encode(xml)) - result['name'] = 'default' - result['field_parent'] = False - result['view_id'] = 0 + result.update( + arch=etree.fromstring(encode(xml)), + name='default', + field_parent=False, + view_id=0) if parent_view_model != self._name: ctx = context.copy() From 734c1a43a0e4a208185ab967cc1bf087a2d15e5d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 13:12:21 +0200 Subject: [PATCH 080/526] [REF] extract the two inlined default view generators left into fields_view_get into their own methods follow the preexisting __get_default_$name_view bzr revid: xmo@openerp.com-20110915111221-20ktl1bi2qne5wz7 --- openerp/osv/orm.py | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 315d2bce0de..960217b4de8 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1689,6 +1689,28 @@ class orm_template(object): raise except_orm('View error', msg) return arch, fields + def __get_default_form_view(self, cr, user, context=None): + # TODO it seems fields_get can be replaced by _all_columns (no need for translation) + res = self.fields_get(cr, user, context=context) + xml = ' ' \ + '
' % (self._description,) + for x in res: + if res[x]['type'] not in ('one2many', 'many2many'): + xml += '' % (x,) + if res[x]['type'] == 'text': + xml += "" + xml += "" + return xml + + def __get_default_tree_view(self, cr, user, context=None): + _rec_name = self._rec_name + if _rec_name not in self._columns: + _rec_name = self._columns.keys()[0] + xml = '' \ + '' \ + % (self._description, _rec_name) + return xml + def __get_default_calendar_view(self): """Generate a default calendar view (For internal use only). """ @@ -1965,24 +1987,10 @@ class orm_template(object): # otherwise, build some kind of default view if view_type == 'form': - # TODO it seems fields_get can be replaced by _all_columns (no need for translation) - res = self.fields_get(cr, user, context=context) - xml = ' ' \ - '
' % (self._description,) - for x in res: - if res[x]['type'] not in ('one2many', 'many2many'): - xml += '' % (x,) - if res[x]['type'] == 'text': - xml += "" - xml += "" + xml = self.__get_default_form_view(cr, user, context) elif view_type == 'tree': - _rec_name = self._rec_name - if _rec_name not in self._columns: - _rec_name = self._columns.keys()[0] - xml = '' \ - '' \ - % (self._description, _rec_name) + xml = self.__get_default_tree_view(cr, user, context) elif view_type == 'calendar': xml = self.__get_default_calendar_view() From ebb2072207c23fe1ad12310207a72ac27e2a754e Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 13:12:55 +0200 Subject: [PATCH 081/526] [IMP] regroup all __get_default_$name_view calls into a single parametric getattr Also rename __get_default_ to _get_default_ so it can be found by getattr bzr revid: xmo@openerp.com-20110915111255-1i0y555er3nwtkzg --- openerp/osv/orm.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 960217b4de8..773feb5808d 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1689,7 +1689,7 @@ class orm_template(object): raise except_orm('View error', msg) return arch, fields - def __get_default_form_view(self, cr, user, context=None): + def _get_default_form_view(self, cr, user, context=None): # TODO it seems fields_get can be replaced by _all_columns (no need for translation) res = self.fields_get(cr, user, context=context) xml = ' ' \ @@ -1702,7 +1702,7 @@ class orm_template(object): xml += "" return xml - def __get_default_tree_view(self, cr, user, context=None): + def _get_default_tree_view(self, cr, user, context=None): _rec_name = self._rec_name if _rec_name not in self._columns: _rec_name = self._columns.keys()[0] @@ -1711,7 +1711,7 @@ class orm_template(object): % (self._description, _rec_name) return xml - def __get_default_calendar_view(self): + def _get_default_calendar_view(self, cr, user, context=None): """Generate a default calendar view (For internal use only). """ # TODO could return an etree instead of a string @@ -1758,7 +1758,7 @@ class orm_template(object): return arch - def __get_default_search_view(self, cr, uid, context=None): + def _get_default_search_view(self, cr, uid, context=None): form_view = self.fields_view_get(cr, uid, False, 'form', context=context) tree_view = self.fields_view_get(cr, uid, False, 'tree', context=context) @@ -1984,23 +1984,14 @@ class orm_template(object): name=sql_res['name'], field_parent=sql_res['field_parent'] or False) else: - # otherwise, build some kind of default view - if view_type == 'form': - xml = self.__get_default_form_view(cr, user, context) - - elif view_type == 'tree': - xml = self.__get_default_tree_view(cr, user, context) - - elif view_type == 'calendar': - xml = self.__get_default_calendar_view() - - elif view_type == 'search': - xml = self.__get_default_search_view(cr, user, context) - - else: + try: + xml = getattr(self, '_get_default_%s_view' % view_type)( + cr, user, context) + except AttributeError: # what happens here, graph case? raise except_orm(_('Invalid Architecture!'), _("There is no view of type '%s' defined for the structure!") % view_type) + result.update( arch=etree.fromstring(encode(xml)), name='default', From 005e2b87a57bba82cec4b73b26ce8af6392ffc4d Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:04:29 +0200 Subject: [PATCH 082/526] [IMP] etree-ify default view generations * Search already used etree internally, remove serialization to string * form and tree were easy to convert * pull down fvg's parsing (etree.fromstring) into calendar generator before converting it bzr revid: xmo@openerp.com-20110915120429-syz190w61iq52rel --- openerp/osv/orm.py | 70 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 773feb5808d..c60c440af24 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1690,32 +1690,53 @@ class orm_template(object): return arch, fields def _get_default_form_view(self, cr, user, context=None): + """ Generates a default single-line form view using all fields + of the current model except the m2m and o2m ones. + + :param cr: database cursor + :param int user: user id + :param dict context: connection context + :returns: a form view as an lxml document + :rtype: etree._Element + """ + view = etree.Element('form', string=self._description) # TODO it seems fields_get can be replaced by _all_columns (no need for translation) - res = self.fields_get(cr, user, context=context) - xml = ' ' \ - '
' % (self._description,) - for x in res: - if res[x]['type'] not in ('one2many', 'many2many'): - xml += '' % (x,) - if res[x]['type'] == 'text': - xml += "" - xml += "" - return xml + for field, descriptor in self.fields_get(cr, user, context=context).iteritems(): + if descriptor['type'] in ('one2many', 'many2many'): + continue + etree.SubElement(view, 'field', name=field) + if descriptor['type'] == 'text': + etree.SubElement(view, 'newline') + return view def _get_default_tree_view(self, cr, user, context=None): + """ Generates a single-field tree view, using _rec_name if + it's one of the columns or the first column it finds otherwise + + :param cr: database cursor + :param int user: user id + :param dict context: connection context + :returns: a tree view as an lxml document + :rtype: etree._Element + """ _rec_name = self._rec_name if _rec_name not in self._columns: _rec_name = self._columns.keys()[0] - xml = '' \ - '' \ - % (self._description, _rec_name) - return xml + + view = etree.Element('tree', string=self._description) + etree.SubElement(view, 'field', name=_rec_name) + return view def _get_default_calendar_view(self, cr, user, context=None): - """Generate a default calendar view (For internal use only). + """ Generates a default calendar view by trying to infer + calendar fields from a number of pre-set attribute names + + :param cr: database cursor + :param int user: user id + :param dict context: connection context + :returns: a calendar view + :rtype: etree._Element """ - # TODO could return an etree instead of a string - arch = ('\n' '\n' '') % (self._rec_name) - return arch + return etree.fromstring(arch.encode('utf-8')) def _get_default_search_view(self, cr, uid, context=None): + """ + :param cr: database cursor + :param int user: user id + :param dict context: connection context + :returns: an lxml document of the view + :rtype: etree._Element + """ form_view = self.fields_view_get(cr, uid, False, 'form', context=context) tree_view = self.fields_view_get(cr, uid, False, 'tree', context=context) @@ -1783,7 +1811,7 @@ class orm_template(object): field_group.append(etree.Element("field", attrib={'name': field_name})) #TODO tostring can be removed as fromstring is call directly after... - return etree.tostring(search_view, encoding="utf-8").replace('\t', '') + return search_view # # if view_id, view_type is not required @@ -1986,14 +2014,14 @@ class orm_template(object): else: # otherwise, build some kind of default view try: - xml = getattr(self, '_get_default_%s_view' % view_type)( + view = getattr(self, '_get_default_%s_view' % view_type)( cr, user, context) except AttributeError: # what happens here, graph case? raise except_orm(_('Invalid Architecture!'), _("There is no view of type '%s' defined for the structure!") % view_type) result.update( - arch=etree.fromstring(encode(xml)), + arch=view, name='default', field_parent=False, view_id=0) From 4cdda26b6c959d2c6c7323dd9e173f352e83c788 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:11:29 +0200 Subject: [PATCH 083/526] [IMP] convert default calendar view generation (from fields_view_get) to lxml.etree bzr revid: xmo@openerp.com-20110915121129-fcdq8e0rix2ocpwt --- openerp/osv/orm.py | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index c60c440af24..e8556dbc9d4 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1737,8 +1737,8 @@ class orm_template(object): :returns: a calendar view :rtype: etree._Element """ - arch = ('\n' - '\n' - ' \n' - '') % (self._rec_name) - - return etree.fromstring(arch.encode('utf-8')) + return view def _get_default_search_view(self, cr, uid, context=None): """ From 74e98f422043bec3b2b10067b1e5665eeb9f9aff Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:15:44 +0200 Subject: [PATCH 084/526] [ADD] error when neither date_stop nor date_delay can be generated for a default calendar view bzr revid: xmo@openerp.com-20110915121544-eq3mumulb0qp9yfa --- openerp/osv/orm.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index e8556dbc9d4..1d9fb9ccfa0 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1761,12 +1761,15 @@ class orm_template(object): if dt_stop in self._columns: view.set('date_stop', dt_stop) break - - if not view.get('date_stop'): + if 'date_stop' not in view.attrib: for dt_delay in ["date_delay", "planned_hours", "x_date_delay", "x_planned_hours"]: if dt_delay in self._columns: view.set('date_delay', dt_delay) break + if 'date_delay' not in view.attrib: + raise except_orm( + _('Invalid Object Architecture!'), + _("Insufficient fields to generate a Calendar View for %s, missing a date_stop or a date_delay" % (self._name))) return view From e878560c7450f4f3bbf7b43d93aa04214218a07a Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:24:43 +0200 Subject: [PATCH 085/526] [ADD] small helper to calendar generation code, in order to make flow clearer bzr revid: xmo@openerp.com-20110915122443-hvk1u2372ppd77m1 --- openerp/osv/orm.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 1d9fb9ccfa0..c0ca7e9d696 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1737,6 +1737,19 @@ class orm_template(object): :returns: a calendar view :rtype: etree._Element """ + def set_first_of(seq, in_, to): + """Sets the first value of ``set`` also found in ``in`` to + the ``to`` attribute of the view being closed over. + + Returns whether it's found a suitable value (and set it on + the attribute) or not + """ + for item in seq: + if item in in_: + view.set(key, item) + return True + return False + view = etree.Element('calendar', string=self._description) etree.SubElement(view, 'field', name=self._rec_name) @@ -1752,21 +1765,13 @@ class orm_template(object): raise except_orm(_('Invalid Object Architecture!'), _("Insufficient fields for Calendar View!")) view.set('date_start', self._date_name) - for color in ["user_id", "partner_id", "x_user_id", "x_partner_id"]: - if color in self._columns: - view.set('color', color) - break + set_first_of(["user_id", "partner_id", "x_user_id", "x_partner_id"], + self._columns, 'color') - for dt_stop in ["date_stop", "date_end", "x_date_stop", "x_date_end"]: - if dt_stop in self._columns: - view.set('date_stop', dt_stop) - break - if 'date_stop' not in view.attrib: - for dt_delay in ["date_delay", "planned_hours", "x_date_delay", "x_planned_hours"]: - if dt_delay in self._columns: - view.set('date_delay', dt_delay) - break - if 'date_delay' not in view.attrib: + if not set_first_of(["date_stop", "date_end", "x_date_stop", "x_date_end"], + self._columns, 'date_stop'): + if not set_first_of(["date_delay", "planned_hours", "x_date_delay", "x_planned_hours"], + self._columns, 'date_delay'): raise except_orm( _('Invalid Object Architecture!'), _("Insufficient fields to generate a Calendar View for %s, missing a date_stop or a date_delay" % (self._name))) From eeb31efa882408be3bbdb98afb928bd8a297edb7 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:28:34 +0200 Subject: [PATCH 086/526] [IMP] automatic search view generation: comprehensions are awesome bzr revid: xmo@openerp.com-20110915122834-0ec4dkaznbhtc0kc --- openerp/osv/orm.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index c0ca7e9d696..48b607273de 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1789,12 +1789,11 @@ class orm_template(object): form_view = self.fields_view_get(cr, uid, False, 'form', context=context) tree_view = self.fields_view_get(cr, uid, False, 'tree', context=context) - fields_to_search = set() # TODO it seems _all_columns could be used instead of fields_get (no need for translated fields info) fields = self.fields_get(cr, uid, context=context) - for field in fields: - if fields[field].get('select'): - fields_to_search.add(field) + fields_to_search = set( + field for field, descriptor in fields.iteritems() + if descriptor.get('select')) for view in (form_view, tree_view): view_root = etree.fromstring(view['arch']) # Only care about select=1 in xpath below, because select=2 is covered From 7bb856b834b6ba30f406459d0b36b34dcd351973 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:31:55 +0200 Subject: [PATCH 087/526] [IMP] search view generation: update fields set in place instead of creating a new set via union bzr revid: xmo@openerp.com-20110915123155-a5ht8nhfp9kq0kst --- openerp/osv/orm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 48b607273de..625247d464f 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1794,11 +1794,12 @@ class orm_template(object): fields_to_search = set( field for field, descriptor in fields.iteritems() if descriptor.get('select')) + for view in (form_view, tree_view): view_root = etree.fromstring(view['arch']) # Only care about select=1 in xpath below, because select=2 is covered # by the custom advanced search in clients - fields_to_search = fields_to_search.union(view_root.xpath("//field[@select=1]/@name")) + fields_to_search.update(view_root.xpath("//field[@select=1]/@name")) tree_view_root = view_root # as provided by loop above search_view = etree.Element("search", attrib={'string': tree_view_root.get("string", "")}) From bd03e6aabfe178476894912df1025a17253c6ddc Mon Sep 17 00:00:00 2001 From: "RavishchanraMurari (Open ERP)" Date: Thu, 15 Sep 2011 18:06:42 +0530 Subject: [PATCH 088/526] project_issue_kanban bzr revid: rmu@tinyerp.com-20110915123642-hiq0sfcunrciytk3 --- addons/project_issue/project_issue.py | 18 ++++- addons/project_issue/project_issue_menu.xml | 2 +- addons/project_issue/project_issue_view.xml | 75 +++++++++++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/addons/project_issue/project_issue.py b/addons/project_issue/project_issue.py index 8237159af7a..128be8c2191 100644 --- a/addons/project_issue/project_issue.py +++ b/addons/project_issue/project_issue.py @@ -227,6 +227,7 @@ class project_issue(crm.crm_case, osv.osv): multi='compute_day', type="float", store=True), 'inactivity_days': fields.function(_compute_day, string='Days since last action', \ multi='compute_day', type="integer", help="Difference in days between last action and current date"), + 'color': fields.integer('Color Index'), 'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', domain=[('model','=',_name)]), 'date_action_last': fields.datetime('Last Action', readonly=1), 'date_action_next': fields.datetime('Next Action', readonly=1), @@ -268,7 +269,22 @@ class project_issue(crm.crm_case, osv.osv): 'project_id':_get_project, 'categ_id' : lambda *a: False, #'assigned_to' : lambda obj, cr, uid, context: uid, - } + } + + def set_priority(self, cr, uid, ids, priority): + """Set lead priority + """ + return self.write(cr, uid, ids, {'priority' : priority}) + + def set_high_priority(self, cr, uid, ids, *args): + """Set lead priority to high + """ + return self.set_priority(cr, uid, ids, '1') + + def set_normal_priority(self, cr, uid, ids, *args): + """Set lead priority to normal + """ + return self.set_priority(cr, uid, ids, '3') def convert_issue_task(self, cr, uid, ids, context=None): case_obj = self.pool.get('project.issue') diff --git a/addons/project_issue/project_issue_menu.xml b/addons/project_issue/project_issue_menu.xml index 18c8779704f..0915b2b96d7 100644 --- a/addons/project_issue/project_issue_menu.xml +++ b/addons/project_issue/project_issue_menu.xml @@ -10,7 +10,7 @@ Issues project.issue form - tree,calendar + tree,calendar,kanban {"search_default_user_id": uid, "search_default_current":1, "search_default_project_id":project_id} diff --git a/addons/project_issue/project_issue_view.xml b/addons/project_issue/project_issue_view.xml index 0bf171d6aeb..b54690a3deb 100644 --- a/addons/project_issue/project_issue_view.xml +++ b/addons/project_issue/project_issue_view.xml @@ -263,6 +263,81 @@
+ + + + + Project Issue Kanban + project.issue + kanban + + + + + + + + + + + + +
+
+ + + + + + +
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+ Version +
+
+ + +
+
+
+
+
+
+
+ + + + + + # ------------------------------------------------------ # Feature Requests # ------------------------------------------------------ From a09be0e9b94626d45db20272ba9ecab469aa8c66 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:37:07 +0200 Subject: [PATCH 089/526] [IMP] simplify search view generation: rely on etree.SubElement to graft new nodes to the search view bzr revid: xmo@openerp.com-20110915123707-901mkfkz8e6ikre4 --- openerp/osv/orm.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 625247d464f..8070d56d9e4 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -1802,14 +1802,12 @@ class orm_template(object): fields_to_search.update(view_root.xpath("//field[@select=1]/@name")) tree_view_root = view_root # as provided by loop above - search_view = etree.Element("search", attrib={'string': tree_view_root.get("string", "")}) - field_group = etree.Element("group") - search_view.append(field_group) + search_view = etree.Element("search", string=tree_view_root.get("string", "")) + field_group = etree.SubElement(search_view, "group") for field_name in fields_to_search: - field_group.append(etree.Element("field", attrib={'name': field_name})) + etree.SubElement(field_group, "field", name=field_name)) - #TODO tostring can be removed as fromstring is call directly after... return search_view # From 662c8842f4eef498feb6711c87a3fdd2c75a3611 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Thu, 15 Sep 2011 14:47:41 +0200 Subject: [PATCH 090/526] [IMP] fields_view_get: listcomps are still awesome also use itertools.chain to iterate over multiple lists in sequence, instead of concatenating them bzr revid: xmo@openerp.com-20110915124741-v5uneqmt2c3v6cmp --- openerp/osv/orm.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openerp/osv/orm.py b/openerp/osv/orm.py index 8070d56d9e4..c08fe1e2357 100644 --- a/openerp/osv/orm.py +++ b/openerp/osv/orm.py @@ -44,6 +44,7 @@ import calendar import copy import datetime +import itertools import logging import warnings import operator @@ -2052,13 +2053,11 @@ class orm_template(object): resrelate = ir_values_obj.get(cr, user, 'action', 'client_action_relate', [(self._name, False)], False, context) - resprint = map(clean, resprint) - resaction = map(clean, resaction) - resaction = filter(lambda x: not x.get('multi', False), resaction) - resprint = filter(lambda x: not x.get('multi', False), resprint) + resaction = [clean(action) for action in resaction if not action.get('multi')] + resprint = [clean(print_) for print_ in resprint if not print_.get('multi')] resrelate = map(lambda x: x[2], resrelate) - for x in resprint + resaction + resrelate: + for x in itertools.chain(resprint, resaction, resrelate): x['string'] = x['name'] result['toolbar'] = { From d7e38b95b498b8f1c8028327a8dbd636cddbd0d0 Mon Sep 17 00:00:00 2001 From: "RavishchanraMurari (Open ERP)" Date: Fri, 16 Sep 2011 12:05:55 +0530 Subject: [PATCH 091/526] project_issue_karban_view bzr revid: rmu@tinyerp.com-20110916063555-m9l28fb9ravuf7r9 --- addons/project_issue/project_issue_view.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/addons/project_issue/project_issue_view.xml b/addons/project_issue/project_issue_view.xml index b54690a3deb..9c10b958d80 100644 --- a/addons/project_issue/project_issue_view.xml +++ b/addons/project_issue/project_issue_view.xml @@ -284,13 +284,13 @@
- - - +
+ + +
-
@@ -306,7 +306,7 @@
- Version + Version
From 52c5e2971031f0ada8497e631ac1cdb2f9e23d7e Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Fri, 16 Sep 2011 15:23:28 +0530 Subject: [PATCH 092/526] [FIX] edi_company: add default addess bzr revid: hmo@tinyerp.com-20110916095328-8wlmqjz4wjfrt0ir --- openerp/addons/base/res/edi_res_company.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openerp/addons/base/res/edi_res_company.py b/openerp/addons/base/res/edi_res_company.py index 0eabd39781e..945678c3259 100644 --- a/openerp/addons/base/res/edi_res_company.py +++ b/openerp/addons/base/res/edi_res_company.py @@ -85,8 +85,8 @@ class res_company(osv.osv): for company in records: edi_doc = {} company_address = False - res = partner_pool.address_get(cr, uid, [company.partner_id.id], ['contact', 'invoice']) - addr_id = res['invoice'] or res['contact'] + res = partner_pool.address_get(cr, uid, [company.partner_id.id], ['default', 'contact', 'invoice']) + addr_id = res['invoice'] or res['contact'] or res['default'] if addr_id: address = partner_address_pool.browse(cr, uid, addr_id, context=context) ctx = context.copy() From dc1ba50c5e1f857e869b1f6aa508f7517bc8a4a6 Mon Sep 17 00:00:00 2001 From: "Amit (OpenERP)" Date: Fri, 16 Sep 2011 15:42:14 +0530 Subject: [PATCH 093/526] [IMP]:hr:added kanban view for hr employess. bzr revid: apa@tinyerp.com-20110916101214-hlff8uqv8y87h5bi --- addons/hr/__openerp__.py | 2 +- addons/hr/hr.py | 5 ++- addons/hr/hr_view.xml | 83 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/addons/hr/__openerp__.py b/addons/hr/__openerp__.py index 598b49a725e..bca65c54cb2 100644 --- a/addons/hr/__openerp__.py +++ b/addons/hr/__openerp__.py @@ -38,7 +38,7 @@ You can manage: 'author': 'OpenERP SA', 'website': 'http://www.openerp.com', 'images': ['images/hr_department.jpeg', 'images/hr_employee.jpeg','images/hr_job_position.jpeg'], - 'depends': ['base_setup', 'resource', 'board'], + 'depends': ['base_setup','mail', 'resource', 'board'], 'init_xml': [], 'update_xml': [ 'security/hr_security.xml', diff --git a/addons/hr/hr.py b/addons/hr/hr.py index 1424d5e3378..512985b9b32 100644 --- a/addons/hr/hr.py +++ b/addons/hr/hr.py @@ -149,7 +149,9 @@ class hr_employee(osv.osv): 'coach_id': fields.many2one('hr.employee', 'Coach'), 'job_id': fields.many2one('hr.job', 'Job'), 'photo': fields.binary('Photo'), - 'passport_id':fields.char('Passport No', size=64) + 'passport_id':fields.char('Passport No', size=64), + 'color': fields.integer('Color Index'), + 'city': fields.related('address_id', 'city', type='char', string='City'), } def unlink(self, cr, uid, ids, context=None): @@ -198,6 +200,7 @@ class hr_employee(osv.osv): 'active': 1, 'photo': _get_photo, 'marital': 'single', + 'color': 0, } def _check_recursion(self, cr, uid, ids, context=None): diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml index fb10baedc9e..d71f78a3053 100644 --- a/addons/hr/hr_view.xml +++ b/addons/hr/hr_view.xml @@ -125,7 +125,72 @@
- + + + HR - Employess Kanban + hr.employee + kanban + + + + + + + + + + + + + + + + +
+ Employees Structure hr.employee @@ -149,13 +214,27 @@ Employees hr.employee form - tree,form + tree,form,kanban [] {"search_default_department_id": department_id,"search_default_active":eval('True')} Here you can manage your work force by creating employees and assigning them specific properties in the system. Maintain all employee related information and keep track of anything that needs to be recorded for them. The personal information tab will help you maintain their identity data. The Categories tab gives you the opportunity to assign them related employee categories depending on their position and activities within the company. A category can be a seniority level within the company or a department. The Timesheets tab allows to assign them a specific timesheet and analytic journal where they will be able to enter time through the system. In the note tab, you can enter text data that should be recorded for a specific employee. + + + + tree + + + + + + + form + + + From fce6f5932b1859db8e26dffff5fbb0d445885df5 Mon Sep 17 00:00:00 2001 From: "Tejas (OpenERP)" Date: Fri, 16 Sep 2011 16:15:01 +0530 Subject: [PATCH 094/526] [kanban] contacts view for kanban bzr revid: tta@openerp.com-20110916104501-tuwftefbl7lumix7 --- openerp/addons/base/res/res_partner.py | 1 + openerp/addons/base/res/res_partner_view.xml | 64 ++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 4b8f1279787..2e692e2d4b7 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -300,6 +300,7 @@ class res_partner_address(osv.osv): 'active': fields.boolean('Active', help="Uncheck the active field to hide the contact."), # 'company_id': fields.related('partner_id','company_id',type='many2one',relation='res.company',string='Company', store=True), 'company_id': fields.many2one('res.company', 'Company',select=1), + 'color': fields.integer('Color Index'), } _defaults = { 'active': lambda *a: 1, diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml index 8b26023701d..c47edc57aad 100644 --- a/openerp/addons/base/res/res_partner_view.xml +++ b/openerp/addons/base/res/res_partner_view.xml @@ -105,11 +105,75 @@
+ + + res.partner.address.kanban + res.partner.address + kanban + + + + + + + + + + + + + + + + + +
+ + Addresses ir.actions.act_window res.partner.address form + tree,form,kanban {"search_default_customer":1} Customers (also called Partners in other areas of the system) helps you manage your address book of companies whether they are prospects, customers and/or suppliers. The partner form allows you to track and record all the necessary information to interact with your partners from the company address to their contacts as well as pricelists, and much more. If you installed the CRM, with the history tab, you can track all the interactions with a partner such as opportunities, emails, or sales orders issued. From 63987af41eac0814801dfdb7bc97f6d7109472ab Mon Sep 17 00:00:00 2001 From: "Harry (OpenERP)" Date: Fri, 16 Sep 2011 16:38:00 +0530 Subject: [PATCH 095/526] [FIX] edi: get partner already exits or not using get_object and get_object_by_name. bzr revid: hmo@tinyerp.com-20110916110800-8dec58lrgyilv144 --- openerp/addons/base/ir/ir_edi.py | 11 +++++---- openerp/addons/base/res/edi_res_company.py | 26 +++++++++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/openerp/addons/base/ir/ir_edi.py b/openerp/addons/base/ir/ir_edi.py index 062517b1f87..e830e18428f 100644 --- a/openerp/addons/base/ir/ir_edi.py +++ b/openerp/addons/base/ir/ir_edi.py @@ -420,6 +420,12 @@ class edi(object): openobject = model.browse(cr, uid, data.res_id, context=context) return openobject + def edi_create_relation(self, cr, uid, relation_model, relation_value, context=None): + relation_model = self.pool.get(relation_model) + values = {} #relation_model.default_get(cr, uid, fields, context=context) + values[relation_model._rec_name] = relation_value + return relation_model.create(cr, uid, values, context=context) + def edi_import_relation(self, cr, uid, relation_model, relation_value, xml_id=None, context=None): relation = False if xml_id: @@ -427,10 +433,7 @@ class edi(object): if not relation: relation = self.edi_get_object_by_name(cr, uid, relation_value, relation_model, context=context) if not relation: - relation_model = self.pool.get(relation_model) - values = {} #relation_model.default_get(cr, uid, fields, context=context) - values[relation_model._rec_name] = relation_value - relation_id = relation_model.create(cr, uid, values, context=context) + relation_id = self.edi_create_relation(cr, uid, relation_model, relation_value, context=context) relation = relation_model.browse(cr, uid, relation_id, context=context) record_xml = self.record_xml_id(cr, uid, relation, context=context) diff --git a/openerp/addons/base/res/edi_res_company.py b/openerp/addons/base/res/edi_res_company.py index 945678c3259..53ddf487996 100644 --- a/openerp/addons/base/res/edi_res_company.py +++ b/openerp/addons/base/res/edi_res_company.py @@ -54,13 +54,29 @@ class res_company(osv.osv): """ if values is None: values = {} - partner_pool = self.pool.get('res.partner') + partner_model = 'res.partner' + partner_pool = self.pool.get(partner_model) + xml_id = edi_document['company_id'][0] + company_address = edi_document.get('company_address', False) + partner_name = edi_document['company_id'][1] + partner = partner_pool.edi_get_object(cr, uid, xml_id, partner_model, context=context) + if not partner: + partner = partner_pool.edi_get_object_by_name(cr, uid, partner_name, partner_model, context=context) + + if partner: + record_xml = partner_pool.record_xml_id(cr, uid, partner, context=context) + if record_xml: + module, xml_id = record_xml + xml_id = '%s.%s' % (module, xml_id) + edi_document_partner = { - '__model': 'res.partner', - '__id' : edi_document['company_id'][0], - 'name' : edi_document['company_id'][1], - 'address': [edi_document['company_address']] + '__model': partner_model, + '__id' : xml_id, + 'name' : partner_name, } + if company_address: + edi_document_partner['address'] = [company_address] + edi_document_partner.update(values) return partner_pool.edi_import(cr, uid, edi_document_partner, context=context) From 00a2a259301fe612f8d7247563ff956a3b5f929e Mon Sep 17 00:00:00 2001 From: "Tejas (OpenERP)" Date: Fri, 16 Sep 2011 16:45:26 +0530 Subject: [PATCH 096/526] [kanban] id changed and make email and phone italic bzr revid: tta@openerp.com-20110916111526-znb3mc918jfzosw7 --- openerp/addons/base/res/res_partner_view.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml index c47edc57aad..35c31cb0b06 100644 --- a/openerp/addons/base/res/res_partner_view.xml +++ b/openerp/addons/base/res/res_partner_view.xml @@ -106,7 +106,7 @@ - + res.partner.address.kanban res.partner.address kanban @@ -143,9 +143,9 @@ ,
,
- + , - + From 2e35f396065201547bb289a458e9e51e6d343798 Mon Sep 17 00:00:00 2001 From: "Tejas (OpenERP)" Date: Fri, 16 Sep 2011 17:19:45 +0530 Subject: [PATCH 097/526] [kanban] bold tag removed bzr revid: tta@openerp.com-20110916114945-3zge1h6e6x2ohxnm --- openerp/addons/base/res/res_partner_view.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml index 35c31cb0b06..722d11c0770 100644 --- a/openerp/addons/base/res/res_partner_view.xml +++ b/openerp/addons/base/res/res_partner_view.xml @@ -106,7 +106,7 @@
- + res.partner.address.kanban res.partner.address kanban @@ -142,10 +142,10 @@ ,
- ,
- + ,
+ , -
+ From 7123f18cc0eb19c36b9cc08d3755864978ffa215 Mon Sep 17 00:00:00 2001 From: "Amit (OpenERP)" Date: Fri, 16 Sep 2011 17:33:52 +0530 Subject: [PATCH 098/526] [IMP]:applied amb's patch and improved it for add the kanban view for hr_recruitment. bzr revid: apa@tinyerp.com-20110916120352-4o5ycwzx3p9zktax --- addons/hr_recruitment/hr_recruitment.py | 17 ++++ addons/hr_recruitment/hr_recruitment_menu.xml | 2 +- addons/hr_recruitment/hr_recruitment_view.xml | 79 +++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/addons/hr_recruitment/hr_recruitment.py b/addons/hr_recruitment/hr_recruitment.py index 7fde90dc8e9..1499243500b 100644 --- a/addons/hr_recruitment/hr_recruitment.py +++ b/addons/hr_recruitment/hr_recruitment.py @@ -177,6 +177,7 @@ class hr_applicant(crm.crm_case, osv.osv): multi='day_open', type="float", store=True), 'day_close': fields.function(_compute_day, string='Days to Close', \ multi='day_close', type="float", store=True), + 'color': fields.integer('Color Index'), } def _get_stage(self, cr, uid, context=None): @@ -191,6 +192,7 @@ class hr_applicant(crm.crm_case, osv.osv): 'priority': lambda *a: '', 'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c), 'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0], + 'color': 0, } def onchange_job(self,cr, uid, ids, job, context=None): @@ -440,6 +442,21 @@ class hr_applicant(crm.crm_case, osv.osv): res = super(hr_applicant, self).case_reset(cr, uid, ids, *args) self.write(cr, uid, ids, {'date_open': False, 'date_closed': False}) return res + + def set_priority(self, cr, uid, ids, priority): + """Set lead priority + """ + return self.write(cr, uid, ids, {'priority' : priority}) + + def set_high_priority(self, cr, uid, ids, *args): + """Set lead priority to high + """ + return self.set_priority(cr, uid, ids, '1') + + def set_normal_priority(self, cr, uid, ids, *args): + """Set lead priority to normal + """ + return self.set_priority(cr, uid, ids, '3') hr_applicant() diff --git a/addons/hr_recruitment/hr_recruitment_menu.xml b/addons/hr_recruitment/hr_recruitment_menu.xml index 9883e311fba..0bac3f8615a 100644 --- a/addons/hr_recruitment/hr_recruitment_menu.xml +++ b/addons/hr_recruitment/hr_recruitment_menu.xml @@ -6,7 +6,7 @@ Applicants hr.applicant - tree,form,graph,calendar + tree,form,graph,calendar,kanban {"search_default_user_id":uid, 'search_default_current': 1,"search_default_department_id": department_id} diff --git a/addons/hr_recruitment/hr_recruitment_view.xml b/addons/hr_recruitment/hr_recruitment_view.xml index 629b1c68012..290dedd2fb3 100644 --- a/addons/hr_recruitment/hr_recruitment_view.xml +++ b/addons/hr_recruitment/hr_recruitment_view.xml @@ -283,7 +283,86 @@ + + + Hr Applicants kanban + hr.applicant + kanban + + + + + + + + + + +
+
+
+ + + + + +
+ + + + + +
+
+
+ + + + +
+
+ +
+
+ + + (Not Good) + (On Average) + (Good) + (Very Good) + (Excellent) +
+
+ +
+
+ +
+
+
+ +
+
+
+
+
+
+
# ------------------------------------------------------ # HR Job # ------------------------------------------------------ From dc54be9bb925f50008907dc657a8a5e8bcb2fcbe Mon Sep 17 00:00:00 2001 From: "Amit Bhavsar (Open ERP)" Date: Fri, 16 Sep 2011 18:17:25 +0530 Subject: [PATCH 099/526] [kabanization],kanban view for the partner bzr revid: amb@tinyerp.com-20110916124725-1d07ph63gx8nv3u1 --- openerp/addons/base/res/res_partner.py | 4 ++ openerp/addons/base/res/res_partner_view.xml | 72 ++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/openerp/addons/base/res/res_partner.py b/openerp/addons/base/res/res_partner.py index 4b8f1279787..d88bf8463a4 100644 --- a/openerp/addons/base/res/res_partner.py +++ b/openerp/addons/base/res/res_partner.py @@ -131,12 +131,15 @@ class res_partner(osv.osv): 'customer': fields.boolean('Customer', help="Check this box if the partner is a customer."), 'supplier': fields.boolean('Supplier', help="Check this box if the partner is a supplier. If it's not checked, purchase people will not see it when encoding a purchase order."), 'city': fields.related('address', 'city', type='char', string='City'), + 'function': fields.related('address', 'function', type='char', string='function'), + 'subname': fields.related('address', 'name', type='char', string='Contact Name'), 'phone': fields.related('address', 'phone', type='char', string='Phone'), 'mobile': fields.related('address', 'mobile', type='char', string='Mobile'), 'country': fields.related('address', 'country_id', type='many2one', relation='res.country', string='Country'), 'employee': fields.boolean('Employee', help="Check this box if the partner is an Employee."), 'email': fields.related('address', 'email', type='char', size=240, string='E-mail'), 'company_id': fields.many2one('res.company', 'Company', select=1), + 'color': fields.integer('Color Index'), } def _default_category(self, cr, uid, context={}): @@ -150,6 +153,7 @@ class res_partner(osv.osv): 'address': [{'type': 'default'}], 'category_id': _default_category, 'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'res.partner', context=c), + 'color': 0, } def copy(self, cr, uid, id, default={}, context={}): diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml index 8b26023701d..8f7111bd738 100644 --- a/openerp/addons/base/res/res_partner_view.xml +++ b/openerp/addons/base/res/res_partner_view.xml @@ -353,12 +353,84 @@
+ + + + RES - PARTNER KANBAN + res.partner + kanban + + + + + + + + + + + + + + + + + +
+ Customers ir.actions.act_window res.partner form + kanban {"search_default_customer":1} A customer is an entity you do business with, like a company or an organization. A customer can have several contacts or addresses which are the people working for this company. You can use the history tab, to follow all transactions related to a customer: sales order, emails, opportunities, claims, etc. If you use the email gateway, the Outlook or the Thunderbird plugin, don't forget to register emails to each contact so that the gateway will automatically attach incoming emails to the right partner. From e5e20ba47cfb68fb23d33b7a911f7ca5cb92108a Mon Sep 17 00:00:00 2001 From: "Amit (OpenERP)" Date: Fri, 16 Sep 2011 18:43:02 +0530 Subject: [PATCH 100/526] [IMP]:improved bzr revid: apa@tinyerp.com-20110916131302-3jjj3ly3xy00f0so --- addons/project_issue/project_issue_view.xml | 93 +++++++++++---------- 1 file changed, 51 insertions(+), 42 deletions(-) diff --git a/addons/project_issue/project_issue_view.xml b/addons/project_issue/project_issue_view.xml index 9c10b958d80..605c26f232b 100644 --- a/addons/project_issue/project_issue_view.xml +++ b/addons/project_issue/project_issue_view.xml @@ -274,58 +274,67 @@ - - - - - - - + + + + + + +
- - - - - -
- - - -
- -
-
- -
-
- -
-
- -
-
- Version -
-
- -
-
+
+ + + + + +
+ + + + + +
+
+
+ + + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ -
+
+
From 0ee38e29ade5acc7fd9aa8955901c5fbc69c788f Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Fri, 16 Sep 2011 15:22:28 +0200 Subject: [PATCH 101/526] [IMP] convert form widgets to use a root class to find themselves back in the rendered dom, instead of a root id bzr revid: xmo@openerp.com-20110916132228-h2qszo33q6z289qf --- addons/web/static/src/js/view_form.js | 22 ++++++++-------- addons/web/static/src/xml/base.xml | 37 +++++++++++++-------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index e28936b850d..16a4ba908a7 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -82,7 +82,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# _.each(this.widgets, function(w) { w.start(); }); - this.$form_header = this.$element.find('#' + this.element_id + '_header'); + this.$form_header = this.$element.find('.oe_form_header'); this.$form_header.find('div.oe_form_pager button[data-pager-action]').click(function() { var action = $(this).data('pager-action'); self.on_pager_action(action); @@ -196,7 +196,7 @@ openerp.web.FormView = openerp.web.View.extend( /** @lends openerp.web.FormView# } }, do_update_pager: function(hide_index) { - var $pager = this.$element.find('#' + this.element_id + '_header div.oe_form_pager'); + var $pager = this.$form_header.find('div.oe_form_pager'); var index = hide_index ? '-' : this.dataset.index + 1; $pager.find('span.oe_pager_index').html(index); $pager.find('span.oe_pager_count').html(this.dataset.ids.length); @@ -619,6 +619,7 @@ openerp.web.form.compute_domain = function(expr, fields) { openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form.Widget# */{ template: 'Widget', + identifier_prefix: 'formview-widget-', /** * @constructs openerp.web.form.Widget * @extends openerp.web.Widget @@ -632,11 +633,13 @@ openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form. this.modifiers = JSON.parse(this.node.attrs.modifiers || '{}'); this.type = this.type || node.tag; this.element_name = this.element_name || this.type; - this.element_id = [this.view.element_id, this.element_name, this.view.widgets_counter++].join("_"); + this.element_class = [ + 'formview', this.view.view_id, this.element_name, + this.view.widgets_counter++].join("_"); - this._super(view, this.element_id); + this._super(view); - this.view.widgets[this.element_id] = this; + this.view.widgets[this.element_class] = this; this.children = node.children; this.colspan = parseInt(node.attrs.colspan || 1, 10); this.decrease_max_width = 0; @@ -649,7 +652,7 @@ openerp.web.form.Widget = openerp.web.Widget.extend(/** @lends openerp.web.form. this.width = this.node.attrs.width; }, start: function() { - this.$element = $('#' + this.element_id); + this.$element = this.view.$element.find('.' + this.element_class); }, stop: function() { if (this.$element) { @@ -896,7 +899,7 @@ openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({ var self = this; this.$element.find("label").dblclick(function() { var widget = self['for'] || self; - console.log(widget.element_id , widget); + console.log(widget.element_class , widget); window.w = widget; }); } @@ -1932,9 +1935,8 @@ openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ }, validate: function() { this.invalid = false; - var self = this; - var view = self.viewmanager.views[self.viewmanager.active_view].controller; - if (self.viewmanager.active_view === "form") { + var view = this.viewmanager.views[this.viewmanager.active_view].controller; + if (this.viewmanager.active_view === "form") { for (var f in view.fields) { f = view.fields[f]; if (!f.is_valid()) { diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index c2b6ca0bdf2..daf85c43ee7 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -649,7 +649,7 @@ -
+ + + + + + Customers From ad60a40879c05116fd9a4998e49fa1939569e21d Mon Sep 17 00:00:00 2001 From: "Amit (OpenERP)" Date: Mon, 19 Sep 2011 14:47:01 +0530 Subject: [PATCH 108/526] [IMP]:improved bzr revid: apa@tinyerp.com-20110919091701-t4jzmqm4245knacb --- openerp/addons/base/res/res_partner_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openerp/addons/base/res/res_partner_view.xml b/openerp/addons/base/res/res_partner_view.xml index 221d5c34119..efac1a6c603 100644 --- a/openerp/addons/base/res/res_partner_view.xml +++ b/openerp/addons/base/res/res_partner_view.xml @@ -390,7 +390,7 @@
- +
From c8c52f6257fc2157ef979bf89435cb289dfb6644 Mon Sep 17 00:00:00 2001 From: "Amit (OpenERP)" Date: Mon, 19 Sep 2011 15:03:43 +0530 Subject: [PATCH 109/526] improved bzr revid: apa@tinyerp.com-20110919093343-nykuuvw7d8d0o1xn --- addons/hr/hr_view.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml index d71f78a3053..4b5d64746a9 100644 --- a/addons/hr/hr_view.xml +++ b/addons/hr/hr_view.xml @@ -157,8 +157,8 @@
-
- + +
From b4e8ac180c6811587e036c62a6c0603fadd2277c Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 19 Sep 2011 12:15:17 +0200 Subject: [PATCH 110/526] [REM] redundant initializers in formview widgets: move most formview widget templates to the class-level bzr revid: xmo@openerp.com-20110919101517-40303tgi58cfshtk --- addons/web/static/src/js/view_form.js | 58 ++++++++------------------- 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index b1ce7542210..23a0f19f121 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -816,9 +816,9 @@ openerp.web.form.WidgetNotebookPage = openerp.web.form.WidgetFrame.extend({ }); openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({ + template: 'WidgetSeparator', init: function(view, node) { this._super(view, node); - this.template = "WidgetSeparator"; this.orientation = node.attrs.orientation || 'horizontal'; if (this.orientation === 'vertical') { this.width = '1'; @@ -828,9 +828,9 @@ openerp.web.form.WidgetSeparator = openerp.web.form.Widget.extend({ }); openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ + template: 'WidgetButton', init: function(view, node) { this._super(view, node); - this.template = "WidgetButton"; if (this.string) { // We don't have button key bindings in the webclient this.string = this.string.replace(/_/g, ''); @@ -881,6 +881,7 @@ openerp.web.form.WidgetButton = openerp.web.form.Widget.extend({ }); openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({ + template: 'WidgetLabel', init: function(view, node) { this.element_name = 'label_' + node.attrs.name; @@ -891,7 +892,6 @@ openerp.web.form.WidgetLabel = openerp.web.form.Widget.extend({ this.template = "WidgetParagraph"; this.colspan = parseInt(this.node.attrs.colspan || 1, 10); } else { - this.template = "WidgetLabel"; this.colspan = 1; this.width = '1%'; this.decrease_max_width = 1; @@ -1049,10 +1049,7 @@ openerp.web.form.Field = openerp.web.form.Widget.extend(/** @lends openerp.web.f }); openerp.web.form.FieldChar = openerp.web.form.Field.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldChar"; - }, + template: 'FieldChar', start: function() { this._super.apply(this, arguments); this.$element.find('input').change(this.on_ui_change); @@ -1085,10 +1082,7 @@ openerp.web.form.FieldChar = openerp.web.form.Field.extend({ }); openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldEmail"; - }, + template: 'FieldEmail', start: function() { this._super.apply(this, arguments); this.$element.find('button').click(this.on_button_clicked); @@ -1107,10 +1101,7 @@ openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({ }); openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldUrl"; - }, + template: 'FieldUrl', start: function() { this._super.apply(this, arguments); this.$element.find('button').click(this.on_button_clicked); @@ -1136,9 +1127,9 @@ openerp.web.form.FieldFloat = openerp.web.form.FieldChar.extend({ }); openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ + template: 'FieldDate', init: function(view, node) { this._super(view, node); - this.template = "FieldDate"; this.jqueryui_object = 'datetimepicker'; }, start: function() { @@ -1229,10 +1220,7 @@ openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({ }); openerp.web.form.FieldText = openerp.web.form.Field.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldText"; - }, + template: 'FieldText', start: function() { this._super.apply(this, arguments); this.$element.find('textarea').change(this.on_ui_change); @@ -1265,10 +1253,7 @@ openerp.web.form.FieldText = openerp.web.form.Field.extend({ }); openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldBoolean"; - }, + template: 'FieldBoolean', start: function() { var self = this; this._super.apply(this, arguments); @@ -1299,10 +1284,7 @@ openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({ }); openerp.web.form.FieldProgressBar = openerp.web.form.Field.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldProgressBar"; - }, + template: 'FieldProgressBar', start: function() { this._super.apply(this, arguments); this.$element.find('div').progressbar({ @@ -1325,10 +1307,10 @@ openerp.web.form.FieldTextXml = openerp.web.form.Field.extend({ }); openerp.web.form.FieldSelection = openerp.web.form.Field.extend({ + template: 'FieldSelection', init: function(view, node) { var self = this; this._super(view, node); - this.template = "FieldSelection"; this.values = this.field.selection; _.each(this.values, function(v, i) { if (v[0] === false && v[1] === '') { @@ -1435,9 +1417,9 @@ openerp.web.form.dialog = function(content, options) { }; openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ + template: 'FieldMany2One', init: function(view, node) { this._super(view, node); - this.template = "FieldMany2One"; this.limit = 7; this.value = null; this.cm_id = _.uniqueId('m2o_cm_'); @@ -1762,10 +1744,10 @@ var commands = { } }; openerp.web.form.FieldOne2Many = openerp.web.form.Field.extend({ + template: 'FieldOne2Many', multi_selection: false, init: function(view, node) { this._super(view, node); - this.template = "FieldOne2Many"; this.is_started = $.Deferred(); this.form_last_update = $.Deferred(); this.disable_utility_classes = true; @@ -2020,10 +2002,10 @@ openerp.web.form.One2ManyListView = openerp.web.ListView.extend({ }); openerp.web.form.FieldMany2Many = openerp.web.form.Field.extend({ + template: 'FieldMany2Many', multi_selection: false, init: function(view, node) { this._super(view, node); - this.template = "FieldMany2Many"; this.list_id = _.uniqueId("many2many"); this.is_started = $.Deferred(); }, @@ -2365,9 +2347,9 @@ openerp.web.form.FormOpenDataset = openerp.web.ReadOnlyDataSetSearch.extend({ }); openerp.web.form.FieldReference = openerp.web.form.Field.extend({ + template: 'FieldReference', init: function(view, node) { this._super(view, node); - this.template = "FieldReference"; this.fields_view = { fields: { selection: { @@ -2502,10 +2484,7 @@ openerp.web.form.FieldBinary = openerp.web.form.Field.extend({ }); openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldBinaryFile"; - }, + template: 'FieldBinaryFile', set_value: function(value) { this._super.apply(this, arguments); var show_value = (value != null && value !== false) ? value : ''; @@ -2533,10 +2512,7 @@ openerp.web.form.FieldBinaryFile = openerp.web.form.FieldBinary.extend({ }); openerp.web.form.FieldBinaryImage = openerp.web.form.FieldBinary.extend({ - init: function(view, node) { - this._super(view, node); - this.template = "FieldBinaryImage"; - }, + template: 'FieldBinaryImage', start: function() { this._super.apply(this, arguments); this.$image = this.$element.find('img.oe-binary-image'); From 04f843a986181a74d5512c9916f153faf8fb5080 Mon Sep 17 00:00:00 2001 From: "Amit (OpenERP)" Date: Mon, 19 Sep 2011 15:54:16 +0530 Subject: [PATCH 111/526] set images bzr revid: apa@tinyerp.com-20110919102416-x0vk0x60llm1qjjb --- addons/hr/hr_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml index 4b5d64746a9..7b7b25a1557 100644 --- a/addons/hr/hr_view.xml +++ b/addons/hr/hr_view.xml @@ -158,7 +158,7 @@ -
- +
From 2ad93ea0cd777538670dcab06c995586b14dbfc7 Mon Sep 17 00:00:00 2001 From: "Amit (OpenERP)" Date: Mon, 19 Sep 2011 16:17:42 +0530 Subject: [PATCH 112/526] [IMP] bzr revid: apa@tinyerp.com-20110919104742-f52pkqpluov2aob6 --- addons/hr/hr_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/hr/hr_view.xml b/addons/hr/hr_view.xml index 7b7b25a1557..b5192b63d44 100644 --- a/addons/hr/hr_view.xml +++ b/addons/hr/hr_view.xml @@ -158,7 +158,7 @@
- +
From ea6299dae6e1a5341e6bec8d164c642492af0d12 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 19 Sep 2011 13:57:46 +0200 Subject: [PATCH 113/526] [FIX] form.FieldOne2Many, nesting itself inside itself, breaking the self-matcher bzr revid: xmo@openerp.com-20110919115746-16jz8hajcdgl010f --- addons/web/static/src/xml/base.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 498a8bb1dd2..c88b0116a27 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -855,8 +855,6 @@ -
-
From 566f9a43e8fce80cb53386aa4a003f4c6af21113 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 19 Sep 2011 14:24:16 +0200 Subject: [PATCH 114/526] [ADD] readonly branch to a bunch of form widgets bzr revid: xmo@openerp.com-20110919122416-h773tyz1kogazyy0 --- addons/web/static/src/js/view_form.js | 70 +++++++++++++++++++---- addons/web/static/src/xml/base.xml | 80 +++++++++++++++++++++------ 2 files changed, 122 insertions(+), 28 deletions(-) diff --git a/addons/web/static/src/js/view_form.js b/addons/web/static/src/js/view_form.js index 23a0f19f121..1a14cf1745d 100644 --- a/addons/web/static/src/js/view_form.js +++ b/addons/web/static/src/js/view_form.js @@ -1057,7 +1057,12 @@ openerp.web.form.FieldChar = openerp.web.form.Field.extend({ set_value: function(value) { this._super.apply(this, arguments); var show_value = openerp.web.format_value(value, this, ''); - this.$element.find('input').val(show_value); + if (this.view.readonly) { + this.$element.find('div').text(show_value); + } else { + this.$element.find('input').val(show_value); + } + return show_value; }, update_dom: function() { this._super.apply(this, arguments); @@ -1095,8 +1100,12 @@ openerp.web.form.FieldEmail = openerp.web.form.FieldChar.extend({ } }, set_value: function(value) { - this._super.apply(this, arguments); - this.$element.find('a').attr('href', 'mailto:' + this.$element.find('input').val()); + var displayed = this._super.apply(this, arguments); + if (this.view.readonly) { + this.$element.find('a') + .attr('href', 'mailto:' + displayed) + .text(displayed); + } } }); @@ -1112,6 +1121,14 @@ openerp.web.form.FieldUrl = openerp.web.form.FieldChar.extend({ } else { window.open(this.value); } + }, + set_value: function(value) { + var displayed = this._super.apply(this, arguments); + if (this.view.readonly) { + this.$element.find('a') + .attr('href', displayed) + .text(displayed); + } } }); @@ -1130,11 +1147,14 @@ openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ template: 'FieldDate', init: function(view, node) { this._super(view, node); - this.jqueryui_object = 'datetimepicker'; + if (!this.view.readonly) { + this.jqueryui_object = 'datetimepicker'; + } }, start: function() { var self = this; this._super.apply(this, arguments); + if (this.view.readonly) { return; } this.$element.find('input').change(this.on_ui_change); this.picker({ onSelect: this.on_picker_select, @@ -1164,7 +1184,11 @@ openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ set_value: function(value) { value = this.parse(value); this._super(value); - this.$element.find('input').val(value ? this.format_client(value) : ''); + if (this.view.readonly) { + this.$element.find('div').text(value ? this.format_client(value) : ''); + } else { + this.$element.find('input').val(value ? this.format_client(value) : ''); + } }, get_value: function() { return this.format(this.value); @@ -1211,7 +1235,9 @@ openerp.web.form.FieldDatetime = openerp.web.form.Field.extend({ openerp.web.form.FieldDate = openerp.web.form.FieldDatetime.extend({ init: function(view, node) { this._super(view, node); - this.jqueryui_object = 'datepicker'; + if (!this.view.readonly) { + this.jqueryui_object = 'datepicker'; + } }, on_picker_select: function(text, instance) { this._super(text, instance); @@ -1228,7 +1254,11 @@ openerp.web.form.FieldText = openerp.web.form.Field.extend({ set_value: function(value) { this._super.apply(this, arguments); var show_value = openerp.web.format_value(value, this, ''); - this.$element.find('textarea').val(show_value); + if (this.view.readonly) { + this.$element.find('div').text(show_value); + } else { + this.$element.find('textarea').val(show_value); + } }, update_dom: function() { this._super.apply(this, arguments); @@ -1265,7 +1295,11 @@ openerp.web.form.FieldBoolean = openerp.web.form.Field.extend({ }, set_value: function(value) { this._super.apply(this, arguments); - this.$element.find('input')[0].checked = value; + if (this.view.readonly) { + this.$element.find('span').text(value ? '✔' : '✘'); + } else { + this.$element.find('input')[0].checked = value; + } }, set_value_from_ui: function() { this.value = this.$element.find('input').is(':checked'); @@ -1347,11 +1381,17 @@ openerp.web.form.FieldSelection = openerp.web.form.Field.extend({ value = value === null ? false : value; value = value instanceof Array ? value[0] : value; this._super(value); - var index = 0; - for (var i = 0, ii = this.values.length; i < ii; i++) { - if (this.values[i][0] === value) index = i; + if (this.view.readonly) { + var option = _(this.values) + .detect(function (record) { return record[0] === value; }); + this.$element.find('div').text(option ? option[1] : this.values[0][1]); + } else { + var index = 0; + for (var i = 0, ii = this.values.length; i < ii; i++) { + if (this.values[i][0] === value) index = i; + } + this.$element.find('select')[0].selectedIndex = index; } - this.$element.find('select')[0].selectedIndex = index; }, set_value_from_ui: function() { this.value = this.values[this.$element.find('select')[0].selectedIndex][0]; @@ -1362,6 +1402,7 @@ openerp.web.form.FieldSelection = openerp.web.form.Field.extend({ this.$element.find('select').attr('disabled', this.readonly); }, validate: function() { + if (this.view.readonly) { return; } var value = this.values[this.$element.find('select')[0].selectedIndex]; this.invalid = !(value && !(this.required && value[0] === false)); }, @@ -1428,6 +1469,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ }, start: function() { this._super(); + if (this.view.readonly) { return; } var self = this; this.$input = this.$element.find("input"); this.$drop_down = this.$element.find(".oe-m2o-drop-down-button"); @@ -1647,6 +1689,10 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({ self.update_dom(); self.on_value_changed(); var real_set_value = function(rval) { + if (self.view.readonly) { + self.$element.find('div').text(rval ? rval[1] : ''); + return; + } self.tmp_value = undefined; self.value = rval; self.original_value = undefined; diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index c88b0116a27..45812e9fd08 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -730,15 +730,23 @@
- - - + + +

+ + +
+ + + + + @@ -764,7 +772,15 @@

+
+ +
- + + # + @@ -790,9 +807,10 @@ -
- + + # + @@ -801,7 +819,14 @@
+
@@ -833,8 +833,8 @@ -
- +
+ @@ -886,8 +887,8 @@ + t-att-id="widget.element_id" + t-attf-class="field_#{widget.type}"/>
@@ -902,7 +903,7 @@ t-att-border="widget.readonly ? 0 : 1" t-att-id="widget.element_id + '_field'" t-att-name="widget.name" - t-attf-class="field_#{widget.type} #{widget.element_class}" + t-attf-class="field_#{widget.type}" t-att-width="widget.node.attrs.img_width || widget.node.attrs.width" t-att-height="widget.node.attrs.img_height || widget.node.attrs.height" /> @@ -950,7 +951,7 @@
@@ -995,7 +996,6 @@