From 9e8d29c986358c879be61b24a7409d78ca274479 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 1 Jul 2015 12:48:16 +0200 Subject: [PATCH 01/10] [FIX] project_timesheet: analyis view must include lines without timesheet While working on project task it is possible to record "task work" that will automatically create timesheet lines. These are not by default included into any timesheet, but they need to appear in reporting nonetheless. These lines disappeared from the analysis view after the performance improvement of rev. fe31451899ea84fbffe1bc17d7f41ec10a8d786a which introduced a JOIN with the `totals` CTE table - and should have been LEFT JOIN. --- addons/hr_timesheet_sheet/report/hr_timesheet_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/hr_timesheet_sheet/report/hr_timesheet_report.py b/addons/hr_timesheet_sheet/report/hr_timesheet_report.py index d3e0ea59957..052c9fac080 100644 --- a/addons/hr_timesheet_sheet/report/hr_timesheet_report.py +++ b/addons/hr_timesheet_sheet/report/hr_timesheet_report.py @@ -73,7 +73,7 @@ class hr_timesheet_report(osv.osv): htss.state""" def _from(self): - return super(hr_timesheet_report, self)._from() + "left join hr_timesheet_sheet_sheet as htss ON (hat.sheet_id=htss.id) join totals as t on (t.sheet_id = hat.sheet_id and t.date = aal.date)" + return super(hr_timesheet_report, self)._from() + "left join hr_timesheet_sheet_sheet as htss ON (hat.sheet_id=htss.id) left join totals as t on (t.sheet_id = hat.sheet_id and t.date = aal.date)" def _group_by(self): return super(hr_timesheet_report, self)._group_by() + """, From 09397384795f440b5924d9b325988e9cc6eb4d09 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 30 Jun 2015 16:23:16 +0200 Subject: [PATCH 02/10] [IMP] fields: speedup of `convert_to_cache` for x2many fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The algorithm's complexity changed from O(n²) to O(n). --- openerp/fields.py | 33 ++++++++++++++++----------------- openerp/tools/misc.py | 13 ++++++++++++- 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/openerp/fields.py b/openerp/fields.py index 9d3fb496044..e9dbfaa9104 100644 --- a/openerp/fields.py +++ b/openerp/fields.py @@ -30,7 +30,7 @@ import logging import pytz import xmlrpclib -from openerp.tools import float_round, frozendict, html_sanitize, ustr +from openerp.tools import float_round, frozendict, html_sanitize, ustr, OrderedSet from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT @@ -1606,34 +1606,33 @@ class _RelationalMulti(_Relational): return value.with_env(record.env) elif isinstance(value, list): # value is a list of record ids or commands - if not record.id: - record = record.browse() # new record has no value - result = record[self.name] - # modify result with the commands; - # beware to not introduce duplicates in result + comodel = record.env[self.comodel_name] + ids = OrderedSet(record[self.name].ids) + # modify ids with the commands for command in value: if isinstance(command, (tuple, list)): if command[0] == 0: - result += result.new(command[2]) + ids.add(comodel.new(command[2]).id) elif command[0] == 1: - result.browse(command[1]).update(command[2]) - result += result.browse(command[1]) - result + comodel.browse(command[1]).update(command[2]) + ids.add(command[1]) elif command[0] == 2: # note: the record will be deleted by write() - result -= result.browse(command[1]) + ids.discard(command[1]) elif command[0] == 3: - result -= result.browse(command[1]) + ids.discard(command[1]) elif command[0] == 4: - result += result.browse(command[1]) - result + ids.add(command[1]) elif command[0] == 5: - result = result.browse() + ids.clear() elif command[0] == 6: - result = result.browse(command[2]) + ids = OrderedSet(command[2]) elif isinstance(command, dict): - result += result.new(command) + ids.add(comodel.new(command).id) else: - result += result.browse(command) - result - return result + ids.add(command) + # return result as a recordset + return comodel.browse(list(ids)) elif not value: return self.null(record.env) raise ValueError("Wrong value for %s: %s" % (self, value)) diff --git a/openerp/tools/misc.py b/openerp/tools/misc.py index 36b12893bc4..35f570fb7b6 100644 --- a/openerp/tools/misc.py +++ b/openerp/tools/misc.py @@ -37,7 +37,7 @@ import threading import time import werkzeug.utils import zipfile -from collections import defaultdict, Mapping +from collections import defaultdict, Mapping, OrderedDict from datetime import datetime from itertools import islice, izip, groupby from lxml import etree @@ -1271,6 +1271,17 @@ class frozendict(dict): def update(self, *args, **kwargs): raise NotImplementedError("'update' not supported on frozendict") +class OrderedSet(OrderedDict): + """ A simple collection that remembers the elements insertion order. """ + def __init__(self, seq=()): + super(OrderedSet, self).__init__([(x, None) for x in seq]) + + def add(self, elem): + self[elem] = None + + def discard(self, elem): + self.pop(elem, None) + @contextmanager def ignore(*exc): try: From 78568b59ab03988d2c8156168cab900750d39d2f Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Wed, 1 Jul 2015 14:33:21 +0200 Subject: [PATCH 03/10] [FIX] hr_timesheet_invoice: invoice tasks without factor customer_name In Project > Invoicing > Invoice Tasks, When selecting several lines to invoice, with a factor (`to_invoice') without `customer_name` opw-643794 --- addons/hr_timesheet_invoice/hr_timesheet_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py index 34cea3c92cf..3e65338932f 100644 --- a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py +++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py @@ -199,7 +199,7 @@ class account_analytic_line(osv.osv): unit_price = total_price*-1.0 / total_qty factor = self.pool['hr_timesheet_invoice.factor'].browse(cr, uid, factor_id, context=uom_context) - factor_name = factor.customer_name + factor_name = factor.customer_name or '' curr_invoice_line = { 'price_unit': unit_price, 'quantity': total_qty, From 49f2514a9d71f13fcc718fe326ba2e8e76ef0e28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Du=C5=A1an=20Laznik?= Date: Wed, 1 Jul 2015 15:23:39 +0200 Subject: [PATCH 04/10] [CLA] mentis Received by email on June 30th 2015 opw-643688 --- doc/cla/corporate/mentis.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 doc/cla/corporate/mentis.md diff --git a/doc/cla/corporate/mentis.md b/doc/cla/corporate/mentis.md new file mode 100644 index 00000000000..d8d9562f74b --- /dev/null +++ b/doc/cla/corporate/mentis.md @@ -0,0 +1,17 @@ +Slovenia, 2015-06-29 + +Mentis d.o.o. agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Dušan Laznik dusan.laznik@mentis.si https://github.com/laznikd + +List of contributors: + +Dušan Laznik dusan.laznik@mentis.si https://github.com/laznikd +Aleksander Dirntiš sandi.dirntis@mentis.si https://github.com/adirntis +Matjaž Kalič matjaz.kalic@mentis.si https://github.com/kalicm From bdf980e4b462d674b4e7bf647f526beb32a2ad07 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Wed, 1 Jul 2015 15:55:28 +0200 Subject: [PATCH 05/10] [FIX] stock_account: stock valuation report without result This is related to rev. 55b7f15ee2427e4c99edf05a5a61b069a5395bab Prevent crash when there is no line to display in the stock valuation report Reporting > Warehouse > Stock Valuation. Like when there is not yet any data, or the filter gives no result opw-643748 --- addons/stock_account/wizard/stock_valuation_history.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/stock_account/wizard/stock_valuation_history.py b/addons/stock_account/wizard/stock_valuation_history.py index 66b670c2a75..13e35c7b91d 100644 --- a/addons/stock_account/wizard/stock_valuation_history.py +++ b/addons/stock_account/wizard/stock_valuation_history.py @@ -56,8 +56,10 @@ class stock_history(osv.osv): for product_id in ids: line_ids.add(product_id) line_ids = list(line_ids) - cr.execute('SELECT id, product_id, price_unit_on_quant, company_id, quantity FROM stock_history WHERE id in %s', (tuple(line_ids),)) - lines_rec = cr.dictfetchall() + lines_rec = {} + if line_ids: + cr.execute('SELECT id, product_id, price_unit_on_quant, company_id, quantity FROM stock_history WHERE id in %s', (tuple(line_ids),)) + lines_rec = cr.dictfetchall() lines_dict = dict((line['id'], line) for line in lines_rec) product_ids = list(set(line_rec['product_id'] for line_rec in lines_rec)) products_rec = self.pool['product.product'].read(cr, uid, product_ids, ['cost_method', 'product_tmpl_id'], context=context) From 988baeb533a064ec393bcd159123097137ee31ea Mon Sep 17 00:00:00 2001 From: Alexandre Fayolle Date: Wed, 20 May 2015 11:23:58 +0200 Subject: [PATCH 06/10] [FIX] setup: use pypi version of PyChart PyChart 1.39 is available on pypi.python.org, use it in dependency specs instead of the direct link to unreliable download.gna.org Closes #6781 --- requirements.txt | 2 +- setup.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 047bc19d8f2..53682ca9993 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ Jinja2==2.7.3 Mako==1.0.0 MarkupSafe==0.23 Pillow==2.5.1 -http://download.gna.org/pychart/PyChart-1.39.tar.gz#egg=PyChart +Python-Chart==1.39 PyYAML==3.11 Werkzeug==0.9.6 argparse==1.2.1 diff --git a/setup.py b/setup.py index c7bde8e0cf7..ee607be5e00 100644 --- a/setup.py +++ b/setup.py @@ -87,7 +87,7 @@ def py2exe_options(): 'PIL', 'poplib', 'psutil', - 'pychart', + 'python-chart', 'pydot', 'pyparsing', 'pyPdf', @@ -128,7 +128,6 @@ setup( packages=find_packages(), package_dir={'%s' % lib_name: 'openerp'}, include_package_data=True, - dependency_links=['http://download.gna.org/pychart/'], install_requires=[ 'babel >= 1.0', 'decorator', @@ -144,7 +143,7 @@ setup( 'psutil', # windows binary code.google.com/p/psutil/downloads/list 'psycogreen', 'psycopg2 >= 2.2', - 'pychart', # not on pypi, use: pip install http://download.gna.org/pychart/PyChart-1.39.tar.gz + 'python-chart', 'pydot', 'pyparsing < 2', 'pypdf', From 2758aaa6f87311f01c0baf82ad9c1f0ef4535ab5 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Wed, 1 Jul 2015 12:36:17 +0200 Subject: [PATCH 07/10] [IMP] models: sanitize error messages in import Revert 83282f2d for a cleaner sanitizing earlier in the generation of the error message. If the import is failing, the error message contains the value that is problematic. Escape this value in case it contains '%' --- openerp/addons/base/ir/ir_fields.py | 91 ++++++++++++------- .../addons/test_impex/tests/test_import.py | 13 ++- openerp/models.py | 7 +- 3 files changed, 73 insertions(+), 38 deletions(-) diff --git a/openerp/addons/base/ir/ir_fields.py b/openerp/addons/base/ir/ir_fields.py index 77c0c830cba..69dbe144f87 100644 --- a/openerp/addons/base/ir/ir_fields.py +++ b/openerp/addons/base/ir/ir_fields.py @@ -37,6 +37,19 @@ class ConversionNotFound(ValueError): pass class ir_fields_converter(models.Model): _name = 'ir.fields.converter' + @api.model + def _format_import_error(self, error_type, error_msg, error_params=(), error_args=None): + # sanitize error params for later formatting by the import system + sanitize = lambda p: p.replace('%', '%%') if isinstance(p, basestring) else p + if error_params: + if isinstance(error_params, basestring): + error_params = sanitize(error_params) + elif isinstance(error_params, dict): + error_params = dict((k, sanitize(v)) for k, v in error_params.iteritems()) + elif isinstance(error_params, tuple): + error_params = tuple(map(sanitize, error_params)) + return error_type(error_msg % error_params, error_args) + @api.model def for_model(self, model, fromtype=str): """ Returns a converter object for the model. A converter is a @@ -148,29 +161,34 @@ class ir_fields_converter(models.Model): if value.lower() in falses: return False, [] - return True, [ImportWarning( - _(u"Unknown value '%s' for boolean field '%%(field)s', assuming '%s'") - % (value, yes), { - 'moreinfo': _(u"Use '1' for yes and '0' for no") - })] + return True, [self._format_import_error( + ImportWarning, + _(u"Unknown value '%s' for boolean field '%%(field)s', assuming '%s'"), + (value, yes), + {'moreinfo': _(u"Use '1' for yes and '0' for no")} + )] @api.model def _str_to_integer(self, model, field, value): try: return int(value), [] except ValueError: - raise ValueError( - _(u"'%s' does not seem to be an integer for field '%%(field)s'") - % value) + raise self._format_import_error( + ValueError, + _(u"'%s' does not seem to be an integer for field '%%(field)s'"), + value + ) @api.model def _str_to_float(self, model, field, value): try: return float(value), [] except ValueError: - raise ValueError( - _(u"'%s' does not seem to be a number for field '%%(field)s'") - % value) + raise self._format_import_error( + ValueError, + _(u"'%s' does not seem to be a number for field '%%(field)s'"), + value + ) @api.model def _str_id(self, model, field, value): @@ -184,10 +202,12 @@ class ir_fields_converter(models.Model): time.strptime(value, DEFAULT_SERVER_DATE_FORMAT) return value, [] except ValueError: - raise ValueError( - _(u"'%s' does not seem to be a valid date for field '%%(field)s'") % value, { - 'moreinfo': _(u"Use the format '%s'") % u"2012-12-31" - }) + raise self._format_import_error( + ValueError, + _(u"'%s' does not seem to be a valid date for field '%%(field)s'"), + value, + {'moreinfo': _(u"Use the format '%s'") % u"2012-12-31"} + ) @api.model def _input_tz(self): @@ -215,10 +235,12 @@ class ir_fields_converter(models.Model): parsed_value = datetime.datetime.strptime( value, DEFAULT_SERVER_DATETIME_FORMAT) except ValueError: - raise ValueError( - _(u"'%s' does not seem to be a valid datetime for field '%%(field)s'") % value, { - 'moreinfo': _(u"Use the format '%s'") % u"2012-12-31 23:59:59" - }) + raise self._format_import_error( + ValueError, + _(u"'%s' does not seem to be a valid datetime for field '%%(field)s'"), + value, + {'moreinfo': _(u"Use the format '%s'") % u"2012-12-31 23:59:59"} + ) input_tz = self._input_tz()# Apply input tz to the parsed naive datetime dt = input_tz.localize(parsed_value, is_dst=False) @@ -251,12 +273,12 @@ class ir_fields_converter(models.Model): if value == unicode(item) or value in labels: return item, [] - raise ValueError( - _(u"Value '%s' not found in selection field '%%(field)s'") % ( - value), { - 'moreinfo': [_label or unicode(item) for item, _label in selection - if _label or item] - }) + raise self._format_import_error( + ValueError, + _(u"Value '%s' not found in selection field '%%(field)s'"), + value, + {'moreinfo': [_label or unicode(item) for item, _label in selection if _label or item]} + ) @api.model def db_id_for(self, model, field, subfield, value): @@ -298,8 +320,10 @@ class ir_fields_converter(models.Model): id = tentative_id except psycopg2.DataError: # type error - raise ValueError( - _(u"Invalid database id '%s' for the field '%%(field)s'") % value, + raise self._format_import_error( + ValueError, + _(u"Invalid database id '%s' for the field '%%(field)s'"), + value, {'moreinfo': action}) elif subfield == 'id': field_type = _(u"external id") @@ -321,12 +345,17 @@ class ir_fields_converter(models.Model): % (len(ids)))) id, _name = ids[0] else: - raise Exception(_(u"Unknown sub-field '%s'") % subfield) + raise self._format_import_error( + Exception, + _(u"Unknown sub-field '%s'"), + subfield + ) if id is None: - raise ValueError( - _(u"No matching record found for %(field_type)s '%(value)s' in field '%%(field)s'") - % {'field_type': field_type, 'value': value}, + raise self._format_import_error( + ValueError, + _(u"No matching record found for %(field_type)s '%(value)s' in field '%%(field)s'"), + {'field_type': field_type, 'value': value}, {'moreinfo': action}) return id, field_type, warnings diff --git a/openerp/addons/test_impex/tests/test_import.py b/openerp/addons/test_impex/tests/test_import.py index 11bacf6cce1..eab986fb29a 100644 --- a/openerp/addons/test_impex/tests/test_import.py +++ b/openerp/addons/test_impex/tests/test_import.py @@ -111,6 +111,12 @@ class test_ids_stuff(ImporterCase): self.import_(['id', 'value'], [['somexmlid', '1234567']]) self.assertEqual([1234567], values(self.read())) + def test_wrong_format(self): + self.assertEqual( + self.import_(['value'], [['50%']]), + error(1, u"'50%' does not seem to be an integer for field 'unknown'")) + + class test_boolean_field(ImporterCase): model_name = 'export.boolean' @@ -163,10 +169,13 @@ class test_boolean_field(ImporterCase): # Problem: OpenOffice (and probably excel) output localized booleans ['VRAI'], [u'OFF'], + [u'是的'], + ['!&%#${}'], + ['%(field)s'], ]), - ok(8)) + ok(11)) self.assertEqual( - [True] * 8, + [True] * 11, values(self.read())) class test_integer_field(ImporterCase): diff --git a/openerp/models.py b/openerp/models.py index 83a212096ac..c72bdd1a0ea 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -1199,11 +1199,8 @@ class BaseModel(object): type = 'warning' if isinstance(exception, Warning) else 'error' # logs the logical (not human-readable) field name for automated # processing of response, but injects human readable in message - # Before applying string substitution, we need to escape all '%' - # characters that are not part of substitutable reference. - regex = '%%(?!\((%s)\)s)' % '|'.join(base.keys()) - message = unicode(re.sub(regex, '%%', exception.args[0])) % base - record = dict(base, type=type, field=field, message=message) + record = dict(base, type=type, field=field, + message=unicode(exception.args[0]) % base) if len(exception.args) > 1 and exception.args[1]: record.update(exception.args[1]) log(record) From d0735fc55713117e8470c1036a9fe9dd947ea8be Mon Sep 17 00:00:00 2001 From: "Pedro M. Baeza" Date: Tue, 19 May 2015 22:16:01 +0200 Subject: [PATCH 08/10] [FIX] procurement_jit_stock: don't lose autocommit parameter run method in procurement as the positional parameter autocommit that was not kept in procurement_jit_stock which may introduce issues in override --- addons/procurement_jit_stock/procurement_jit_stock.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addons/procurement_jit_stock/procurement_jit_stock.py b/addons/procurement_jit_stock/procurement_jit_stock.py index 2f4de3e5754..5353b3edc0d 100644 --- a/addons/procurement_jit_stock/procurement_jit_stock.py +++ b/addons/procurement_jit_stock/procurement_jit_stock.py @@ -25,14 +25,14 @@ from openerp.osv import osv class procurement_order(osv.osv): _inherit = "procurement.order" - def run(self, cr, uid, ids, context=None): + def run(self, cr, uid, ids, autocommit=False, context=None): context = dict(context or {}, procurement_autorun_defer=True) - res = super(procurement_order, self).run(cr, uid, ids, context=context) + res = super(procurement_order, self).run(cr, uid, ids, autocommit=autocommit, context=context) procurement_ids = self.search(cr, uid, [('move_dest_id.procurement_id', 'in', ids)], order='id', context=context) if procurement_ids: - return self.run(cr, uid, procurement_ids, context=context) + return self.run(cr, uid, procurement_ids, autocommit=autocommit, context=context) return res class stock_move(osv.osv): From e3bd2a591491c48e805b66d5ee9f898bab4b5432 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 1 Jul 2015 17:49:56 +0200 Subject: [PATCH 09/10] [FIX] web_calendar, website: avoid rendering errors with special characters --- addons/web_calendar/static/src/js/web_calendar.js | 6 +++--- addons/website/static/src/js/website.snippets.animation.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/addons/web_calendar/static/src/js/web_calendar.js b/addons/web_calendar/static/src/js/web_calendar.js index f9d108b90f7..87554e52e1d 100644 --- a/addons/web_calendar/static/src/js/web_calendar.js +++ b/addons/web_calendar/static/src/js/web_calendar.js @@ -544,7 +544,7 @@ openerp.web_calendar = function(instance) { attendee_showed += 1; if (attendee_showed<= MAX_ATTENDEES) { if (self.avatar_model !== null) { - the_title_avatar += ''; } else { @@ -552,12 +552,12 @@ openerp.web_calendar = function(instance) { tempColor = (self.all_filters[the_attendee_people] !== undefined) ? self.all_filters[the_attendee_people].color : (self.all_filters[-1] ? self.all_filters[-1].color : 1); - the_title_avatar += ''; + the_title_avatar += ''; }//else don't add myself } } else { - attendee_other += self.all_attendees[the_attendee_people] +", "; + attendee_other += _.escape(self.all_attendees[the_attendee_people]) +", "; } } ); diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js index f68097c3f69..e184e70e509 100644 --- a/addons/website/static/src/js/website.snippets.animation.js +++ b/addons/website/static/src/js/website.snippets.animation.js @@ -152,7 +152,7 @@ website.snippet.animationRegistry.media_video = website.snippet.Animation.extend({ selector: ".media_iframe_video", start: function () { - this.$target.html('
 
'); + this.$target.html('
 
'); }, }); })(); From 22bd3bdd2f5d16a69a688e3f7f79875ae44b3f30 Mon Sep 17 00:00:00 2001 From: Olivier Dony Date: Wed, 1 Jul 2015 17:55:24 +0200 Subject: [PATCH 10/10] [FIX] website_google_map: avoid errors in json serialization --- addons/website_google_map/controllers/main.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/addons/website_google_map/controllers/main.py b/addons/website_google_map/controllers/main.py index efddba1b10f..1abc7c41397 100644 --- a/addons/website_google_map/controllers/main.py +++ b/addons/website_google_map/controllers/main.py @@ -4,6 +4,7 @@ import json from openerp import SUPERUSER_ID from openerp.addons.web import http from openerp.addons.web.http import request +from openerp.tools import html_escape as escape class google_map(http.Controller): @@ -50,10 +51,10 @@ class google_map(http.Controller): for partner in partner_obj.browse(cr, SUPERUSER_ID, partners_ids, context=context): partner_data["partners"].append({ 'id': partner.id, - 'name': partner.name, - 'address': '\n'.join(partner.name_get()[0][1].split('\n')[1:]), - 'latitude': partner.partner_latitude, - 'longitude': partner.partner_longitude, + 'name': escape(partner.name), + 'address': escape('\n'.join(partner.name_get()[0][1].split('\n')[1:])), + 'latitude': escape(str(partner.partner_latitude)), + 'longitude': escape(str(partner.partner_longitude)), }) # generate the map