diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 3a52a0c8392..b3f719f1449 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -371,7 +371,7 @@ class AccountMoveLine(models.Model): else: date = partial_line.credit_move_id.date if partial_line.debit_move_id == line else partial_line.debit_move_id.date rate = line.currency_id.with_context(date=date).rate - amount_residual_currency += sign_partial_line * line.currency_id.round(partial_line.amount * rate) + amount_residual_currency += sign_partial_line * partial_line.amount * rate #computing the `reconciled` field. reconciled = False diff --git a/addons/hw_escpos/escpos/escpos.py b/addons/hw_escpos/escpos/escpos.py index 3573efb1d98..bc1c5e1842f 100644 --- a/addons/hw_escpos/escpos/escpos.py +++ b/addons/hw_escpos/escpos/escpos.py @@ -888,14 +888,23 @@ class Escpos: self._raw(PAPER_FULL_CUT) - def cashdraw(self, pin): - """ Send pulse to kick the cash drawer """ - if pin == 2: - self._raw(CD_KICK_2) - elif pin == 5: - self._raw(CD_KICK_5) - else: - raise CashDrawerError() + def cashdraw(self, pin, tries=5): + """ Send pulse to kick the cash drawer + + With some printers the drawer will not open after one pulse, for this reason we will check + the drawer status up to 'tries' times and send a new pulse if the drawer is still closed. If the + drawer status is open, we will stop sending pulses. + """ + for i in range(tries): + if pin == 2: + self._raw(CD_KICK_2) + elif pin == 5: + self._raw(CD_KICK_5) + else: + raise CashDrawerError() + + if i != tries - 1 and not self.get_printer_status()['printer']['drawer_pin_high']: + break def hw(self, hw): diff --git a/addons/l10n_be_hr_payroll_fleet/__manifest__.py b/addons/l10n_be_hr_payroll_fleet/__manifest__.py index 5577d12c671..f05e58bff6e 100644 --- a/addons/l10n_be_hr_payroll_fleet/__manifest__.py +++ b/addons/l10n_be_hr_payroll_fleet/__manifest__.py @@ -11,6 +11,7 @@ 'views/fleet_views.xml', 'views/res_config_settings_views.xml', 'views/hr_contract_views.xml', + 'security/security.xml', ], 'auto_install': True, } diff --git a/addons/l10n_be_hr_payroll_fleet/security/security.xml b/addons/l10n_be_hr_payroll_fleet/security/security.xml new file mode 100644 index 00000000000..3fc21715a4c --- /dev/null +++ b/addons/l10n_be_hr_payroll_fleet/security/security.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/addons/l10n_mx/data/account_tag_data.xml b/addons/l10n_mx/data/account_tag_data.xml index 244c9b65ff3..1e8b23dacda 100644 --- a/addons/l10n_mx/data/account_tag_data.xml +++ b/addons/l10n_mx/data/account_tag_data.xml @@ -479,25 +479,25 @@ 119.01 IVA pendiente de pago 4 accounts - A + D 119.02 IVA de importación pendiente de pago 4 accounts - A + D 119.03 IEPS pendiente de pago 4 accounts - A + D 119.04 IEPS pendiente de pago en importación 4 accounts - A + D 120.01 Anticipo a proveedores nacional @@ -917,7 +917,7 @@ 180.01 Crédito mercantil 4 accounts - A + D 181.01 Gastos de instalación @@ -2423,7 +2423,7 @@ 503 Devoluciones, descuentos o bonificaciones sobre compras 1 accounts - D + A 503.01 Devoluciones, descuentos o bonificaciones sobre compras @@ -2585,13 +2585,13 @@ 505.01 Costo por venta de activo fijo 4 accounts - A + D 505.02 Costo por baja de activo fijo 4 accounts - A + D 601.01 Sueldos y salarios @@ -4781,7 +4781,7 @@ 607.01 Participación de los trabajadores en las utilidades 4 accounts - A + D 608.01 Participación en resultados de subsidiarias @@ -4799,7 +4799,7 @@ 610.01 Participación de los trabajadores en las utilidades diferida 4 accounts - A + D 611.01 Impuesto Sobre la renta @@ -4817,7 +4817,7 @@ 612.01 Gastos no deducibles para CUFIN 4 accounts - A + D 613.01 Depreciación de edificios diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index 36ced25ed75..5b1306dc11d 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -103,7 +103,7 @@ class MailActivity(models.Model): ('today', 'Today'), ('planned', 'Planned')], 'State', compute='_compute_state') - recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type", readonly=True) + recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type") previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type', readonly=True) has_recommended_activities = fields.Boolean( 'Next activities available', diff --git a/addons/mail/models/mail_mail.py b/addons/mail/models/mail_mail.py index 1d53888c9f3..27e3cdd746f 100644 --- a/addons/mail/models/mail_mail.py +++ b/addons/mail/models/mail_mail.py @@ -5,6 +5,7 @@ import base64 import datetime import logging import psycopg2 +import smtplib import threading from collections import defaultdict @@ -150,6 +151,7 @@ class MailMail(models.Model): if notif_emails: notifications = self.env['mail.notification'].search([ ('mail_message_id', 'in', notif_emails.mapped('mail_message_id').ids), + ('res_partner_id', 'in', notif_emails.mapped('recipient_ids').ids), ('is_email', '=', True)]) if mail_sent: notifications.write({ @@ -301,6 +303,20 @@ class MailMail(models.Model): }) mail_sent = False + # Update notification in a transient exception state to avoid concurrent + # update in case an email bounces while sending all emails related to current + # mail record. + notifs = self.env['mail.notification'].search([ + ('is_email', '=', True), + ('mail_message_id', 'in', mail.mapped('mail_message_id').ids), + ('res_partner_id', 'in', mail.mapped('recipient_ids').ids), + ('email_status', 'not in', ('sent', 'canceled')) + ]) + if notifs: + notifs.sudo().write({ + 'email_status': 'exception', + }) + # build an RFC2822 email.message.Message object and send it without queuing res = None for email in email_list: @@ -348,11 +364,12 @@ class MailMail(models.Model): 'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option', mail.id, mail.message_id) raise - except psycopg2.Error: - # If an error with the database occurs, chances are that the cursor is unusable. - # This will lead to an `psycopg2.InternalError` being raised when trying to write - # `state`, shadowing the original exception and forbid a retry on concurrent - # update. Let's bubble it. + except (psycopg2.Error, smtplib.SMTPServerDisconnected): + # If an error with the database or SMTP session occurs, chances are that the cursor + # or SMTP session are unusable, causing further errors when trying to save the state. + _logger.exception( + 'Exception while processing mail with ID %r and Msg-Id %r.', + mail.id, mail.message_id) raise except Exception as e: failure_reason = tools.ustr(e) diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index d60ddb5e3a5..c22a8a10cc9 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -353,6 +353,7 @@ class MailComposer(models.TransientModel): - normal mode: return rendered values /!\ for x2many field, this onchange return command instead of ids """ + attachment_ids = [] if template_id and composition_mode == 'mass_mail': template = self.env['mail.template'].browse(template_id) fields = ['subject', 'body_html', 'email_from', 'reply_to', 'mail_server_id'] @@ -378,7 +379,7 @@ class MailComposer(models.TransientModel): 'res_id': 0, 'type': 'binary', # override default_type from context, possibly meant for another model! } - values.setdefault('attachment_ids', list()).append(Attachment.create(data_attach).id) + attachment_ids.append(Attachment.create(data_attach).id) else: default_values = self.with_context(default_composition_mode=composition_mode, default_model=model, default_res_id=res_id).default_get(['composition_mode', 'model', 'res_id', 'parent_id', 'partner_ids', 'subject', 'body', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id']) values = dict((key, default_values[key]) for key in ['subject', 'body', 'partner_ids', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'] if key in default_values) @@ -391,6 +392,8 @@ class MailComposer(models.TransientModel): # this force the complete replacement of x2many field with # command and is compatible with onchange api.v7 values = self._convert_to_write(values) + if attachment_ids: + values.update(attachment_ids=[(6, 0, attachment_ids)]) return {'value': values} diff --git a/addons/point_of_sale/static/src/js/gui.js b/addons/point_of_sale/static/src/js/gui.js index b5cff2f233a..0d8c67bc3d9 100644 --- a/addons/point_of_sale/static/src/js/gui.js +++ b/addons/point_of_sale/static/src/js/gui.js @@ -458,7 +458,7 @@ var Gui = core.Class.extend({ } } else if (input === '-') { if (options.firstinput) { - newbuf = '-'; + newbuf = '-0'; } else if ( newbuf[0] === '-' ) { newbuf = newbuf.substring(1,newbuf.length); } else { @@ -473,6 +473,9 @@ var Gui = core.Class.extend({ newbuf += input; } } + if (newbuf === "-") { + newbuf = ""; + } // End of input buffer at 12 characters. if (newbuf.length > buffer.length && newbuf.length > 12) { diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index fd49ac36aed..b591f497745 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -1040,7 +1040,7 @@ class SaleOrderLine(models.Model): result.setdefault(so_line_id, 0.0) uom = product_uom_map.get(item['product_uom_id'][0]) if so_line.product_uom.category_id == uom.category_id: - qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom) + qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom, rounding_method='HALF-UP') else: qty = item['unit_amount'] result[so_line_id] += qty diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js index e28e1e0d14f..c9e28c31d6a 100644 --- a/addons/website/static/src/js/content/snippets.animation.js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -825,6 +825,10 @@ registry.gallerySlider = Animation.extend({ destroy: function () { this._super.apply(this, arguments); + if (!this.$indicator) { + return; + } + this.$prev.prependTo(this.$indicator); this.$next.appendTo(this.$indicator); this.$carousel.off('.gallery_slider'); diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml index f441eb74dcb..1948413b47a 100644 --- a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml +++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml @@ -170,18 +170,12 @@ diff --git a/addons/website_customer/views/website_customer_templates.xml b/addons/website_customer/views/website_customer_templates.xml index 5674f3d3440..9cbab253c0c 100644 --- a/addons/website_customer/views/website_customer_templates.xml +++ b/addons/website_customer/views/website_customer_templates.xml @@ -239,4 +239,13 @@ + + diff --git a/doc/cla/corporate/ecoservice.md b/doc/cla/corporate/ecoservice.md new file mode 100644 index 00000000000..b1b1cbe8ff0 --- /dev/null +++ b/doc/cla/corporate/ecoservice.md @@ -0,0 +1,21 @@ +Germany, 2019-05-14 + +ecoservice GbR 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, + +Falk Neubert neubert@ecoservice.de https://github.com/FNeu75 + +List of contributors: + +* Christian Engelhardt c.engelhardt@ecoservice.de https://github.com/c-engelhardt-ecoservice +* Christian Schöttke c.schoettke@ecoservice.de https://github.com/cschoettke +* Falk Neubert neubert@ecoservice.de https://github.com/FNeu75 +* Gülhan Celik g.celik@ecoservice.de https://github.com/guelhancelik +* Jan Brodersen 4rmitxes@gmail.com https://github.com/Armitxes +* Kamal Prajapati k.prajapati@ecoservice.de https://github.com/kamalprajapati +* Marcel Much m.much@ecoservice.de https://github.com/mmuch diff --git a/doc/cla/corporate/savoirfairelinux.md b/doc/cla/corporate/savoirfairelinux.md index 913e317cb07..04f4563d4b7 100644 --- a/doc/cla/corporate/savoirfairelinux.md +++ b/doc/cla/corporate/savoirfairelinux.md @@ -12,17 +12,24 @@ João Alfredo Gama Batista joao.gama@savoirfairelinux.com https://github.com/joa List of contributors: -Agathe Mollé agathe.molle@savoirfairelinux.com https://github.com/Ehtaga -Bruno Joliveau bruno.joliveau@savoirfairelinux.com https://github.com/bjoliveau -David Dufresne david.dufresne@savoirfairelinux.com https://github.com/dufresnedavid -Istvan SZALAÏ istvan.szalai@savoirfairelinux.com https://github.com/ventilooo -João Alfredo Gama Batista joao.gama@savoirfairelinux.com https://github.com/joaoalf -Julien Jezequel-Breard julien.jezequel-breard@savoirfairelinux.com https://github.com/jjbreard -Quentin Lavallée-Bourdeau quentin.lavallee@savoirfairelinux.com https://github.com/qtiplb + Rim Ben Dhaou rim.bendhaou@savoirfairelinux.com https://github.com/rimbendhaou -Yasmine El Mrini yasmine.elmrini@savoirfairelinux.com https://github.com/yasmineelmrini Luis Garcia Ruiz luis.garcia@savoirfairelinux.com https://github.com/eilst Dimitar Apostolov dimitar.apostolov@savoirfairelinux.com https://github.com/dapostolov-sfl +Pierre Gault pierre.gault@savoirfairelinux.com https://github.com/gaultp +Maroua Romdhane maroua.romdhane@savoirfairelinux.com https://github.com/mromdhane +Jean-François Bolduc jean-francois.bolduc@savoirfairelinux.com https://github.com/fjdrake +Blaise Frison blaise.frison@savoirfairelinux.com https://github.com/bfrison-sfl +Larbi Gharib larbi.gharib@savoirfairelinux.com https://github.com/lgharib +Jananjoy Rajkumar jananjoy.rajkumar@savoirfairelinux.com https://github.com/jananjoy +Yasmine El Mrini yasmine.elmrini@savoirfairelinux.com https://github.com/yasmineelmrini (up to 2019-05-13) +Agathe Mollé agathe.molle@savoirfairelinux.com https://github.com/Ehtaga (up to 2019-05-13) +Bruno Joliveau bruno.joliveau@savoirfairelinux.com https://github.com/bjoliveau (up to 2019-05-13) +David Dufresne david.dufresne@savoirfairelinux.com https://github.com/dufresnedavid (up to 2019-05-13) +Istvan SZALAÏ istvan.szalai@savoirfairelinux.com https://github.com/ventilooo (up to 2019-05-13) +João Alfredo Gama Batista joao.gama@savoirfairelinux.com https://github.com/joaoalf (up to 2019-05-13) +Julien Jezequel-Breard julien.jezequel-breard@savoirfairelinux.com https://github.com/jjbreard (up to 2019-05-13) +Quentin Lavallée-Bourdeau quentin.lavallee@savoirfairelinux.com https://github.com/qtiplb (up to 2019-05-13) Jérome Boisvert-Chouinard jerome.boisvertchouinard@savoirfairelinux.com https://github.com/jbchouinard (up to 2017-10-17) Julie Moussu julie.mousse@savoirfairelinux.com https://github.com/JulieSFL (up to 2017-10-17) Pierre Lamarche pierre.lamarche@savoirfairelinux.com https://github.com/plamarche (up to 2017-10-17) @@ -34,6 +41,5 @@ Jordi Riera jordi.riera@savoirfairelinux.com https://github.com/foutoucour (up t Julien Roux julien.roux@savoirfairelinux.com https://github.com/jrouxsfl (up to 2017-05-12) Loïc Faure-Lacroix loic.lacroix@savoirfairelinux.com https://github.com/llacroix (up to 2017-05-12) Maxime Chambreuil maxime.chambreuil@savoirfairelinux.com https://github.com/max3903 (up to 2017-05-12) -Pierre Gault pierre.gault@savoirfairelinux.com https://github.com/gaultp (up to 2017-05-12) Sandy Carter sandy.carter@savoirfairelinux.com https://github.com/bwrsandman (up to 2017-05-12) Vincent Vinet vincent.vinet@savoirfairelinux.com https://github.com/veloutin (up to 2017-05-12) diff --git a/doc/cla/individual/gjanssens.md b/doc/cla/individual/gjanssens.md new file mode 100644 index 00000000000..d3cda29d978 --- /dev/null +++ b/doc/cla/individual/gjanssens.md @@ -0,0 +1,11 @@ +Belgium, 2019-05-09 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Geert Janssens geert@kobaltwit.be https://github.com/gjanssens diff --git a/odoo/addons/base/models/ir_mail_server.py b/odoo/addons/base/models/ir_mail_server.py index 9cf939dcf05..8102d2e65e7 100644 --- a/odoo/addons/base/models/ir_mail_server.py +++ b/odoo/addons/base/models/ir_mail_server.py @@ -119,10 +119,18 @@ def encode_rfc2822_address_header(header_text): # Header as a string, using an unlimited line length.", the old one # was "A synonym for Header.encode()." so call encode() directly? name = Header(pycompat.to_text(name)).encode() - return formataddr((name, email)) + # if the from does not follow the (name ),* convention, we might + # try to encode meaningless strings as address, as getaddresses is naive + # note it would also fail on real addresses with non-ascii characters + try: + return formataddr((name, email)) + except UnicodeEncodeError: + _logger.warning(_('Failed to encode the address %s\n' + 'from mail header:\n%s') % addr, header_text) + return "" addresses = getaddresses([pycompat.to_native(ustr(header_text))]) - return COMMASPACE.join(encode_addr(a) for a in addresses) + return COMMASPACE.join(a for a in (encode_addr(addr) for addr in addresses) if a) class IrMailServer(models.Model): @@ -481,6 +489,8 @@ class IrMailServer(models.Model): # do not quit() a pre-established smtp_session if not smtp_session: smtp.quit() + except smtplib.SMTPServerDisconnected: + raise except Exception as e: params = (ustr(smtp_server), e.__class__.__name__, ustr(e)) msg = _("Mail delivery failed via SMTP server '%s'.\n%s: %s") % params diff --git a/odoo/addons/base/models/res_users.py b/odoo/addons/base/models/res_users.py index 96c6b221a40..1d78832fe14 100644 --- a/odoo/addons/base/models/res_users.py +++ b/odoo/addons/base/models/res_users.py @@ -874,9 +874,9 @@ class UsersView(models.Model): group_multi_company = self.env.ref('base.group_multi_company', False) if group_multi_company and 'company_ids' in values: if len(user.company_ids) <= 1 and user.id in group_multi_company.users.ids: - group_multi_company.write({'users': [(3, user.id)]}) + user.write({'groups_id': [(3, group_multi_company.id)]}) elif len(user.company_ids) > 1 and user.id not in group_multi_company.users.ids: - group_multi_company.write({'users': [(4, user.id)]}) + user.write({'groups_id': [(4, group_multi_company.id)]}) return user @api.multi @@ -887,9 +887,9 @@ class UsersView(models.Model): if group_multi_company and 'company_ids' in values: for user in self: if len(user.company_ids) <= 1 and user.id in group_multi_company.users.ids: - group_multi_company.write({'users': [(3, user.id)]}) + user.write({'groups_id': [(3, group_multi_company.id)]}) elif len(user.company_ids) > 1 and user.id not in group_multi_company.users.ids: - group_multi_company.write({'users': [(4, user.id)]}) + user.write({'groups_id': [(4, group_multi_company.id)]}) return res def _remove_reified_groups(self, values): diff --git a/odoo/addons/test_convert/__init__.py b/odoo/addons/test_convert/__init__.py index e69de29bb2d..0650744f6bc 100644 --- a/odoo/addons/test_convert/__init__.py +++ b/odoo/addons/test_convert/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/odoo/addons/test_convert/__manifest__.py b/odoo/addons/test_convert/__manifest__.py index af6cd1af6e4..a9c67447615 100644 --- a/odoo/addons/test_convert/__manifest__.py +++ b/odoo/addons/test_convert/__manifest__.py @@ -5,4 +5,5 @@ 'name': 'test_convert', 'description': "Data for xml conversion tests", 'version': '0.0.1', + 'data': ['ir.model.access.csv'], } diff --git a/odoo/addons/test_convert/ir.model.access.csv b/odoo/addons/test_convert/ir.model.access.csv new file mode 100644 index 00000000000..014e4d1c827 --- /dev/null +++ b/odoo/addons/test_convert/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_test_convert_test_model,access_test_convert_test_model,model_test_convert_test_model,,1,1,1,1 \ No newline at end of file diff --git a/odoo/addons/test_convert/models.py b/odoo/addons/test_convert/models.py new file mode 100644 index 00000000000..0f059e56e86 --- /dev/null +++ b/odoo/addons/test_convert/models.py @@ -0,0 +1,16 @@ +from odoo import api, models + +class TestModel(models.Model): + _name = 'test_convert.test_model' + + @api.model + def action_test_date(self, today_date): + return True + + @api.model + def action_test_time(self, cur_time): + return True + + @api.model + def action_test_timezone(self, timezone): + return True diff --git a/odoo/addons/test_convert/tests/test_convert.py b/odoo/addons/test_convert/tests/test_convert.py index e81526dd43c..1bb79619c21 100644 --- a/odoo/addons/test_convert/tests/test_convert.py +++ b/odoo/addons/test_convert/tests/test_convert.py @@ -4,8 +4,10 @@ import collections import unittest +from lxml import etree as ET from lxml.builder import E +import odoo from odoo.tests import common from odoo.tools.convert import _eval_xml @@ -16,6 +18,25 @@ class TestEvalXML(common.TransactionCase): def eval_xml(self, node, obj=None): return _eval_xml(obj, node, self.env) + def test_function_eval(self): + def id_get(): pass + Obj = collections.namedtuple('Obj', ['module', 'idref', 'id_get']) + obj = Obj('test_convert', {}, id_get) + + try: + test_datetime = ET.XML("") + self.eval_xml(node=test_datetime, obj=obj) + test_time = ET.XML("") + self.eval_xml(node=test_time, obj=obj) + test_timedelta = ET.XML("") + self.eval_xml(node=test_timedelta, obj=obj) + test_relativedelta = ET.XML("") + self.eval_xml(node=test_relativedelta, obj=obj) + test_timezone = ET.XML("") + self.eval_xml(node=test_timezone, obj=obj) + except ValueError as e: + self.fail(e.message) + def test_char(self): self.assertEqual( self.eval_xml(Field("foo")), diff --git a/odoo/tools/convert.py b/odoo/tools/convert.py index 16dd569239f..94b1c26b1ce 100644 --- a/odoo/tools/convert.py +++ b/odoo/tools/convert.py @@ -179,15 +179,16 @@ def _eval_xml(self, node, env): elif node.tag == "function": args = [] a_eval = node.get('eval') + model_str = node.get('model') # FIXME: should probably be exclusive if a_eval: - self.idref['ref'] = self.id_get - args = safe_eval(a_eval, self.idref) + idref2 = _get_idref(self, env, model_str, self.idref) + args = safe_eval(a_eval, idref2) for n in node: return_val = _eval_xml(self, n, env) if return_val is not None: args.append(return_val) - model = env[node.get('model')] + model = env[model_str] method = node.get('name') # this one still depends on the old API return odoo.api.call_kw(model, method, args, {})