From ff915dd96e187e128e7cf0982009401b67703651 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Mon, 8 Feb 2016 16:47:20 +0100 Subject: [PATCH 1/9] [FIX] import: import new records without xml_ids This is related to revision 80b373f. The above revision leads to the inability to import new records without any `xml_id` set Technically speaking, this is because you cannot concatenate `False` with `'_'`, in `xml_id + '_'` opw-668962 --- openerp/addons/base/ir/ir_model.py | 33 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/openerp/addons/base/ir/ir_model.py b/openerp/addons/base/ir/ir_model.py index 137f912e164..01077356ffe 100644 --- a/openerp/addons/base/ir/ir_model.py +++ b/openerp/addons/base/ir/ir_model.py @@ -1044,22 +1044,23 @@ class ir_model_data(osv.osv): else: if mode=='init' or (mode=='update' and xml_id): inherit_xml_ids = [] - for table, field_name in model_obj._inherits.items(): - xml_ids = self.pool['ir.model.data'].search(cr, uid, [ - ('module', '=', module), - ('name', '=', xml_id + '_' + table.replace('.', '_')), - ], context=context) - # XML ID found in the database, try to recover an existing record - if xml_ids: - found_xml_id = self.pool['ir.model.data'].browse(cr, uid, xml_ids[0], context=context) - record = self.pool[found_xml_id.model].browse(cr, uid, [found_xml_id.res_id], context=context)[0] - # The record exists, store the id and don't recreate the XML ID - if record.exists(): - inherit_xml_ids.append(found_xml_id.model) - values[field_name] = found_xml_id.res_id - # Orphan XML ID, delete it - else: - found_xml_id.unlink() + if xml_id: + for table, field_name in model_obj._inherits.items(): + xml_ids = self.pool['ir.model.data'].search(cr, uid, [ + ('module', '=', module), + ('name', '=', xml_id + '_' + table.replace('.', '_')), + ], context=context) + # XML ID found in the database, try to recover an existing record + if xml_ids: + found_xml_id = self.pool['ir.model.data'].browse(cr, uid, xml_ids[0], context=context) + record = self.pool[found_xml_id.model].browse(cr, uid, [found_xml_id.res_id], context=context)[0] + # The record exists, store the id and don't recreate the XML ID + if record.exists(): + inherit_xml_ids.append(found_xml_id.model) + values[field_name] = found_xml_id.res_id + # Orphan XML ID, delete it + else: + found_xml_id.unlink() res_id = model_obj.create(cr, uid, values, context=context) if xml_id: From d82e489add442f0bef52b58620c8d7988919872d Mon Sep 17 00:00:00 2001 From: Laurent Mignon Date: Thu, 5 Mar 2015 22:31:55 +0100 Subject: [PATCH 2/9] [FIX] models: use `column._symbol_c` as placeholder for the column value When generating SQL queries, `column._symbol_c` must be used as placeholder as for the method 'set' of the column itself. Otherwise it is not possible to define specialized columns. --- openerp/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openerp/models.py b/openerp/models.py index 27f846958f8..390941db954 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -3876,7 +3876,7 @@ class BaseModel(object): self._check_selection_field_value(cr, user, field, vals[field], context=context) if column._classic_write and not hasattr(column, '_fnct_inv'): if (not totranslate) or not column.translate: - updates.append((field, '%s', column._symbol_set[1](vals[field]))) + updates.append((field, column._symbol_set[0], column._symbol_set[1](vals[field]))) direct.append(field) else: upd_todo.append(field) @@ -4189,7 +4189,7 @@ class BaseModel(object): for field in vals: current_field = self._columns[field] if current_field._classic_write: - updates.append((field, '%s', current_field._symbol_set[1](vals[field]))) + updates.append((field, current_field._symbol_set[0], current_field._symbol_set[1](vals[field]))) #for the function fields that receive a value, we set them directly in the database #(they may be required), but we also need to trigger the _fct_inv() @@ -4422,7 +4422,7 @@ class BaseModel(object): value[v] = value[v][0] except: pass - updates.append((v, '%s', column._symbol_set[1](value[v]))) + updates.append((v, column._symbol_set[0], column._symbol_set[1](value[v]))) if updates: query = 'UPDATE "%s" SET %s WHERE id = %%s' % ( self._table, ','.join('"%s"=%s' % u[:2] for u in updates), @@ -4446,8 +4446,8 @@ class BaseModel(object): value = value[0] except: pass - query = 'UPDATE "%s" SET "%s"=%%s WHERE id = %%s' % ( - self._table, f, + query = 'UPDATE "%s" SET "%s"=%s WHERE id = %%s' % ( + self._table, f, column._symbol_set[0], ) cr.execute(query, (column._symbol_set[1](value), id)) From 5763c32a6dd558f65864c661a7c3d068f012df27 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Tue, 9 Feb 2016 10:01:34 +0100 Subject: [PATCH 3/9] [FIX] addons: fix usage of decorators `api.v7`/`api.v8` Specifically, when one API implementation calls the other one, it has to call the method *from the same class*. Otherwise, overriding the method may result in an infinite recursion. Consider: class A(Model): _name = 'stuff' @api.v8 def foo(self): return 42 @api.v7 def foo(self, cr, uid, context=None): return self.browse(cr, uid, [], context).foo() class B(Model): _inherit = 'stuff' def foo(self, cr, uid, context=None): return super(B, self).foo(cr, uid, context=context) + 1 and now call: `env['stuff'].foo()`. This invokes `B.foo` (new-API), which calls `B.foo` (old-API), which calls `A.foo` (old-API), which calls `B.foo` (new-API) instead of `A.foo`! This issue would not be present if old-API `A.foo` was defined as: @api.v7 def foo(self, cr, uid, context=None): return A.foo(self.browse(cr, uid, [], context)) --- addons/account/account.py | 4 ++-- addons/account/account_invoice.py | 4 ++-- addons/report/models/report.py | 16 ++++++++-------- openerp/api.py | 10 ++++++++++ 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/addons/account/account.py b/addons/account/account.py index 18cabe9e169..da19fcf7197 100644 --- a/addons/account/account.py +++ b/addons/account/account.py @@ -2141,8 +2141,8 @@ class account_tax(osv.osv): @api.v8 def compute_all(self, price_unit, quantity, product=None, partner=None, force_excluded=False): - return self._model.compute_all( - self._cr, self._uid, self, price_unit, quantity, + return account_tax.compute_all( + self._model, self._cr, self._uid, self, price_unit, quantity, product=product, partner=partner, force_excluded=force_excluded) def compute(self, cr, uid, taxes, price_unit, quantity, product=None, partner=None): diff --git a/addons/account/account_invoice.py b/addons/account/account_invoice.py index 48f22763986..90d4d7fd954 100644 --- a/addons/account/account_invoice.py +++ b/addons/account/account_invoice.py @@ -1209,7 +1209,7 @@ class account_invoice(models.Model): def pay_and_reconcile(self, cr, uid, ids, pay_amount, pay_account_id, period_id, pay_journal_id, writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context=None, name=''): recs = self.browse(cr, uid, ids, context) - return recs.pay_and_reconcile(pay_amount, pay_account_id, period_id, pay_journal_id, + return account_invoice.pay_and_reconcile(recs, pay_amount, pay_account_id, period_id, pay_journal_id, writeoff_acc_id, writeoff_period_id, writeoff_journal_id, name=name) class account_invoice_line(models.Model): @@ -1600,7 +1600,7 @@ class account_invoice_tax(models.Model): def compute(self, cr, uid, invoice_id, context=None): recs = self.browse(cr, uid, [], context) invoice = recs.env['account.invoice'].browse(invoice_id) - return recs.compute(invoice) + return account_invoice_tax.compute(recs, invoice) @api.model def move_line_get(self, invoice_id): diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 10142ff0695..aa6b6d143b1 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -177,8 +177,8 @@ class Report(osv.Model): @api.v8 def get_html(self, records, report_name, data=None): - return self._model.get_html(self._cr, self._uid, records.ids, report_name, - data=data, context=self._context) + return Report.get_html(self._model, self._cr, self._uid, records.ids, + report_name, data=data, context=self._context) @api.v7 def get_pdf(self, cr, uid, ids, report_name, html=None, data=None, context=None): @@ -276,8 +276,8 @@ class Report(osv.Model): @api.v8 def get_pdf(self, records, report_name, html=None, data=None): - return self._model.get_pdf(self._cr, self._uid, records.ids, report_name, - html=html, data=data, context=self._context) + return Report.get_pdf(self._model, self._cr, self._uid, records.ids, + report_name, html=html, data=data, context=self._context) @api.v7 def get_action(self, cr, uid, ids, report_name, data=None, context=None): @@ -313,8 +313,8 @@ class Report(osv.Model): @api.v8 def get_action(self, records, report_name, data=None): - return self._model.get_action(self._cr, self._uid, records.ids, report_name, - data=data, context=self._context) + return Report.get_action(self._model, self._cr, self._uid, records.ids, + report_name, data=data, context=self._context) #-------------------------------------------------------------------------- # Report generation helpers @@ -360,8 +360,8 @@ class Report(osv.Model): @api.v8 def _check_attachment_use(self, records, report): - return self._model._check_attachment_use( - self._cr, self._uid, records.ids, report, context=self._context) + return Report._check_attachment_use( + self._model, self._cr, self._uid, records.ids, report, context=self._context) def _check_wkhtmltopdf(self): return wkhtmltopdf_state diff --git a/openerp/api.py b/openerp/api.py index 3602790848b..010299d2a6a 100644 --- a/openerp/api.py +++ b/openerp/api.py @@ -608,6 +608,16 @@ def v7(method_v7): def foo(self): ... + Special care must be taken if one method calls the other one, because + the method may be overridden! In that case, one should call the method + from the current class (say ``MyClass``), for instance:: + + @api.v7 + def foo(self, cr, uid, ids, context=None): + # Beware: records.foo() may call an overriding of foo() + records = self.browse(cr, uid, ids, context) + return MyClass.foo(records) + Note that the wrapper method uses the docstring of the first method. """ # retrieve method_v8 from the caller's frame From 3266d43399669484af990312d7f2699808928ebc Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Tue, 9 Feb 2016 11:25:26 +0100 Subject: [PATCH 4/9] [FIX] marketing_campaign: pass context as kwargs in overriden `search` When converting a new API call to an old API call, the context is expected to be found within the kwargs argument. If not, it is seen as a regular argument. See `get_context_split` in `openerp/api.py` As it was not the case in the overidden method `search` in `marketing_campaign.py`, if a module overriden the method `search` of `ir.actions.report.xml` using the new API, the context wasn't treated as such correctly, and it leaded to wrong number of arguments passed. I take the opportunity to pass all arguments that are passed as kwargs in the base `search` as kwargs as well, to be clean. opw-668471 --- addons/marketing_campaign/marketing_campaign.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/marketing_campaign/marketing_campaign.py b/addons/marketing_campaign/marketing_campaign.py index 6a6e3d8e9ad..0b77c35124b 100644 --- a/addons/marketing_campaign/marketing_campaign.py +++ b/addons/marketing_campaign/marketing_campaign.py @@ -819,7 +819,7 @@ class report_xml(osv.osv): if object_id: model = self.pool.get('ir.model').browse(cr, uid, object_id, context=context).model args.append(('model', '=', model)) - return super(report_xml, self).search(cr, uid, args, offset, limit, order, context, count) + return super(report_xml, self).search(cr, uid, args, offset=offset, limit=limit, order=order, context=context, count=count) From cdf21ebe4aa3537d4fec12a649e5529b70b1c57a Mon Sep 17 00:00:00 2001 From: Joren Van Onder Date: Tue, 9 Feb 2016 16:30:01 +0100 Subject: [PATCH 5/9] [FIX] point_of_sale: make POSBox print only one ticket when losing wifi The POSBox attempts to maintain whatever Wi-Fi connection it has as best it can. When it loses it's current Wi-Fi connection it will attempt to recreate it every 30 seconds. This works well, but a side-effect of this is that it'll also print a 'Could not connect to LAN' ticket every time it fails. If you where to leave the POSBox with Wi-Fi on for an extended period of time you could return to a lot of 'Could not connect to LAN' tickets. This makes it so that the 'Could not connect to LAN' ticket only gets printed once upon connection loss. Although it would be simpler to just not print this ticket at all when losing connection, it is very useful to know when the POSBox has lost connection. Otherwise when it loses connection it would stop working and noone would know why. --- .../posbox/configuration/connect_to_wifi.sh | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/addons/point_of_sale/tools/posbox/configuration/connect_to_wifi.sh b/addons/point_of_sale/tools/posbox/configuration/connect_to_wifi.sh index 11f6d18eae4..681732ae714 100755 --- a/addons/point_of_sale/tools/posbox/configuration/connect_to_wifi.sh +++ b/addons/point_of_sale/tools/posbox/configuration/connect_to_wifi.sh @@ -7,6 +7,7 @@ function connect () { WPA_PASS_FILE="/tmp/wpa_pass.txt" PERSISTENT_WIFI_NETWORK_FILE="/home/pi/wifi_network.txt" CURRENT_WIFI_NETWORK_FILE="/tmp/current_wifi_network.txt" # used to repair connection when we lose it + LOST_WIFI_FILE="/tmp/lost_wifi.txt" ESSID="${1}" PASSWORD="${2}" PERSIST="${3}" @@ -15,6 +16,7 @@ function connect () { sleep 3 sudo pkill -f keep_wifi_alive.sh + WIFI_WAS_LOST=$? # make network choice persistent if [ -n "${ESSID}" ] ; then @@ -54,13 +56,24 @@ function connect () { # give dhcp some time timeout 30 sh -c 'until ifconfig wlan0 | grep "inet addr:" ; do sleep 0.1 ; done' + TIMEOUT_RETURN=$? - if [ $? -eq 124 ] && [ -z "${NO_AP}" ] ; then + if [ ${TIMEOUT_RETURN} -eq 124 ] && [ -z "${NO_AP}" ] ; then logger -t posbox_connect_to_wifi "Failed to connect, forcing Posbox AP" sudo /home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/wireless_ap.sh "force" & else - logger -t posbox_connect_to_wifi "Restarting odoo" - sudo service odoo restart + if [ ${TIMEOUT_RETURN} -ne 124 ] ; then + rm -f "${LOST_WIFI_FILE}" + fi + + if [ ! -f "${LOST_WIFI_FILE}" ] ; then + logger -t posbox_connect_to_wifi "Restarting odoo" + sudo service odoo restart + fi + + if [ ${WIFI_WAS_LOST} -eq 0 ] ; then + touch "${LOST_WIFI_FILE}" + fi logger -t posbox_connect_to_wifi "Starting wifi keep alive script" /home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/keep_wifi_alive.sh & From 669b6f7b3def8af10e68d320f120236c96d582b4 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Mon, 8 Feb 2016 18:08:17 +0100 Subject: [PATCH 6/9] [FIX] tools: less strict evaluation of translations Do not parse the translation of an attribute value. The content of a translatable node attribute (such as 'string') should not be tested for XML/HTML validity: the value can contain special chars such as `&` in its source and should not be escaped when evaluated. This fixes escaped page title in the partner form `Sales & Purchases` => `Ventes & Achats` --- openerp/tools/translate.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openerp/tools/translate.py b/openerp/tools/translate.py index 753ea460c18..10a14b557ad 100644 --- a/openerp/tools/translate.py +++ b/openerp/tools/translate.py @@ -219,6 +219,12 @@ class XMLTranslator(object): text = text.replace(term, trans) return text + def process_attr(self, attr): + """ Translate the given node attribute value. """ + term = attr.strip() + trans = term and self.callback(term) + return attr.replace(term, trans) if trans else attr + def process(self, node): """ Process the given xml `node`: collect `todo` and `done` items. """ if ( @@ -249,7 +255,7 @@ class XMLTranslator(object): # complete translations and serialize result as done for attr in TRANSLATED_ATTRS: if node.get(attr): - node.set(attr, self.process_text(node.get(attr))) + node.set(attr, self.process_attr(node.get(attr))) self.done(self.serialize(node.tag, node.attrib, child_trans.get_done())) # add node tail as todo From 0b27a371565fc229f2e82b2d7701e00229fe5051 Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 10 Feb 2016 14:46:59 +0100 Subject: [PATCH 7/9] [FIX] ir_translation: validate translation values and do not sanitize them The value validation was de facto nullified by escaping invalid translations in `XMLTranslator.process_text`. Reimplement it by checking whether the translated term can be translated back to its source term in the field value. The sanitization of translated terms is now gone, as not all terms should be sanitized (like attribute values). --- openerp/addons/base/ir/ir_translation.py | 28 ++++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/openerp/addons/base/ir/ir_translation.py b/openerp/addons/base/ir/ir_translation.py index 6ebd2721bd9..f1cf5982ec2 100644 --- a/openerp/addons/base/ir/ir_translation.py +++ b/openerp/addons/base/ir/ir_translation.py @@ -532,13 +532,24 @@ class ir_translation(osv.osv): records.check_field_access_rights(fmode, model_fields[mname]) records.check_access_rule(fmode) + @api.constrains('type', 'name', 'value') + def _check_value(self): + for trans in self.with_context(lang=None): + if trans.type == 'model' and trans.value: + mname, fname = trans.name.split(',') + record = trans.env[mname].browse(trans.res_id) + field = record._fields[fname] + if callable(field.translate): + # check whether applying (trans.src -> trans.value) then + # (trans.value -> trans.src) gives the original value back + value0 = field.translate(lambda term: None, record[fname]) + value1 = field.translate({trans.src: trans.value}.get, value0) + value2 = field.translate({trans.value: trans.src}.get, value1) + if value2 != value0: + raise ValueError(_("Translation is not valid:\n%s") % trans.value) + @api.model def create(self, vals): - if vals.get('type') == 'model' and vals.get('value'): - # check and sanitize value - mname, fname = vals['name'].split(',') - field = self.env[mname]._fields[fname] - vals['value'] = field.check_trans_value(vals['value']) record = super(ir_translation, self.sudo()).create(vals).with_env(self.env) record.check('create') self.clear_caches() @@ -548,13 +559,6 @@ class ir_translation(osv.osv): def write(self, vals): if vals.get('value'): vals.setdefault('state', 'translated') - ttype = vals.get('type') or self[:1].type - if ttype == 'model': - # check and sanitize value - name = vals.get('name') or self[:1].name - mname, fname = name.split(',') - field = self.env[mname]._fields[fname] - vals['value'] = field.check_trans_value(vals['value']) elif vals.get('src') or not vals.get('value', True): vals.setdefault('state', 'to_translate') self.check('write') From 5d14c2c0ff3db18b0894fe0979413cd58be9720b Mon Sep 17 00:00:00 2001 From: Jeremy Kersten Date: Wed, 10 Feb 2016 12:04:08 +0100 Subject: [PATCH 8/9] [FIX] website_forum: don't display user profile link in post when it is private Return 404 for seo when profile is private. --- addons/website_forum/controllers/main.py | 2 +- addons/website_forum/views/website_forum.xml | 14 +++++++++----- file_state.csv | 0 3 files changed, 10 insertions(+), 6 deletions(-) create mode 100644 file_state.csv diff --git a/addons/website_forum/controllers/main.py b/addons/website_forum/controllers/main.py index e9776226bc8..5bc71a5848c 100644 --- a/addons/website_forum/controllers/main.py +++ b/addons/website_forum/controllers/main.py @@ -603,7 +603,7 @@ class WebsiteForum(http.Controller): if (user_id != request.session.uid and not (user.website_published or (count_user_questions and current_user.karma > forum.karma_unlink_all))): - return request.website.render("website_forum.private_profile", values) + return request.render("website_forum.private_profile", values, status=404) # limit length of visible posts by default for performance reasons, except for the high # karma users (not many of them, and they need it to properly moderate the forum) diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml index 879ca5ba379..ab29205ea40 100644 --- a/addons/website_forum/views/website_forum.xml +++ b/addons/website_forum/views/website_forum.xml @@ -768,7 +768,7 @@ By - @@ -916,13 +916,17 @@