summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2018-02-09 16:13:08 +0100
committerChristophe Simonis <chs@odoo.com>2018-02-09 16:13:08 +0100
commit5f3301e5cbbb2c6e7bff732761eef80e8dd5a33d (patch)
tree48ab6bd9bbda92dea713c0c52bca3396ef939d82
parent715e602bf04ae5a0c9704c599f2145b2808b31b7 (diff)
parent20e4cd5bcd1816a478aedd441efa90a34cf63334 (diff)
[MERGE] forward port branch 11.0 up to 20e4cd5bcd1
-rw-r--r--addons/account/models/chart_template.py2
-rw-r--r--addons/account/static/src/js/reconciliation/reconciliation_renderer.js2
-rw-r--r--addons/base_vat_autocomplete/models/res_partner.py25
-rw-r--r--addons/calendar/i18n/calendar.pot14
-rw-r--r--addons/calendar/models/calendar.py4
-rw-r--r--addons/crm_phone_validation/__manifest__.py1
-rw-r--r--addons/crm_phone_validation/models/crm_lead.py5
-rw-r--r--addons/crm_phone_validation/models/res_partner.py4
-rw-r--r--addons/crm_phone_validation/views/crm_lead_views.xml13
-rw-r--r--addons/hr_maintenance/__manifest__.py2
-rw-r--r--addons/l10n_be/i18n_extra/nl_BE.po4
-rw-r--r--addons/l10n_co/data/account.account.template.csv68
-rw-r--r--addons/l10n_co/data/account.tax.template.csv108
-rw-r--r--addons/l10n_co/models/res_partner.py15
-rw-r--r--addons/l10n_co/views/res_partner.xml2
-rw-r--r--addons/mail/models/mail_message.py5
-rw-r--r--addons/mail/static/src/js/systray.js2
-rw-r--r--addons/payment_stripe/static/src/js/stripe.js5
-rw-r--r--addons/phone_validation/models/phone_validation_mixin.py7
-rw-r--r--addons/phone_validation/tools/phone_validation.py2
-rw-r--r--addons/point_of_sale/static/src/js/screens.js1
-rw-r--r--addons/purchase/models/purchase.py36
-rw-r--r--addons/sale/controllers/portal.py2
-rw-r--r--addons/sale_payment/views/sale_portal_templates.xml4
-rw-r--r--addons/sale_timesheet/models/account.py109
-rw-r--r--addons/sms/__manifest__.py4
-rw-r--r--addons/sms/static/src/js/sms_widget.js120
-rw-r--r--addons/sms/static/src/tests/sms_widget_test.js67
-rw-r--r--addons/sms/static/src/xml/sms_widget.xml6
-rw-r--r--addons/sms/views/templates.xml16
-rw-r--r--addons/sms/wizard/send_sms.py3
-rw-r--r--addons/sms/wizard/send_sms_views.xml2
-rw-r--r--addons/stock/models/stock_move.py3
-rw-r--r--addons/stock/models/stock_move_line.py9
-rw-r--r--addons/stock/tests/test_move.py71
-rw-r--r--addons/test_mail/tests/test_mail_message.py9
-rw-r--r--addons/web/static/src/js/views/basic/basic_controller.js2
-rw-r--r--addons/web/static/src/js/views/basic/basic_renderer.js51
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_model.js1
-rw-r--r--addons/web/static/tests/views/form_tests.js5
-rw-r--r--addons/web/static/tests/views/list_tests.js33
-rw-r--r--addons/web/static/tests/views/pivot_tests.js29
-rw-r--r--addons/web_editor/controllers/main.py15
-rw-r--r--addons/web_editor/static/src/js/backend/fields.js3
-rw-r--r--addons/web_editor/static/src/js/editor/rte.summernote.js6
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js7
-rw-r--r--addons/web_editor/static/src/js/editor/summernote.js24
-rw-r--r--addons/web_editor/static/src/js/editor/transcoder.js32
-rw-r--r--addons/web_editor/static/src/js/inline.js1
-rw-r--r--addons/web_editor/static/src/js/widgets/widgets.js137
-rw-r--r--addons/web_editor/static/src/xml/editor.xml7
-rw-r--r--addons/website/models/ir_qweb.py2
-rw-r--r--addons/website_quote/models/sale_order.py4
-rw-r--r--doc/cla/corporate/wt-io-it.md15
-rw-r--r--doc/cla/individual/remi-filament.md2
-rw-r--r--doc/setup/cdn.rst3
-rw-r--r--odoo/addons/base/models/ir_attachment.py11
-rw-r--r--odoo/addons/base/models/ir_http.py7
-rw-r--r--odoo/addons/base/models/res_company.py4
59 files changed, 881 insertions, 272 deletions
diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py
index 21fcbff39a7..7512bf15f3b 100644
--- a/addons/account/models/chart_template.py
+++ b/addons/account/models/chart_template.py
@@ -770,7 +770,7 @@ class WizardMultiChartsAccounts(models.TransientModel):
if company_id:
company = self.env['res.company'].browse(company_id)
currency_id = company.on_change_country(company.country_id.id)['value']['currency_id']
- res.update({'currency_id': currency_id.id})
+ res.update({'currency_id': currency_id})
chart_templates = account_chart_template.search([('visible', '=', True)])
if chart_templates:
diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
index 91139aa0926..048d6ce9387 100644
--- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
+++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js
@@ -613,7 +613,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
/**
* @private
*/
- _onFilterChange: function () {
+ _onFilterChange: function (event) {
this.trigger_up('change_filter', {'data': _.str.strip($(event.target).val())});
},
/**
diff --git a/addons/base_vat_autocomplete/models/res_partner.py b/addons/base_vat_autocomplete/models/res_partner.py
index 41b40ec9c1d..93d74c497f8 100644
--- a/addons/base_vat_autocomplete/models/res_partner.py
+++ b/addons/base_vat_autocomplete/models/res_partner.py
@@ -30,16 +30,23 @@ class ResPartner(models.Model):
city = lines.pop()
return (cp, city)
else:
- result = re.match('((?:L-|AT-)?[0-9\-]+) (.+)', lines[-1])
+ result = re.match('((?:L-|AT-)?[0-9\-]+[A-Z]{,2}) (.+)', lines[-1])
if result:
lines.pop()
return (result.group(1), result.group(2))
return False
+ def _set_address_field(partner, field, value):
+ partner[field] = value
+ non_set_address_fields.remove(field)
+
if stdnum_vat is None:
return {}
for partner in self:
+ # If a field is non set in this algorithm
+ # wipe it anyway
+ non_set_address_fields = set(['street', 'street2', 'city', 'zip', 'state_id', 'country_id'])
if not partner.vat:
return {}
if len(partner.vat) > 5 and partner.vat[:2].lower() in stdnum_vat.country_codes:
@@ -68,14 +75,20 @@ class ResPartner(models.Model):
lines = [x.strip() for x in lines[0].split(',') if x]
if len(lines) == 1:
lines = [x.strip() for x in lines[0].split(' ') if x]
- partner.street = lines.pop(0)
+
+ _set_address_field(partner, 'street', lines.pop(0))
+
if len(lines) > 0:
res = _check_city(lines, result['countryCode'])
if res:
- partner.zip = res[0]
- partner.city = res[1]
+ _set_address_field(partner, 'zip', res[0])
+ _set_address_field(partner, 'city', res[1])
if len(lines) > 0:
- partner.street2 = lines.pop(0)
+ _set_address_field(partner, 'street2', lines.pop(0))
country = self.env['res.country'].search([('code', '=', result['countryCode'])], limit=1)
- partner.country_id = country and country.id or False
+ _set_address_field(partner, 'country_id', country and country.id or False)
+
+ for field in non_set_address_fields:
+ if partner[field]:
+ partner[field] = False
diff --git a/addons/calendar/i18n/calendar.pot b/addons/calendar/i18n/calendar.pot
index b7f2a7cfe3d..f13382c3979 100644
--- a/addons/calendar/i18n/calendar.pot
+++ b/addons/calendar/i18n/calendar.pot
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 11.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-11-30 13:12+0000\n"
-"PO-Revision-Date: 2017-11-30 13:12+0000\n"
+"POT-Creation-Date: 2018-02-07 10:30+0000\n"
+"PO-Revision-Date: 2018-02-07 10:30+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -273,11 +273,6 @@ msgid "\n"
msgstr ""
#. module: calendar
-#: model:ir.model.fields,field_description:calendar.field_calendar_event_is_highlighted
-msgid "# Meetings Highlight"
-msgstr ""
-
-#. module: calendar
#: model:mail.template,subject:calendar.calendar_template_meeting_reminder
msgid "${object.event_id.name} - Reminder"
msgstr ""
@@ -806,6 +801,11 @@ msgid "Invite wizard"
msgstr ""
#. module: calendar
+#: model:ir.model.fields,field_description:calendar.field_calendar_event_is_highlighted
+msgid "Is the Event Highlighted"
+msgstr ""
+
+#. module: calendar
#: selection:calendar.event,byday:0
msgid "Last"
msgstr ""
diff --git a/addons/calendar/models/calendar.py b/addons/calendar/models/calendar.py
index 97676349df9..6fcf24f2c35 100644
--- a/addons/calendar/models/calendar.py
+++ b/addons/calendar/models/calendar.py
@@ -823,7 +823,7 @@ class Meeting(models.Model):
attendee_ids = fields.One2many('calendar.attendee', 'event_id', 'Participant', ondelete='cascade')
partner_ids = fields.Many2many('res.partner', 'calendar_event_res_partner_rel', string='Attendees', states={'done': [('readonly', True)]}, default=_default_partners)
alarm_ids = fields.Many2many('calendar.alarm', 'calendar_alarm_calendar_event_rel', string='Reminders', ondelete="restrict", copy=False)
- is_highlighted = fields.Boolean(compute='_compute_is_highlighted', string='# Meetings Highlight')
+ is_highlighted = fields.Boolean(compute='_compute_is_highlighted', string='Is the Event Highlighted')
@api.multi
def _compute_attendee(self):
@@ -1478,7 +1478,7 @@ class Meeting(models.Model):
partners_to_notify = meeting.partner_ids.ids
event_attendees_changes = attendees_create and real_ids and attendees_create[real_ids[0]]
if event_attendees_changes:
- partners_to_notify.append(event_attendees_changes['removed_partners'].ids)
+ partners_to_notify.extend(event_attendees_changes['removed_partners'].ids)
self.env['calendar.alarm_manager'].notify_next_alarm(partners_to_notify)
if (values.get('start_date') or values.get('start_datetime') or
diff --git a/addons/crm_phone_validation/__manifest__.py b/addons/crm_phone_validation/__manifest__.py
index 7c729bfae3e..72a21dbfcfa 100644
--- a/addons/crm_phone_validation/__manifest__.py
+++ b/addons/crm_phone_validation/__manifest__.py
@@ -13,6 +13,7 @@ CRM Phone Numbers Validation
This module allows for validate and format phone numbers for leads and contacts.""",
'data': [
'views/res_config_settings_views.xml',
+ 'views/crm_lead_views.xml'
],
'depends': [
'phone_validation',
diff --git a/addons/crm_phone_validation/models/crm_lead.py b/addons/crm_phone_validation/models/crm_lead.py
index a652f1bd5ca..76c1617c5f2 100644
--- a/addons/crm_phone_validation/models/crm_lead.py
+++ b/addons/crm_phone_validation/models/crm_lead.py
@@ -8,13 +8,12 @@ class Lead(models.Model):
_name = 'crm.lead'
_inherit = ['crm.lead', 'phone.validation.mixin']
- @api.onchange('phone', 'country_id')
+ @api.onchange('phone', 'country_id', 'company_id')
def _onchange_phone_validation(self):
if self.phone:
self.phone = self.phone_format(self.phone)
- @api.onchange('mobile', 'country_id')
+ @api.onchange('mobile', 'country_id', 'company_id')
def _onchange_mobile_validation(self):
if self.mobile:
self.mobile = self.phone_format(self.mobile)
-
diff --git a/addons/crm_phone_validation/models/res_partner.py b/addons/crm_phone_validation/models/res_partner.py
index 97f5eaf891d..5328fe9e7d8 100644
--- a/addons/crm_phone_validation/models/res_partner.py
+++ b/addons/crm_phone_validation/models/res_partner.py
@@ -8,12 +8,12 @@ class Contact(models.Model):
_name = 'res.partner'
_inherit = ['res.partner', 'phone.validation.mixin']
- @api.onchange('phone', 'country_id')
+ @api.onchange('phone', 'country_id', 'company_id')
def _onchange_phone_validation(self):
if self.phone:
self.phone = self.phone_format(self.phone)
- @api.onchange('mobile', 'country_id')
+ @api.onchange('mobile', 'country_id', 'company_id')
def _onchange_mobile_validation(self):
if self.mobile:
self.mobile = self.phone_format(self.mobile)
diff --git a/addons/crm_phone_validation/views/crm_lead_views.xml b/addons/crm_phone_validation/views/crm_lead_views.xml
new file mode 100644
index 00000000000..bcf1840bd7a
--- /dev/null
+++ b/addons/crm_phone_validation/views/crm_lead_views.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <record id="crm_lead_opportunity_view_form" model="ir.ui.view">
+ <field name="name">crm.lead.form.opportunity.inherit.crm.phone.validation</field>
+ <field name="model">crm.lead</field>
+ <field name="inherit_id" ref="crm.crm_case_form_view_oppor" />
+ <field name="arch" type="xml">
+ <xpath expr="//group[@name='Misc']" position="inside">
+ <field name="company_id" invisible="1"/>
+ </xpath>
+ </field>
+ </record>
+</odoo>
diff --git a/addons/hr_maintenance/__manifest__.py b/addons/hr_maintenance/__manifest__.py
index 71be2347203..5554d8e2dec 100644
--- a/addons/hr_maintenance/__manifest__.py
+++ b/addons/hr_maintenance/__manifest__.py
@@ -14,5 +14,5 @@
],
'demo': [],
'installable': True,
- 'auto-install': True,
+ 'auto_install': True,
}
diff --git a/addons/l10n_be/i18n_extra/nl_BE.po b/addons/l10n_be/i18n_extra/nl_BE.po
index 08330c05d08..88d6c4b61ee 100644
--- a/addons/l10n_be/i18n_extra/nl_BE.po
+++ b/addons/l10n_be/i18n_extra/nl_BE.po
@@ -147,7 +147,7 @@ msgstr "12% Diverse goederen"
#: model:account.tax,name:l10n_be.1_attn_VAT-IN-V83-12-CC
#: model:account.tax.template,name:l10n_be.attn_VAT-IN-V83-12-CC
msgid "12% Cocont. - Biens d'investissement"
-msgstr "0% Medecont. - Investeringsgoederen"
+msgstr "12% Medecont. - Investeringsgoederen"
#. module: l10n_be
#: model:account.tax,name:l10n_be.1_attn_VAT-IN-V83-12-CC-C1
@@ -389,7 +389,7 @@ msgstr "21% Medecont. G."
#: model:account.tax,name:l10n_be.1_attn_VAT-IN-V83-21-CC-C2
#: model:account.tax.template,name:l10n_be.attn_VAT-IN-V83-21-CC-C2
msgid "21% Cocont. Récupérable - Biens d'investissement"
-msgstr "12% Medecont. Verhaalbaar - Investeringsgoederen"
+msgstr "21% Medecont. Verhaalbaar - Investeringsgoederen"
#. module: l10n_be
#: model:account.tax,name:l10n_be.1_attn_VAT-IN-V81-21-CC-C2
diff --git a/addons/l10n_co/data/account.account.template.csv b/addons/l10n_co/data/account.account.template.csv
index 4bb5aedf148..e73728e96cf 100644
--- a/addons/l10n_co/data/account.account.template.csv
+++ b/addons/l10n_co/data/account.account.template.csv
@@ -58,6 +58,34 @@ co_puc_235510,235510,Socios,TRUE,account.data_account_type_payable,l10n_co_chart
co_puc_236005,236005,Dividendos,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
co_puc_236505,236505,Salarios y Pagos Laborales,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
co_puc_236510,236510,Dividendos y/o Participaciones,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23651505,23651505,Retencion en la fuente por honorarios persona natural 10%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23651510,23651510,Retencion en la fuente por honorarios persona Juridica 11%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23651515,23651515,Retencion en la fuente por servicios de licencias software 3.5%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23651520,23651520,Retencion en la fuente por servicios de diseño web y consultorí informática 3.5%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23652005,23652005,Retencion en la fuente por comisiones persona natural 10%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23652010,23652010,Retencion en la fuente por comisiones persona juridica 11%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23652505,23652505,Retencion en la fuente por servicios en general 4%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23652510,23652510,Retencion en la fuente por servicios en general personas naturales no declarantes Renta 6%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23652515,23652515,Retencion en la fuente por servicio transporte nacional carga 1%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23652520,23652520,Retencion en la fuente por servicio transporte terrestre nacional pasajeros 3.5%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23652525,23652525,Retencion en la fuente por servicio transporte aereo y maritimo nacional pasajeros 1%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23652530,23652530,Retencion en la fuente por servicio temporales de empleo (sobre AIU) 1%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23652535,23652535,Retencion en la fuente por servicio de vigilancia y aseo (sobre AIU) 2%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23652540,23652540,Retencion en la fuente por servicio integrales de salud 2%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23652545,23652545,Retencion en la fuente por servicio de hoteles y restaurantes 3.5%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23652550,23652550,Retencion en la fuente por contratos de obra inmuebles 2%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23653005,23653005,Retencion en la fuente por arrendamientos Bienes Muebles 4%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23653010,23653010,Retencion en la fuente por arrendamientos Bienes Inmuebles 3.5%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23653510,23653510,Retencion en la fuente por rendimientos financieros generales 7%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23654005,23654005,Retencion en la fuente por compras declarantes 2.5%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23654010,23654010,Retencion en la fuente por compras no declarantes 3.5%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23654015,23654015,Retencion en la fuente por compras bienes agricolas 1.5%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23654020,23654020,Retencion en la fuente por compra cafe 0.5%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23654025,23654025,Retencion en la fuente por compra combustibles 0.1%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23654030,23654030,Retencion en la fuente por adquisicion de vehiculos 1%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23654035,23654035,Retencion en la fuente por adquisicion de bienes raices comerciales 2.5%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23654040,23654040,Retencion en la fuente por ventas de activos fijos 1%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_23655005,23655005,Retencion en la fuente por consultorias servicios tecnicos y asistencia tecnica pagos al exterior 10%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
co_puc_236575,236575,Autorretenciones,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
co_puc_236705,236705,Impuesto a Las Ventas Retenido,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
co_puc_236805,236805,Impuesto de Industria y Comercio Retenido,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
@@ -81,6 +109,7 @@ co_puc_2408101005,2408101005,Compra Anulada Iva 19%,FALSE,account.data_account_t
co_puc_2408101010,2408101010,Compra Anulada Iva 5%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
co_puc_2408101015,2408101015,Compra Anulada Iva 16%,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
co_puc_240820,240820,Iva Descontable a Regimen Simplificado (Commun/Proporcional) - Solo Esta la DIAN lo Interpreta Utilizable,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
+co_puc_24082005,24082005,Iva retenido a regimen simplificado,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
co_puc_241205,241205,Vigencia Fiscal Corriente,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
co_puc_245605,245605,a Las Importaciones,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
co_puc_249505,249505,Otros,FALSE,account.data_account_type_current_liabilities,l10n_co_chart_template_generic
@@ -107,8 +136,45 @@ co_puc_340515,340515,de Reservas,FALSE,account.data_account_type_equity,l10n_co_
co_puc_340520,340520,de Resultados de Ejercicios Anteriores,FALSE,account.data_account_type_equity,l10n_co_chart_template_generic
co_puc_360505,360505,Utilidad Del Ejercicio,FALSE,account.data_account_type_equity,l10n_co_chart_template_generic
co_puc_370505,370505,Utilidades Acumuladas,FALSE,account.data_account_type_equity,l10n_co_chart_template_generic
-co_puc_415505,415505,Arrendamientos de Bienes Inmuebles,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413502,413502,Venta de vehículos automotores,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413504,413504,"Mantenimiento, reparación y lavado de vehículos automotores",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413506,413506,"Venta de partes, piezas y accesorios de vehículos automotores",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413508,413508,"Venta de combustibles sólidos, líquidos, gaseosos",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413510,413510,"Venta de lubricantes, aditivos, llantas y lujos para automotores",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413512,413512,Venta a cambio de retribución o por contrata,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413514,413514,"Venta de insumos, materias primas agropecuarias y flores",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413516,413516,Venta de otros insumos y materias primas no agropecuarias,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413518,413518,Venta de animales vivos y cueros,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413520,413520,Venta de productos en almacenes no especializados,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413522,413522,Venta de productos agropecuarios,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413524,413524,"Venta de productos textiles, de vestir, de cuero y calzado",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413526,413526,Venta de papel y cartón,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413528,413528,"Venta de libros, revistas, elementos de papelería, útiles y textos escolares",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413530,413530,"Venta de juegos, juguetes y artículos deportivos",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413532,413532,Venta de instrumentos quirúrgicos y ortopédicos,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413534,413534,Venta de artículos en relojerías y joyerías,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413536,413536,Venta de electrodomésticos y muebles,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413538,413538,"Venta de productos de aseo, farmacéuticos, medicinales, y artículos de tocador",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413540,413540,"Venta de cubiertos, vajillas, cristalería, porcelanas, cerámicas y otros artículos de uso doméstico",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413542,413542,"Venta de materiales de construcción, fontanería y calefacción",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413544,413544,Venta de pinturas y lacas,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413546,413546,Venta de productos de vidrios y marquetería,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413548,413548,Venta de herramientas y artículos de ferretría,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413550,413550,Venta de químicos,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413552,413552,"Venta de productos intermedios, desperdicios y desechos",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413554,413554,"Venta de maquinaria, equipo de oficina y programas de computador",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413556,413556,Venta de artículos en cacharrerías y misceláneas,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413558,413558,Venta de instrumentos musicales,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413560,413560,Venta de artículos en casas de empeño y prenderías,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413562,413562,Venta de equipo fotográfico,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413564,413564,Venta de equipo óptico y de precisión,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413566,413566,Venta de empaques,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413568,413568,Venta de equipo profesional y científico,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413570,413570,"Venta de loterías, rifas, chance, apuestas y similares",FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413572,413572,Reparación de efectos personales y electrodomésticos,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
co_puc_413595,413595,Venta de Otros Productos,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_413595,413595,Venta de otros productos,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
+co_puc_415505,415505,Arrendamientos de Bienes Inmuebles,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
co_puc_421005,421005,Intereses,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
co_puc_421020,421020,Diferencia en Cambio,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
co_puc_421040,421040,Descuentos Comerciales Condicionados,FALSE,account.data_account_type_revenue,l10n_co_chart_template_generic
diff --git a/addons/l10n_co/data/account.tax.template.csv b/addons/l10n_co/data/account.tax.template.csv
index de0f14ca931..cf435a1232c 100644
--- a/addons/l10n_co/data/account.tax.template.csv
+++ b/addons/l10n_co/data/account.tax.template.csv
@@ -1,54 +1,54 @@
-id,amount,account_id:id,refund_account_id:id,name,type_tax_use,amount_type,children_tax_ids:id,chart_template_id:id
-l10n_co_tax_0,5.0,co_puc_2408100510,co_puc_2408101010,IVA Compra 5%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_1,19.0,co_puc_2408100505,co_puc_2408101005,IVA Compra 19%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_2,16.0,co_puc_2408100515,co_puc_2408101015,IVA Descontable Compra 16% (2016 y Enero 2017),purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_3,0.0,co_puc_236505,co_puc_236505,RteFte Salarios y Pagos Laborales,None,fixed,,l10n_co_chart_template_generic
-l10n_co_tax_4,16.0,,,IVA Devoluciones Ventas 16% 2016,sale,percent,,l10n_co_chart_template_generic
-l10n_co_tax_5,0.0,,,IVA Compra Importaciones,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_6,0.0,,,IVA Compra Excluido,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_7,0.0,,,IVA Compra Exento,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_8,19.0,co_puc_2408050505,co_puc_2408051005,IVA Ventas 19%,sale,percent,,l10n_co_chart_template_generic
-l10n_co_tax_9,5.0,co_puc_2408050510,co_puc_2408051010,IVA Ventas 5%,sale,percent,,l10n_co_chart_template_generic
-l10n_co_tax_10,0.0,,,IVA Excento,sale,percent,,l10n_co_chart_template_generic
-l10n_co_tax_11,0.0,,,IVA Excluido,sale,percent,,l10n_co_chart_template_generic
-l10n_co_tax_12,-2.85,co_puc_236705,co_puc_236705,RteIVA 15% sobre el 19% IVA,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_13,-0.75,co_puc_236705,co_puc_236705,RteIVA 15% sobre el 5% IVA,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_14,0.75,,,RteIVA 15% sobre el 5% IVA Contrapartida,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_15,2.85,,,RteIVA 15% sobre el 19% IVA Contrapartida,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_16,-0.1,,,RteFte Compra Combustibles 0.1%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_17,-0.5,,,RteFte Compra Cafe 0.5%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_18,-10.0,,,RteFte Comisiones Persona Natural 10%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_19,-2.5,,,RteFte Compras Declarantes 2.5%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_20,-10.0,,,"RteFte Consultorias, Servicios Tecnicos y Asistencia Tecnica Pagos al Exterior 10%",purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_21,-1.0,,,RteFte Adquisicion de Vehiculos 1%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_22,-1.0,,,RteFte Compras de Bienes Raices 1%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_23,-4.0,,,RteFte Servicios P Jurídicas y PN Declarantes 4%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_24,-1.0,,,RteFte Servicio Transporte Nacional Carga 1%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_25,-6.0,,,RteFte Servicios en General Personas Naturales No Declarantes Renta 6%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_26,-2.0,,,RteFte Servicio de Vigilancia y Aseo (Sobre AIU) 2%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_27,-3.5,,,RteFte Honorarios Servicios Licencias Software 3.5%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_28,-2.0,,,RteFte Contratos de Obra Inmuebles 2%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_29,-4.0,,,RteFte Arrendamientos Bienes Muebles 4%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_30,-3.5,,,RteFte Arrendamientos Bienes Inmuebles 3.5%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_31,-7.0,,,RteFte Rendimientos Financieros Generales 7%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_32,-10.0,,,RteFte Honorarios Persona Natural 10%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_33,-3.5,,,RteFte Honorarios Diseno Web y Consultoria Informatica 3.5%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_34,-2.5,,,RteFte Adquisicion de Bienes Raices Comerciales 2.5%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_35,-3.5,,,RteFte Compras No Declarantes 3.5%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_36,-11.0,,,RteFte Comisiones Persona Juridica 11%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_37,-1.5,,,RteFte Compras Bienes Agricolas 1.5%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_38,-3.5,,,RteFte Servicio Transporte Terrestre Nacional Pasajeros 3.5%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_39,-1.0,,,RteFte Servicio Temporales de Empleo (Sobre AIU) 1%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_40,-11.0,,,RteFte Honorarios Persona Juridica 11%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_41,-2.0,,,RteFte Servicio Integrales de Salud 2%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_42,-3.5,,,RteFte Servicio de Hoteles y Restaurantes 3.5%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_43,0.0,co_puc_236805,co_puc_236805,RteICA,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_44,-0.69,co_puc_236805,co_puc_236805,RteICA 0.69%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_45,-1.104,co_puc_236805,co_puc_236805,RteICA 1.104%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_46,-0.414,co_puc_236805,co_puc_236805,RteICA 0.414%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_47,-1.38,co_puc_236805,co_puc_236805,RteICA 1.38%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_48,-0.966,co_puc_236805,co_puc_236805,RteICA 0.966%,purchase,percent,,l10n_co_chart_template_generic
-l10n_co_tax_49,16.0,,,IVA Compra 16% RC,purchase,group,"l10n_co_tax_1,l10n_co_tax_12",l10n_co_chart_template_generic
-l10n_co_tax_50,16.0,,,IVA Compra 16% RS,purchase,group,"l10n_co_tax_12,l10n_co_tax_15",l10n_co_chart_template_generic
-l10n_co_tax_51,5.0,,,IVA Compra 5% RS,purchase,group,"l10n_co_tax_13,l10n_co_tax_14",l10n_co_chart_template_generic
-l10n_co_tax_52,5.0,,,IVA Compra 5% RC,purchase,group,"l10n_co_tax_0,l10n_co_tax_13",l10n_co_chart_template_generic
+id,sequence,amount,account_id:id,refund_account_id:id,name,type_tax_use,amount_type,children_tax_ids:id,chart_template_id:id
+l10n_co_tax_0,1,5.0,co_puc_2408100510,co_puc_2408101010,IVA Compra 5%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_1,0,19.0,co_puc_2408100505,co_puc_2408101005,IVA Compra 19%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_2,1,16.0,co_puc_2408100515,co_puc_2408101015,IVA Descontable Compra 16% (2016 y Enero 2017),purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_3,1,0.0,co_puc_236505,co_puc_236505,RteFte Salarios y Pagos Laborales,None,fixed,,l10n_co_chart_template_generic
+l10n_co_tax_4,1,16.0,,,IVA Devoluciones Ventas 16% 2016,sale,percent,,l10n_co_chart_template_generic
+l10n_co_tax_5,1,0.0,,,IVA Compra Importaciones,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_6,1,0.0,,,IVA Compra Excluido,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_7,1,0.0,,,IVA Compra Exento,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_8,0,19.0,co_puc_2408050505,co_puc_2408051005,IVA Ventas 19%,sale,percent,,l10n_co_chart_template_generic
+l10n_co_tax_9,1,5.0,co_puc_2408050510,co_puc_2408051010,IVA Ventas 5%,sale,percent,,l10n_co_chart_template_generic
+l10n_co_tax_10,1,0.0,,,IVA Excento,sale,percent,,l10n_co_chart_template_generic
+l10n_co_tax_11,1,0.0,,,IVA Excluido,sale,percent,,l10n_co_chart_template_generic
+l10n_co_tax_12,1,-2.85,co_puc_236705,co_puc_236705,RteIVA 15% sobre el 19% IVA,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_13,1,-0.75,co_puc_236705,co_puc_236705,RteIVA 15% sobre el 5% IVA,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_14,1,0.75,co_puc_24082005,co_puc_24082005,RteIVA 15% sobre el 5% IVA Contrapartida,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_15,1,2.85,co_puc_24082005,co_puc_24082005,RteIVA 15% sobre el 19% IVA Contrapartida,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_16,1,-0.1,co_puc_23654025,co_puc_23654025,RteFte Compra Combustibles 0.1%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_17,1,-0.5,co_puc_23654020,co_puc_23654020,RteFte Compra Cafe 0.5%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_18,1,-10.0,co_puc_23652005,co_puc_23652005,RteFte Comisiones Persona Natural 10%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_19,1,-2.5,co_puc_23654005,co_puc_23654005,RteFte Compras Declarantes 2.5%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_20,1,-10.0,co_puc_23655005,co_puc_23655005,"RteFte Consultorias, Servicios Tecnicos y Asistencia Tecnica Pagos al Exterior 10%",purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_21,1,-1.0,co_puc_23654030,co_puc_23654030,RteFte Adquisicion de Vehiculos 1%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_22,1,-1.0,co_puc_23654040,co_puc_23654040,RteFte Compras de Bienes Raices 1%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_23,1,-4.0,co_puc_23652505,co_puc_23652505,RteFte Servicios P Jurídicas y PN Declarantes 4%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_24,1,-1.0,co_puc_23652515,co_puc_23652515,RteFte Servicio Transporte Nacional Carga 1%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_25,1,-6.0,co_puc_23652510,co_puc_23652510,RteFte Servicios en General Personas Naturales No Declarantes Renta 6%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_26,1,-2.0,co_puc_23652535,co_puc_23652535,RteFte Servicio de Vigilancia y Aseo (Sobre AIU) 2%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_27,1,-3.5,co_puc_23651515,co_puc_23651515,RteFte Honorarios Servicios Licencias Software 3.5%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_28,1,-2.0,co_puc_23652550,co_puc_23652550,RteFte Contratos de Obra Inmuebles 2%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_29,1,-4.0,co_puc_23653005,co_puc_23653005,RteFte Arrendamientos Bienes Muebles 4%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_30,1,-3.5,co_puc_23653010,co_puc_23653010,RteFte Arrendamientos Bienes Inmuebles 3.5%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_31,1,-7.0,co_puc_23653510,co_puc_23653510,RteFte Rendimientos Financieros Generales 7%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_32,1,-10.0,co_puc_23651505,co_puc_23651505,RteFte Honorarios Persona Natural 10%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_33,1,-3.5,co_puc_23651520,co_puc_23651520,RteFte Honorarios Diseno Web y Consultoria Informatica 3.5%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_34,1,-2.5,co_puc_23654035,co_puc_23654035,RteFte Adquisicion de Bienes Raices Comerciales 2.5%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_35,1,-3.5,co_puc_23654010,co_puc_23654010,RteFte Compras No Declarantes 3.5%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_36,1,-11.0,co_puc_23652010,co_puc_23652010,RteFte Comisiones Persona Juridica 11%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_37,1,-1.5,co_puc_23654015,co_puc_23654015,RteFte Compras Bienes Agricolas 1.5%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_38,1,-3.5,co_puc_23652520,co_puc_23652520,RteFte Servicio Transporte Terrestre Nacional Pasajeros 3.5%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_39,1,-1.0,co_puc_23652530,co_puc_23652530,RteFte Servicio Temporales de Empleo (Sobre AIU) 1%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_40,1,-11.0,co_puc_23651510,co_puc_23651510,RteFte Honorarios Persona Juridica 11%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_41,1,-2.0,co_puc_23652540,co_puc_23652540,RteFte Servicio Integrales de Salud 2%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_42,1,-3.5,co_puc_23652545,co_puc_23652545,RteFte Servicio de Hoteles y Restaurantes 3.5%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_43,1,0.0,co_puc_236805,co_puc_236805,RteICA,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_44,1,-0.69,co_puc_236805,co_puc_236805,RteICA 0.69%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_45,1,-1.104,co_puc_236805,co_puc_236805,RteICA 1.104%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_46,1,-0.414,co_puc_236805,co_puc_236805,RteICA 0.414%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_47,1,-1.38,co_puc_236805,co_puc_236805,RteICA 1.38%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_48,1,-0.966,co_puc_236805,co_puc_236805,RteICA 0.966%,purchase,percent,,l10n_co_chart_template_generic
+l10n_co_tax_49,1,19.0,,,IVA Compra 19% RC,purchase,group,"l10n_co_tax_1,l10n_co_tax_12",l10n_co_chart_template_generic
+l10n_co_tax_50,1,19.0,,,IVA Compra 19% RS,purchase,group,"l10n_co_tax_12,l10n_co_tax_15",l10n_co_chart_template_generic
+l10n_co_tax_51,1,5.0,,,IVA Compra 5% RS,purchase,group,"l10n_co_tax_13,l10n_co_tax_14",l10n_co_chart_template_generic
+l10n_co_tax_52,1,5.0,,,IVA Compra 5% RC,purchase,group,"l10n_co_tax_0,l10n_co_tax_13",l10n_co_chart_template_generic
diff --git a/addons/l10n_co/models/res_partner.py b/addons/l10n_co/models/res_partner.py
index ab7ff0c3c24..b2a23a08f9d 100644
--- a/addons/l10n_co/models/res_partner.py
+++ b/addons/l10n_co/models/res_partner.py
@@ -12,7 +12,7 @@ class ResPartner(models.Model):
('foreign_id_card', 'Cedula de Extranjeria'),
('external_id', 'ID del Exterior')], string='Document Type',
help='Indicates to what document the information in here belongs to.')
- l10n_co_verification_code = fields.Char(compute='_compute_verification_code', string='VC',
+ l10n_co_verification_code = fields.Char(compute='_compute_verification_code', string='VC', # todo remove this field in master
help='Redundancy check to verify the vat number has been typed in correctly.')
@api.depends('vat')
@@ -40,3 +40,16 @@ class ResPartner(models.Model):
partner.l10n_co_verification_code = 11 - number
except ValueError:
partner.l10n_co_verification_code = False
+
+ @api.constrains('vat', 'commercial_partner_country_id', 'l10n_co_document_type')
+ def check_vat(self):
+ # check_vat is implemented by base_vat which this localization
+ # doesn't directly depend on. It is however automatically
+ # installed for Colombia.
+ if self.sudo().env.ref('base.module_base_vat').state == 'installed':
+ # don't check Colombian partners unless they have RUT (= Colombian VAT) set as document type
+ self = self.filtered(lambda partner: partner.country_id != self.env.ref('base.co') or\
+ partner.l10n_co_document_type == 'rut')
+ return super(ResPartner, self).check_vat()
+ else:
+ return True
diff --git a/addons/l10n_co/views/res_partner.xml b/addons/l10n_co/views/res_partner.xml
index 9912521fcca..5c55043fb44 100644
--- a/addons/l10n_co/views/res_partner.xml
+++ b/addons/l10n_co/views/res_partner.xml
@@ -9,7 +9,7 @@
<field name="l10n_co_document_type"/>
</field>
<field name="vat" position="after">
- <field name="l10n_co_verification_code"/>
+ <field name="l10n_co_verification_code" invisible="1"/>
</field>
</field>
</record>
diff --git a/addons/mail/models/mail_message.py b/addons/mail/models/mail_message.py
index 9fa6701be16..45d07d5b97c 100644
--- a/addons/mail/models/mail_message.py
+++ b/addons/mail/models/mail_message.py
@@ -757,9 +757,10 @@ class Message(models.Model):
'datas_fname': name,
'res_model': 'mail.message',
})
+ attachment.generate_access_token()
values['attachment_ids'].append((4, attachment.id))
- data_to_url[key] = '/web/image/%s' % attachment.id
- return '%s%s alt="%s"' % (data_to_url[key], match.group(3), name)
+ data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name]
+ return '%s%s alt="%s"' % (data_to_url[key][0], match.group(3), data_to_url[key][1])
values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body']))
# delegate creation of tracking after the create as sudo to avoid access rights issues
diff --git a/addons/mail/static/src/js/systray.js b/addons/mail/static/src/js/systray.js
index 2006ae297ef..edd9cd3f56c 100644
--- a/addons/mail/static/src/js/systray.js
+++ b/addons/mail/static/src/js/systray.js
@@ -126,7 +126,7 @@ var MessagingMenu = Widget.extend({
this.do_action({
type: 'ir.actions.act_window',
res_model: resModel,
- views: [[false, 'form'], [false, 'kanban']],
+ views: [[false, 'form']],
res_id: resID
});
} else {
diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js
index 2c22dcfa0f4..a9395ea7bd6 100644
--- a/addons/payment_stripe/static/src/js/stripe.js
+++ b/addons/payment_stripe/static/src/js/stripe.js
@@ -148,15 +148,12 @@ odoo.define('payment_stripe.stripe', function(require) {
});
}
create_tx.done(function () {
- if (!_.contains(int_currencies, currency)) {
- amount = amount*100;
- }
getStripeHandler().open({
name: $("input[name='merchant']").val(),
description: $("input[name='invoice_num']").val(),
email: $("input[name='email']").val(),
currency: currency,
- amount: amount,
+ amount: _.contains(int_currencies, currency) ? amount : amount * 100,
});
});
}
diff --git a/addons/phone_validation/models/phone_validation_mixin.py b/addons/phone_validation/models/phone_validation_mixin.py
index 1003ab929f2..4cd9d8eb2bb 100644
--- a/addons/phone_validation/models/phone_validation_mixin.py
+++ b/addons/phone_validation/models/phone_validation_mixin.py
@@ -11,20 +11,21 @@ class PhoneValidationMixin(models.AbstractModel):
def _phone_get_country(self):
if 'country_id' in self:
return self.country_id
- return self.env.user.company_id.country_id
def _phone_get_always_international(self):
- if 'company_id' in self:
+ if 'company_id' in self and self.country_id:
return self.company_id.phone_international_format == 'prefix'
return self.env.user.company_id.phone_international_format == 'prefix'
def phone_format(self, number, country=None, company=None):
country = country or self._phone_get_country()
+ if not country:
+ return number
always_international = company.phone_international_format == 'prefix' if company else self._phone_get_always_international()
return phone_validation.phone_format(
number,
country.code if country else None,
country.phone_code if country else None,
always_international=always_international,
- raise_exception=True
+ raise_exception=False
)
diff --git a/addons/phone_validation/tools/phone_validation.py b/addons/phone_validation/tools/phone_validation.py
index 940c4300f26..7ca7bed6ea8 100644
--- a/addons/phone_validation/tools/phone_validation.py
+++ b/addons/phone_validation/tools/phone_validation.py
@@ -41,7 +41,7 @@ try:
if raise_exception:
raise
else:
- _logger.warning(_('Unable to format %s:\n%s') % number, e)
+ _logger.warning(_('Unable to format %s:\n%s'), number, e)
return number
if always_international or phone_nbr.country_code != country_phone_code:
phone_fmt = phonenumbers.PhoneNumberFormat.INTERNATIONAL
diff --git a/addons/point_of_sale/static/src/js/screens.js b/addons/point_of_sale/static/src/js/screens.js
index 61e72258997..4e53d259248 100644
--- a/addons/point_of_sale/static/src/js/screens.js
+++ b/addons/point_of_sale/static/src/js/screens.js
@@ -2038,6 +2038,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
invoiced.fail(function(error){
self.invoicing = false;
+ order.finalized = false;
if (error.message === 'Missing Customer') {
self.gui.show_popup('confirm',{
'title': _t('Please select the Customer'),
diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index 1a1e65d9611..1aee90fc83c 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -909,22 +909,8 @@ class ProcurementRule(models.Model):
for line in po.order_line:
if line.product_id == product_id and line.product_uom == product_id.uom_po_id:
if line._merge_in_existing_line(product_id, product_qty, product_uom, location_id, name, origin, values):
- procurement_uom_po_qty = product_uom._compute_quantity(product_qty, product_id.uom_po_id)
- seller = product_id._select_seller(
- partner_id=partner,
- quantity=line.product_qty + procurement_uom_po_qty,
- date=po.date_order and po.date_order[:10],
- uom_id=product_id.uom_po_id)
-
- price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, line.product_id.supplier_taxes_id, line.taxes_id, values['company_id']) if seller else 0.0
- if price_unit and seller and po.currency_id and seller.currency_id != po.currency_id:
- price_unit = seller.currency_id.compute(price_unit, po.currency_id)
-
- po_line = line.write({
- 'product_qty': line.product_qty + procurement_uom_po_qty,
- 'price_unit': price_unit,
- 'move_dest_ids': [(4, x.id) for x in values.get('move_dest_ids', [])]
- })
+ vals = self._update_purchase_order_line(product_id, product_qty, product_uom, values, line, partner)
+ po_line = line.write(vals)
break
if not po_line:
vals = self._prepare_purchase_order_line(product_id, product_qty, product_uom, values, po, supplier)
@@ -948,6 +934,24 @@ class ProcurementRule(models.Model):
return schedule_date - relativedelta(days=int(seller.delay))
+ def _update_purchase_order_line(self, product_id, product_qty, product_uom, values, line, partner):
+ procurement_uom_po_qty = product_uom._compute_quantity(product_qty, product_id.uom_po_id)
+ seller = product_id._select_seller(
+ partner_id=partner,
+ quantity=line.product_qty + procurement_uom_po_qty,
+ date=line.order_id.date_order and line.order_id.date_order[:10],
+ uom_id=product_id.uom_po_id)
+
+ price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, line.product_id.supplier_taxes_id, line.taxes_id, values['company_id']) if seller else 0.0
+ if price_unit and seller and line.order_id.currency_id and seller.currency_id != line.order_id.currency_id:
+ price_unit = seller.currency_id.compute(price_unit, line.order_id.currency_id)
+
+ return {
+ 'product_qty': line.product_qty + procurement_uom_po_qty,
+ 'price_unit': price_unit,
+ 'move_dest_ids': [(4, x.id) for x in values.get('move_dest_ids', [])]
+ }
+
@api.multi
def _prepare_purchase_order_line(self, product_id, product_qty, product_uom, values, po, supplier):
procurement_uom_po_qty = product_uom._compute_quantity(product_qty, product_id.uom_po_id)
diff --git a/addons/sale/controllers/portal.py b/addons/sale/controllers/portal.py
index 4ea97383cc9..c33e7b79860 100644
--- a/addons/sale/controllers/portal.py
+++ b/addons/sale/controllers/portal.py
@@ -57,7 +57,7 @@ class CustomerPortal(CustomerPortal):
if access_token:
values['no_breadcrumbs'] = True
values['access_token'] = access_token
- values['portal_confirmation'] = request.env['ir.config_parameter'].sudo().get_param('sale.sale_portal_confirmation_options', default='none')
+ values['portal_confirmation'] = order.get_portal_confirmation_action()
if kwargs.get('error'):
values['error'] = kwargs['error']
diff --git a/addons/sale_payment/views/sale_portal_templates.xml b/addons/sale_payment/views/sale_portal_templates.xml
index e8c5afab5db..06b3f402351 100644
--- a/addons/sale_payment/views/sale_portal_templates.xml
+++ b/addons/sale_payment/views/sale_portal_templates.xml
@@ -31,7 +31,7 @@
<template id="portal_my_orders_payment" name="Payment on my orders" inherit_id="sale.portal_order_page">
<xpath expr="//t[@name='portal_confirmation_sign']" position="after">
- <t t-if="not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and order.payment_tx_id.state != 'done'" name="portal_confirmation_pay">
+ <t t-if="order.amount_total and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and order.payment_tx_id.state != 'done'" name="portal_confirmation_pay">
<a class="btn btn-primary ml8 pull-right" href="#portal_pay">
<i class="fa fa-arrow-circle-right"/> Pay Now
</a>
@@ -49,7 +49,7 @@
</div>
</xpath>
<xpath expr="//div[@id='portal_sign']" position="after">
- <div class="row" t-if="not invoices and order.state in ('sent') and portal_confirmation == 'pay'" id="portal_pay">
+ <div class="row" t-if="order.amount_total and not invoices and order.state in ('sent', 'sale') and portal_confirmation == 'pay' and order.payment_tx_id.state != 'done'" id="portal_pay">
<div class="col-md-offset-3 col-md-6">
<div t-if="pms or s2s_acquirers or form_acquirers" id="payment_method" class="text-left col-md-13">
<h4 class="mb24">Pay with</h4>
diff --git a/addons/sale_timesheet/models/account.py b/addons/sale_timesheet/models/account.py
index 417fb1950d1..a0d348849eb 100644
--- a/addons/sale_timesheet/models/account.py
+++ b/addons/sale_timesheet/models/account.py
@@ -70,6 +70,13 @@ class AccountAnalyticLine(models.Model):
@api.multi
def _timesheet_compute_theorical_revenue(self):
+ for timesheet in self:
+ values = timesheet._timesheet_compute_theorical_revenue_values()
+ timesheet.write(values)
+ return True
+
+ @api.multi
+ def _timesheet_compute_theorical_revenue_values(self):
""" This method set the theorical revenue on the current timesheet lines.
If invoice on delivered quantity:
@@ -82,53 +89,55 @@ class AccountAnalyticLine(models.Model):
else:
0
"""
- for timesheet in self:
- # find the timesheet UoM
- timesheet_uom = timesheet.product_uom_id
- if not timesheet_uom: # fallback on default company timesheet UoM
- timesheet_uom = self.env.user.company_id.project_time_mode_id
-
- # default values
- unit_amount = timesheet.unit_amount
- so_line = timesheet.so_line
- values = {
- 'timesheet_revenue': 0.0,
- 'timesheet_invoice_type': 'non_billable_project' if not timesheet.task_id else 'non_billable',
- }
- # set the revenue and billable type according to the product and the SO line
- if timesheet.task_id and so_line.product_id.type == 'service':
- # find the analytic account to convert revenue into its currency
- analytic_account = timesheet.account_id
- # convert the unit of mesure into hours
- sale_price_hour = so_line.product_uom._compute_price(so_line.price_unit, timesheet_uom)
- sale_price = so_line.currency_id.compute(sale_price_hour, analytic_account.currency_id) # amount from SO should be convert into analytic account currency
-
- # calculate the revenue on the timesheet
- if so_line.product_id.invoice_policy == 'delivery':
- values['timesheet_revenue'] = analytic_account.currency_id.round(unit_amount * sale_price * (1-(so_line.discount/100)))
- values['timesheet_invoice_type'] = 'billable_time' if so_line.product_id.service_type == 'timesheet' else 'billable_fixed'
- elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.service_type == 'timesheet':
- quantity_hour = unit_amount
- if so_line.product_uom.category_id == timesheet_uom.category_id:
- quantity_hour = so_line.product_uom._compute_quantity(so_line.product_uom_qty, timesheet_uom)
- # compute the total revenue the SO since we are in fixed price
- total_revenue_so = analytic_account.currency_id.round(quantity_hour * sale_price * (1-(so_line.discount/100)))
- # compute the total revenue already existing (without the current timesheet line)
- domain = [('so_line', '=', so_line.id)]
- if timesheet.ids:
- domain += [('id', 'not in', timesheet.ids)]
- analytic_lines = timesheet.search(domain)
- total_revenue_invoiced = sum(analytic_lines.mapped('timesheet_revenue'))
- # compute (new) revenue of current timesheet line
- values['timesheet_revenue'] = min(
- analytic_account.currency_id.round(unit_amount * so_line.currency_id.compute(so_line.price_unit, analytic_account.currency_id) * (1-so_line.discount)),
- total_revenue_so - total_revenue_invoiced
- )
- values['timesheet_invoice_type'] = 'billable_fixed'
- # if the so line is already invoiced, and the delivered qty is still smaller than the ordered, then link the timesheet to the invoice
- if so_line.invoice_status == 'invoiced':
- values['timesheet_invoice_id'] = so_line.invoice_lines[0].invoice_id.id
- elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.service_type != 'timesheet':
- values['timesheet_invoice_type'] = 'billable_fixed'
- timesheet.write(values)
- return True
+ self.ensure_one()
+ timesheet = self
+
+ # find the timesheet UoM
+ timesheet_uom = timesheet.product_uom_id
+ if not timesheet_uom: # fallback on default company timesheet UoM
+ timesheet_uom = self.env.user.company_id.project_time_mode_id
+
+ # default values
+ unit_amount = timesheet.unit_amount
+ so_line = timesheet.so_line
+ values = {
+ 'timesheet_revenue': 0.0,
+ 'timesheet_invoice_type': 'non_billable_project' if not timesheet.task_id else 'non_billable',
+ }
+ # set the revenue and billable type according to the product and the SO line
+ if timesheet.task_id and so_line.product_id.type == 'service':
+ # find the analytic account to convert revenue into its currency
+ analytic_account = timesheet.account_id
+ # convert the unit of mesure into hours
+ sale_price_hour = so_line.product_uom._compute_price(so_line.price_unit, timesheet_uom)
+ sale_price = so_line.currency_id.compute(sale_price_hour, analytic_account.currency_id) # amount from SO should be convert into analytic account currency
+
+ # calculate the revenue on the timesheet
+ if so_line.product_id.invoice_policy == 'delivery':
+ values['timesheet_revenue'] = analytic_account.currency_id.round(unit_amount * sale_price * (1-(so_line.discount/100)))
+ values['timesheet_invoice_type'] = 'billable_time' if so_line.product_id.service_type == 'timesheet' else 'billable_fixed'
+ elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.service_type == 'timesheet':
+ quantity_hour = unit_amount
+ if so_line.product_uom.category_id == timesheet_uom.category_id:
+ quantity_hour = so_line.product_uom._compute_quantity(so_line.product_uom_qty, timesheet_uom)
+ # compute the total revenue the SO since we are in fixed price
+ total_revenue_so = analytic_account.currency_id.round(quantity_hour * sale_price * (1-(so_line.discount/100)))
+ # compute the total revenue already existing (without the current timesheet line)
+ domain = [('so_line', '=', so_line.id)]
+ if timesheet.ids:
+ domain += [('id', 'not in', timesheet.ids)]
+ analytic_lines = timesheet.search(domain)
+ total_revenue_invoiced = sum(analytic_lines.mapped('timesheet_revenue'))
+ # compute (new) revenue of current timesheet line
+ values['timesheet_revenue'] = min(
+ analytic_account.currency_id.round(unit_amount * so_line.currency_id.compute(so_line.price_unit, analytic_account.currency_id) * (1-so_line.discount)),
+ total_revenue_so - total_revenue_invoiced
+ )
+ values['timesheet_invoice_type'] = 'billable_fixed'
+ # if the so line is already invoiced, and the delivered qty is still smaller than the ordered, then link the timesheet to the invoice
+ if so_line.invoice_status == 'invoiced':
+ values['timesheet_invoice_id'] = so_line.invoice_lines[0].invoice_id.id
+ elif so_line.product_id.invoice_policy == 'order' and so_line.product_id.service_type != 'timesheet':
+ values['timesheet_invoice_type'] = 'billable_fixed'
+
+ return values
diff --git a/addons/sms/__manifest__.py b/addons/sms/__manifest__.py
index 18330c1d793..c47ed128a8a 100644
--- a/addons/sms/__manifest__.py
+++ b/addons/sms/__manifest__.py
@@ -14,6 +14,10 @@ The service is provided by the In App Purchase Odoo platform.
'data': [
'wizard/send_sms_views.xml',
'views/res_partner_views.xml',
+ 'views/templates.xml',
+ ],
+ 'qweb': [
+ 'static/src/xml/sms_widget.xml',
],
'installable': True,
'auto_install': True,
diff --git a/addons/sms/static/src/js/sms_widget.js b/addons/sms/static/src/js/sms_widget.js
new file mode 100644
index 00000000000..a01ff3753cf
--- /dev/null
+++ b/addons/sms/static/src/js/sms_widget.js
@@ -0,0 +1,120 @@
+odoo.define('sms.sms_widget', function (require) {
+"use strict";
+
+var basicFields = require('web.basic_fields');
+var core = require('web.core');
+var fieldRegistry = require('web.field_registry');
+var framework = require('web.framework');
+
+var FieldText = basicFields.FieldText;
+var QWeb = core.qweb;
+
+/**
+ * SmsWidget is a widget to display a textarea (the body) and a text representing
+ * the number of SMS and the number of characters. This text is computed every
+ * time the user changes the body.
+ */
+var SmsWidget = FieldText.extend({
+ /**
+ * @constructor
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.nbrChar = 0;
+ this.nbrSMS = 0;
+ this.encoding = "GSM7";
+ this.tagName = 'div';
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Compute the number of characters and sms
+ * @private
+ */
+ _compute: function () {
+ var content = this._getValue();
+ this.encoding = this._extractEncoding(content);
+ this.nbrChar = content.length;
+ this.nbrChar += (content.match(/\n/g) || []).length;
+ this.nbrSMS = this._countSMS(this.nbrChar, this.encoding);
+ this._renderSMS();
+ },
+ /**
+ * Count the number of SMS of the content
+ * @private
+ * @returns {integer} Number of SMS
+ */
+ _countSMS: function () {
+ if (this.nbrChar === 0) {
+ return 0;
+ }
+ if (this.encoding === 'UNICODE') {
+ if (this.nbrChar <= 70) {
+ return 1;
+ }
+ return Math.ceil(this.nbrChar / 67);
+ }
+ if (this.nbrChar <= 160) {
+ return 1;
+ }
+ return Math.ceil(this.nbrChar / 153);
+ },
+ /**
+ * @private
+ * @override
+ */
+ _renderEdit: function () {
+ this.$el.empty();
+ this._prepareInput($('<textarea/>')).appendTo(this.$el);
+ this.$el.append($(QWeb.render("sms.sms_count", {})));
+ this._compute();
+ },
+ /**
+ * Extract the encoding depending on the characters in the content
+ * @private
+ * @param {String} content Content of the SMS
+ * @returns {String} Encoding of the content (GSM7 or UNICODE)
+ */
+ _extractEncoding: function (content) {
+ if (String(content).match(RegExp("^[@£$¥èéùìòÇ\\nØø\\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !\\\"#¤%&'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà]*$"))) {
+ return 'GSM7';
+ }
+ return 'UNICODE';
+ },
+ /**
+ * Render the number of characters, sms and the encoding.
+ * @private
+ */
+ _renderSMS: function () {
+ this.$('.o_sms_count').text(this.nbrChar + ' / ' + this.nbrSMS + ' SMS (' + this.encoding + ') ');
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ * @private
+ */
+ _onChange: function () {
+ this._super.apply(this, arguments);
+ this._compute();
+ },
+ /**
+ * @override
+ * @private
+ */
+ _onInput: function () {
+ this._super.apply(this, arguments);
+ this._compute();
+ },
+});
+
+fieldRegistry.add('sms_widget', SmsWidget);
+
+return SmsWidget;
+});
diff --git a/addons/sms/static/src/tests/sms_widget_test.js b/addons/sms/static/src/tests/sms_widget_test.js
new file mode 100644
index 00000000000..b346c5a6f98
--- /dev/null
+++ b/addons/sms/static/src/tests/sms_widget_test.js
@@ -0,0 +1,67 @@
+odoo.define('sms.sms_widget_tests', function (require) {
+"use strict";
+
+var FormView = require('web.FormView');
+var testUtils = require('web.test_utils');
+
+var createView = testUtils.createView;
+
+QUnit.module('fields', {
+ beforeEach: function () {
+ this.data = {
+ partner: {
+ fields: {
+ message: {string: "message", type: "text"},
+ },
+ records: [{
+ id: 1,
+ message: "",
+ }]
+ },
+ };
+ }
+}, function () {
+
+ QUnit.module('SmsWidget');
+
+ QUnit.test('Sms widgets are correctly rendered', function (assert) {
+ assert.expect(9);
+ var form = createView({
+ View: FormView,
+ model: 'partner',
+ data: this.data,
+ arch: '<form><sheet><field name="message" widget="sms_widget"/></sheet></form>',
+ });
+ form.$buttons.find('.o_form_button_edit').click();
+ assert.ok(form.$('.o_sms_count').length, "Should have a sms counter");
+ assert.strictEqual(form.$('.o_sms_count').text(), '0 / 0 SMS (GSM7) ', 'Should be "0 / 0 SMS (GSM7) " by default');
+ // GSM-7
+ form.$('.o_input').val("Hello from Odoo").trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '15 / 1 SMS (GSM7) ', 'Should be "15 / 1 SMS (GSM7) " for "Hello from Odoo"');
+ // GSM-7 with \n => this one count as 2 characters
+ form.$('.o_input').val("Hello from Odoo\n").trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '17 / 1 SMS (GSM7) ', 'Should be "17 / 1 SMS (GSM7) " for "Hello from Odoo\\n"');
+ // Unicode => ê
+ form.$('.o_input').val("Hêllo from Odoo").trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '15 / 1 SMS (UNICODE) ', 'Should be "15 / 1 SMS (UNICODE) " for "Hêllo from Odoo"');
+ // GSM-7 with 160c
+ var text = Array(161).join('a');
+ form.$('.o_input').val(text).trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '160 / 1 SMS (GSM7) ', 'Should be "160 / 1 SMS (GSM7) " for 160 x "a"');
+ // GSM-7 with 161c
+ text = Array(162).join('a');
+ form.$('.o_input').val(text).trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '161 / 2 SMS (GSM7) ', 'Should be "161 / 2 SMS (GSM7) " for 161 x "a"');
+ // Unicode with 70c
+ text = Array(71).join('ê');
+ form.$('.o_input').val(text).trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '70 / 1 SMS (UNICODE) ', 'Should be "70 / 1 SMS (UNICODE) " for 70 x "ê"');
+ // Unicode with 71c
+ text = Array(72).join('ê');
+ form.$('.o_input').val(text).trigger('input');
+ assert.strictEqual(form.$('.o_sms_count').text(), '71 / 2 SMS (UNICODE) ', 'Should be "71 / 2 SMS (UNICODE) " for 71 x "ê"');
+
+ form.destroy();
+ });
+});
+});
diff --git a/addons/sms/static/src/xml/sms_widget.xml b/addons/sms/static/src/xml/sms_widget.xml
new file mode 100644
index 00000000000..9969cf2b3c1
--- /dev/null
+++ b/addons/sms/static/src/xml/sms_widget.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<templates>
+ <t t-name="sms.sms_count">
+ <span class="pull-right"><span class="text-muted o_sms_count"></span> <a href="https://iap-services.odoo.com/iap/sms/pricing" class="fa fa-lg fa-info"/></span>
+ </t>
+</templates>
diff --git a/addons/sms/views/templates.xml b/addons/sms/views/templates.xml
new file mode 100644
index 00000000000..15bf5fc1454
--- /dev/null
+++ b/addons/sms/views/templates.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+ <data>
+ <template id="assets_backend" name="sms_assets" inherit_id="web.assets_backend">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/sms/static/src/js/sms_widget.js"></script>
+ </xpath>
+ </template>
+
+ <template id="qunit_suite" name="sms_widget_tests" inherit_id="web.qunit_suite">
+ <xpath expr="." position="inside">
+ <script type="text/javascript" src="/sms/static/src/tests/sms_widget_test.js"></script>
+ </xpath>
+ </template>
+ </data>
+</odoo>
diff --git a/addons/sms/wizard/send_sms.py b/addons/sms/wizard/send_sms.py
index 139f542e597..15d284e0879 100644
--- a/addons/sms/wizard/send_sms.py
+++ b/addons/sms/wizard/send_sms.py
@@ -71,8 +71,7 @@ class SendSMS(models.TransientModel):
phone_numbers = []
no_phone_partners = []
for partner in partners:
- default_field_name = 'mobile' if partner.mobile else 'phone'
- number = self._sms_sanitization(partner, self.env.context.get('field_name') or default_field_name)
+ number = self._sms_sanitization(partner, self.env.context.get('field_name') or 'mobile')
if number:
phone_numbers.append(number)
else:
diff --git a/addons/sms/wizard/send_sms_views.xml b/addons/sms/wizard/send_sms_views.xml
index 34746c32825..165272737c3 100644
--- a/addons/sms/wizard/send_sms_views.xml
+++ b/addons/sms/wizard/send_sms_views.xml
@@ -9,7 +9,7 @@
<sheet>
<group>
<field name="recipients"/>
- <field name="message" widget="text"/>
+ <field name="message" widget="sms_widget"/>
</group>
</sheet>
<footer>
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index a93e24f84e8..db19f9d1f95 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -816,7 +816,8 @@ class StockMove(models.Model):
# Find a candidate move line to update or create a new one.
for reserved_quant, quantity in quants:
- to_update = self.move_line_ids.filtered(lambda m: m.location_id.id == reserved_quant.location_id.id and m.lot_id.id == reserved_quant.lot_id.id and m.package_id.id == reserved_quant.package_id.id and m.owner_id.id == reserved_quant.owner_id.id)
+ to_update = self.move_line_ids.filtered(lambda m: m.product_id.tracking != 'serial' and
+ m.location_id.id == reserved_quant.location_id.id and m.lot_id.id == reserved_quant.lot_id.id and m.package_id.id == reserved_quant.package_id.id and m.owner_id.id == reserved_quant.owner_id.id)
if to_update:
to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += self.product_id.uom_id._compute_quantity(quantity, self.product_uom, rounding_method='HALF-UP')
else:
diff --git a/addons/stock/models/stock_move_line.py b/addons/stock/models/stock_move_line.py
index a0692a50688..25f567a39db 100644
--- a/addons/stock/models/stock_move_line.py
+++ b/addons/stock/models/stock_move_line.py
@@ -340,8 +340,13 @@ class StockMoveLine(models.Model):
raise UserError(_('You can not delete product moves if the picking is done. You can only correct the done quantities.'))
# Unlinking a move line should unreserve.
if ml.product_id.type == 'product' and not ml.location_id.should_bypass_reservation() and not float_is_zero(ml.product_qty, precision_digits=precision):
- self.env['stock.quant']._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id,
- package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
+ try:
+ self.env['stock.quant']._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
+ except UserError:
+ if ml.lot_id:
+ self.env['stock.quant']._update_reserved_quantity(ml.product_id, ml.location_id, -ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True)
+ else:
+ raise
moves = self.mapped('move_id')
res = super(StockMoveLine, self).unlink()
if moves:
diff --git a/addons/stock/tests/test_move.py b/addons/stock/tests/test_move.py
index 0e52a87d1ca..dfa9fc1f8f7 100644
--- a/addons/stock/tests/test_move.py
+++ b/addons/stock/tests/test_move.py
@@ -606,6 +606,55 @@ class StockMove(TransactionCase):
quants = self.env['stock.quant']._gather(self.product2, self.stock_location)
self.assertEqual(len(quants), 0)
+ def test_mixed_tracking_reservation_8(self):
+ """ Send one product tracked by lot to a customer. In your stock, there are one tracked and
+ one untracked quant. Reserve the move, then edit the lot to one not present in stock. The
+ system will update the reservation and use the untracked quant. Now unreserve, no error
+ should happen
+ """
+ lot1 = self.env['stock.production.lot'].create({
+ 'name': 'lot1',
+ 'product_id': self.product2.id,
+ })
+
+ # at first, we only make the tracked quant available in stock to make sure this one is selected
+ self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1, lot_id=lot1)
+
+ # creation
+ move1 = self.env['stock.move'].create({
+ 'name': 'test_mixed_tracking_reservation_7',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product2.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 1.0,
+ })
+ move1._action_confirm()
+ move1._action_assign()
+
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(move1.move_line_ids.lot_id.id, lot1.id)
+
+ # change the lot_id to one not available in stock while an untracked quant is available
+ self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 1)
+ lot2 = self.env['stock.production.lot'].create({
+ 'name': 'lot2',
+ 'product_id': self.product2.id,
+ })
+ move1.move_line_ids.lot_id = lot2
+ self.assertEqual(move1.reserved_availability, 1.0)
+ self.assertEqual(move1.move_line_ids.lot_id.id, lot2.id)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, strict=True), 0.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot1, strict=True), 1.0)
+
+ # unreserve
+ move1._do_unreserve()
+
+ self.assertEqual(move1.reserved_availability, 0.0)
+ self.assertEqual(len(move1.move_line_ids), 0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, strict=True), 1.0)
+ self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product2, self.stock_location, lot_id=lot1, strict=True), 1.0)
+
def test_putaway_1(self):
""" Receive products from a supplier. Check that putaway rules are rightly applied on
the receipt move line.
@@ -756,6 +805,28 @@ class StockMove(TransactionCase):
self.assertEqual(customer_quants.quantity, 30)
self.assertEqual(customer_quants.reserved_quantity, 0)
+ def test_availability_5(self):
+ """ Check that rerun action assign only create new stock move
+ lines instead of adding quantity in existing one.
+ """
+ self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 2.0)
+ # move from shelf1
+ move = self.env['stock.move'].create({
+ 'name': 'test_edit_moveline_1',
+ 'location_id': self.stock_location.id,
+ 'location_dest_id': self.customer_location.id,
+ 'product_id': self.product2.id,
+ 'product_uom': self.uom_unit.id,
+ 'product_uom_qty': 4.0,
+ })
+ move._action_confirm()
+ move._action_assign()
+
+ self.env['stock.quant']._update_available_quantity(self.product2, self.stock_location, 4.0)
+ move._action_assign()
+
+ self.assertEqual(len(move.move_line_ids), 4.0)
+
def test_unreserve_1(self):
""" Check that unreserving a stock move sets the products reserved as available and
set the state back to confirmed.
diff --git a/addons/test_mail/tests/test_mail_message.py b/addons/test_mail/tests/test_mail_message.py
index 34956afe545..435dc3d9b50 100644
--- a/addons/test_mail/tests/test_mail_message.py
+++ b/addons/test_mail/tests/test_mail_message.py
@@ -111,6 +111,15 @@ class TestMessageValues(common.BaseFunctionalTest, common.MockEmails):
self.assertNotIn('mail.test', msg.message_id)
self.assertNotIn('-%d-' % self.alias_record.id, msg.message_id)
+ def test_mail_message_base64_image(self):
+ msg = self.env['mail.message'].sudo(self.user_employee).create({
+ 'body': 'taratata <img src="data:image/png;base64,iV/+OkI=" width="2"> <img src="data:image/png;base64,iV/+OkI=" width="2">',
+ })
+ self.assertEqual(len(msg.attachment_ids), 1)
+ body = '<p>taratata <img src="/web/image/%s?access_token=%s" alt="image0" width="2"> <img src="/web/image/%s?access_token=%s" alt="image0" width="2"></p>'
+ body = body % (msg.attachment_ids[0].id, msg.attachment_ids[0].access_token, msg.attachment_ids[0].id, msg.attachment_ids[0].access_token)
+ self.assertEqual(msg.body, body)
+
class TestMessageAccess(common.BaseFunctionalTest, common.MockEmails):
diff --git a/addons/web/static/src/js/views/basic/basic_controller.js b/addons/web/static/src/js/views/basic/basic_controller.js
index 1c8060a6fe1..432d810bbbb 100644
--- a/addons/web/static/src/js/views/basic/basic_controller.js
+++ b/addons/web/static/src/js/views/basic/basic_controller.js
@@ -511,7 +511,7 @@ var BasicController = AbstractController.extend(FieldManagerMixin, {
// TODO this will tell the renderer to rerender the widget that
// asked for the discard but will unfortunately lose the click
// made on another row if any
- self._confirmChange(self.handle, [ev.data.fieldName], ev)
+ self._confirmChange(recordID, [ev.data.fieldName], ev)
.always(ev.data.onSuccess);
})
.fail(ev.data.onFailure);
diff --git a/addons/web/static/src/js/views/basic/basic_renderer.js b/addons/web/static/src/js/views/basic/basic_renderer.js
index 042d85d414a..58a5cb7a70e 100644
--- a/addons/web/static/src/js/views/basic/basic_renderer.js
+++ b/addons/web/static/src/js/views/basic/basic_renderer.js
@@ -254,7 +254,10 @@ var BasicRenderer = AbstractRenderer.extend({
if ('readonly' in modifiers && element.widget) {
var mode = modifiers.readonly ? 'readonly' : modifiersData.baseMode;
if (mode !== element.widget.mode) {
- self._rerenderFieldWidget(element.widget, record, mode);
+ self._rerenderFieldWidget(element.widget, record, {
+ keepBaseMode: true,
+ mode: mode,
+ });
return; // Rerendering already applied the modifiers, no need to go further
}
}
@@ -406,12 +409,25 @@ var BasicRenderer = AbstractRenderer.extend({
* @param {Object} record
* @param {jQuery|AbstractField} [element]
* @param {Object} [options]
- * @param {Object} [options.callback] - the callback to call on registration
- * and on modifiers updates
+ * @param {Object} [options.callback] the callback to call on registration
+ * and on modifiers updates
+ * @param {boolean} [options.keepBaseMode=false] this function registers the
+ * 'baseMode' of the node; this is a field widget specific settings which
+ * represents the generic mode of the widget, regardless of its modifiers
+ * (the interesting case is the list view: all widgets are supposed to be
+ * in the baseMode 'readonly', except the ones that are in the line that
+ * is currently being edited).
+ * With option 'keepBaseMode' set to true, the current baseMode of the
+ * node isn't overridden (this is particularily useful when a field widget
+ * is re-rendered because its readonly modifier changed, as in this case,
+ * we don't want to change its base mode).
+ * @param {string} [options.mode] the 'baseMode' of the node is set to this
+ * value (if not given, it is set to this.mode, the mode of the renderer)
* @returns {Object} for code efficiency, returns the last evaluated
- * modifiers for the given node and record.
+ * modifiers for the given node and record.
*/
_registerModifiers: function (node, record, element, options) {
+ options = options || {};
// Check if we already registered the modifiers for the given node
// If yes, this is simply an update of the related element
// If not, check the modifiers to see if it needs registration
@@ -428,12 +444,11 @@ var BasicRenderer = AbstractRenderer.extend({
this.allModifiersData.push(modifiersData);
}
}
- // we register here the base mode of the node. This is a field widget
- // specific settings which represents the generic mode for the widget,
- // regardless of its modifiers. The interesting case is the list view:
- // all widgets are supposed to be in the baseMode 'readonly', except the
- // ones that are in the line that is currently being edited.
- modifiersData.baseMode = (options && options.mode) || this.mode;
+
+ // Compute node's base mode if necessary
+ if (!options.keepBaseMode) {
+ modifiersData.baseMode = options.mode || this.mode;
+ }
// Evaluate if necessary
if (!modifiersData.evaluatedModifiers[record.id]) {
@@ -497,13 +512,16 @@ var BasicRenderer = AbstractRenderer.extend({
* @private
* @param {Object} node
* @param {Object} record
- * @param {Object} [options]
+ * @param {Object} [options] passed to @_registerModifiers
+ * @param {string} [options.mode] either 'edit' or 'readonly' (defaults to
+ * this.mode, the mode of the renderer)
* @returns {jQueryElement}
*/
_renderFieldWidget: function (node, record, options) {
+ options = options || {};
var fieldName = node.attrs.name;
// Register the node-associated modifiers
- var mode = options && options.mode || this.mode;
+ var mode = options.mode || this.mode;
var modifiers = this._registerModifiers(node, record, null, options);
// Initialize and register the widget
// Readonly status is known as the modifiers have just been registered
@@ -545,7 +563,8 @@ var BasicRenderer = AbstractRenderer.extend({
&& !element.widget.isSet()
));
},
- mode: mode
+ keepBaseMode: !!options.keepBaseMode,
+ mode: mode,
});
self._postProcessField(widget, node);
});
@@ -614,11 +633,11 @@ var BasicRenderer = AbstractRenderer.extend({
* @private
* @param {Widget} widget
* @param {Object} record
- * @param {string} mode either 'readonly' or 'edit'
+ * @param {Object} [options] options passed to @_renderFieldWidget
*/
- _rerenderFieldWidget: function (widget, record, mode) {
+ _rerenderFieldWidget: function (widget, record, options) {
// Render the new field widget
- var $el = this._renderFieldWidget(widget.__node, record, {mode: mode});
+ var $el = this._renderFieldWidget(widget.__node, record, options);
widget.$el.replaceWith($el);
// Destroy the old widget and position the new one at the old one's
diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js
index 7eb2ba4bc6b..99a5e4ee334 100644
--- a/addons/web/static/src/js/views/pivot/pivot_model.js
+++ b/addons/web/static/src/js/views/pivot/pivot_model.js
@@ -545,7 +545,6 @@ var PivotModel = AbstractModel.extend({
var data = Array.prototype.slice.call(arguments);
if (data[0][0].__count === 0) {
self.data.has_data = false;
- return;
}
self._prepareData(data);
});
diff --git a/addons/web/static/tests/views/form_tests.js b/addons/web/static/tests/views/form_tests.js
index 89c5a3d6a1d..d2ec320c6ea 100644
--- a/addons/web/static/tests/views/form_tests.js
+++ b/addons/web/static/tests/views/form_tests.js
@@ -630,7 +630,7 @@ QUnit.module('Views', {
});
QUnit.test('readonly attrs on fields are re-evaluated on field change', function (assert) {
- assert.expect(3);
+ assert.expect(4);
var form = createView({
View: FormView,
@@ -656,6 +656,9 @@ QUnit.module('Views', {
form.$('.o_field_boolean input').click();
assert.strictEqual(form.$('span[name="foo"]').length, 1,
"the foo field widget should have been rerendered to now be readonly again");
+ form.$('.o_field_boolean input').click();
+ assert.strictEqual(form.$('input[name="foo"]').length, 1,
+ "the foo field widget should have been rerendered to now be editable again");
form.destroy();
});
diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js
index 4a64e17e881..2fafef26c6f 100644
--- a/addons/web/static/tests/views/list_tests.js
+++ b/addons/web/static/tests/views/list_tests.js
@@ -1929,7 +1929,7 @@ QUnit.module('Views', {
});
QUnit.test('readonly attrs on fields are re-evaluated on field change', function (assert) {
- assert.expect(7);
+ assert.expect(9);
var list = createView({
View: ListView,
@@ -1963,9 +1963,14 @@ QUnit.module('Views', {
assert.strictEqual(list.$('tbody td.o_readonly_modifier').length, 3,
"the foo field widget parent cell should now be readonly again");
- // Reswitch the cell to editable and save the row
list.$('tbody tr:nth(0) td:nth(2) input').click();
- list.$('thead').click();
+ assert.strictEqual(list.$('tbody tr:nth(0) td:nth(1) > input[name="foo"]').length, 1,
+ "the foo field widget should have been rerendered as editable again");
+ assert.strictEqual(list.$('tbody td.o_readonly_modifier').length, 2,
+ "the foo field widget parent cell should not be readonly again");
+
+ // Click outside to leave edition mode
+ list.$el.click();
assert.strictEqual(list.$('tbody td.o_readonly_modifier').length, 2,
"there should be 2 readonly foo cells in readonly mode");
@@ -2773,6 +2778,28 @@ QUnit.module('Views', {
list.destroy();
});
+ QUnit.test('pressing ESC discard the current line changes (with required)', function (assert) {
+ assert.expect(3);
+
+ var list = createView({
+ View: ListView,
+ model: 'foo',
+ data: this.data,
+ arch: '<tree editable="top"><field name="foo" required="1"/></tree>',
+ });
+
+ list.$buttons.find('.o_list_button_add').click();
+
+ list.$('input[name="foo"]').trigger({type: 'keydown', which: $.ui.keyCode.ESCAPE});
+ assert.strictEqual(list.$('tr.o_data_row').length, 4,
+ "should have 4 data row in list");
+ assert.strictEqual(list.$('tr.o_data_row.o_selected_row').length, 0,
+ "no rows should be selected");
+ assert.ok(!list.$buttons.find('.o_list_button_save').is(':visible'),
+ "should not have a visible save button");
+ list.destroy();
+ });
+
QUnit.test('field with password attribute', function (assert) {
assert.expect(2);
diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js
index fe8c4c5b3f9..3e77d7aa8b9 100644
--- a/addons/web/static/tests/views/pivot_tests.js
+++ b/addons/web/static/tests/views/pivot_tests.js
@@ -1,7 +1,6 @@
odoo.define('web.pivot_tests', function (require) {
"use strict";
-var Context = require('web.Context');
var core = require('web.core');
var PivotView = require('web.PivotView');
var testUtils = require('web.test_utils');
@@ -739,7 +738,33 @@ QUnit.module('Views', {
},
});
- $('.o_pivot_download').click();
+ pivot.$buttons.find('.o_pivot_download').click();
+ pivot.destroy();
+ });
+
+ QUnit.test('can download a file without data', function (assert) {
+ assert.expect(1);
+
+ this.data.partner.records = [];
+
+ var pivot = createView({
+ View: PivotView,
+ model: "partner",
+ data: this.data,
+ arch: '<pivot>' +
+ '<field name="date" interval="month" type="col"/>' +
+ '<field name="foo" type="measure"/>' +
+ '</pivot>',
+ session: {
+ get_file: function (args) {
+ assert.strictEqual(args.url, '/web/pivot/export_xls',
+ "should call get_file with correct parameters");
+ args.complete();
+ },
+ },
+ });
+
+ pivot.$buttons.find('.o_pivot_download').click();
pivot.destroy();
});
diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py
index 50a9540323a..cc8d91cbaef 100644
--- a/addons/web_editor/controllers/main.py
+++ b/addons/web_editor/controllers/main.py
@@ -146,6 +146,9 @@ class Web_Editor(http.Controller):
# therefore we have to recover the files from the request object
Attachments = request.env['ir.attachment'] # registry for the attachment table
+ res_model = kwargs.get('res_model', 'ir.ui.view')
+ res_id = res_model != 'ir.ui.view' and kwargs.get('res_id') or None
+
uploads = []
message = None
if not upload: # no image provided, storing the link and the image name
@@ -155,9 +158,11 @@ class Web_Editor(http.Controller):
'type': 'url',
'url': url,
'public': True,
- 'res_model': 'ir.ui.view',
+ 'res_id': res_id,
+ 'res_model': res_model,
})
- uploads += attachment.read(['name', 'mimetype', 'checksum', 'url'])
+ attachment.generate_access_token()
+ uploads += attachment.read(['name', 'mimetype', 'checksum', 'url', 'res_id', 'res_model', 'access_token'])
else: # images provided
try:
attachments = request.env['ir.attachment']
@@ -180,10 +185,12 @@ class Web_Editor(http.Controller):
'datas': base64.b64encode(data),
'datas_fname': c_file.filename,
'public': True,
- 'res_model': 'ir.ui.view',
+ 'res_id': res_id,
+ 'res_model': res_model,
})
+ attachment.generate_access_token()
attachments += attachment
- uploads += attachments.read(['name', 'mimetype', 'checksum', 'url'])
+ uploads += attachments.read(['name', 'mimetype', 'checksum', 'url', 'res_id', 'res_model', 'access_token'])
except Exception as e:
logger.exception("Failed to upload image to attachment")
message = pycompat.text_type(e)
diff --git a/addons/web_editor/static/src/js/backend/fields.js b/addons/web_editor/static/src/js/backend/fields.js
index b84fcd21c91..d3d601bac73 100644
--- a/addons/web_editor/static/src/js/backend/fields.js
+++ b/addons/web_editor/static/src/js/backend/fields.js
@@ -118,6 +118,7 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
*/
_getValue: function () {
if (this.nodeOptions['style-inline']) {
+ transcoder.linkImgToAttachmentThumbnail(this.$content);
transcoder.classToStyle(this.$content);
transcoder.fontToImg(this.$content);
}
@@ -133,6 +134,8 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
this.$textarea.summernote(this._getSummernoteConfig());
this.$content = this.$('.note-editable:first');
this.$content.html(this._textToHtml(this.value));
+ this.$content.data('oe-id', this.recordData.res_id || this.res_id);
+ this.$content.data('oe-model', this.recordData.model || this.model);
// trigger a mouseup to refresh the editor toolbar
this.$content.trigger('mouseup');
if (this.nodeOptions['style-inline']) {
diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js
index 2ac9df2d693..71982f8137b 100644
--- a/addons/web_editor/static/src/js/editor/rte.summernote.js
+++ b/addons/web_editor/static/src/js/editor/rte.summernote.js
@@ -1080,8 +1080,12 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, {
return;
}
data.__alreadyDone = true;
+
var mediaDialog = new weWidgets.MediaDialog(this,
- data.options || {},
+ _.extend({
+ res_model: data.$editable.data('oe-model'),
+ res_id: data.$editable.data('oe-id'),
+ }, data.options),
data.$editable,
data.media
);
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js
index 38dcacf9e77..382bae0d4a7 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -872,7 +872,12 @@ registry.background = SnippetOption.extend({
// Put fake image in the DOM, edit it and use it as background-image
var $image = $('<img/>', {class: 'hidden', src: value}).appendTo(this.$target);
- var _editor = new widget.MediaDialog(this, {}, null, $image[0]).open();
+ var $editable = this.$target.closest('.o_editable');
+ var options = {
+ res_model: $editable.data('oe-model'),
+ res_id: $editable.data('oe-id'),
+ };
+ var _editor = new widget.MediaDialog(this, options, null, $image[0]).open();
_editor.opened(function () {
_editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
});
diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js
index 59609eab8a5..121a1a6d3d7 100644
--- a/addons/web_editor/static/src/js/editor/summernote.js
+++ b/addons/web_editor/static/src/js/editor/summernote.js
@@ -913,15 +913,21 @@ range.WrappedRange.prototype.deleteContents = function (towrite) {
return this;
}
- var prevBP = dom.removeBetween(this.sc, this.so, this.ec, this.eo, towrite);
+ var r;
+ var image = this.isOnImg();
+ if (image) {
+ // If the range matches/is in an image, then the image is to be removed
+ // and the cursor moved to its previous position
+ var parentNode = image.parentNode;
+ var index = _.indexOf(parentNode.childNodes, image);
+ parentNode.removeChild(image);
+ r = new range.WrappedRange(parentNode, index, parentNode, index);
+ } else {
+ r = dom.removeBetween(this.sc, this.so, this.ec, this.eo, towrite);
+ }
- $(dom.node(prevBP.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
- return new range.WrappedRange(
- prevBP.sc,
- prevBP.so,
- prevBP.ec,
- prevBP.eo
- );
+ $(dom.node(r.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
+ return new range.WrappedRange(r.sc, r.so, r.ec, r.eo);
};
range.WrappedRange.prototype.clean = function (mergeFilter, all) {
var node = dom.node(this.sc === this.ec ? this.sc : this.commonAncestor());
@@ -1258,7 +1264,7 @@ $.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
};
function remove_table_content(r) {
- var nodes = dom.listBetween(r.sc, r.ec);
+ var nodes = dom.listBetween(r.sc, r.ec, r.so, r.eo);
if (dom.isText(r.sc)) {
r.sc.textContent = r.sc.textContent.slice(0, r.so);
}
diff --git a/addons/web_editor/static/src/js/editor/transcoder.js b/addons/web_editor/static/src/js/editor/transcoder.js
index 11d23bf603b..54bbc803645 100644
--- a/addons/web_editor/static/src/js/editor/transcoder.js
+++ b/addons/web_editor/static/src/js/editor/transcoder.js
@@ -18,7 +18,7 @@ function getMatchedCSSRules(a) {
var sheets = document.styleSheets;
for (i = sheets.length-1 ; i >= 0 ; i--) {
var rules;
- if (sheets[i].rules) {
+ if (sheets[i].hasOwnProperty('rules')) {
rules = sheets[i].rules;
} else {
// try...catch because Firefox not able to enumerate
@@ -259,10 +259,40 @@ function styleToClass($editable) {
$c.remove();
}
+/**
+ * Converts css display for attachment link to real image.
+ * Without this post process, the display depends on the css and the picture
+ * does not appear when we use the html without css (to send by email for e.g.)
+ *
+ * @param {jQuery} $editable
+ */
+function attachmentThumbnailToLinkImg($editable) {
+ $editable.find('a[href*="/web/content/"][data-mimetype]:empty').each(function () {
+ var $link = $(this);
+ var $img = $('<img/>')
+ .attr('src', $link.css('background-image').replace(/(^url\(['"])|(['"]\)$)/g, ''))
+ .css('height', Math.max(1, $link.height()) + 'px')
+ .css('width', Math.max(1, $link.width()) + 'px');
+ $link.append($img);
+ });
+}
+
+/**
+ * Revert attachmentThumbnailToLinkImg changes
+ *
+ * @see attachmentThumbnailToLinkImg
+ * @param {jQuery} $editable
+ */
+function linkImgToAttachmentThumbnail($editable) {
+ $editable.find('a[href*="/web/content/"][data-mimetype] > img').remove();
+}
+
return {
fontToImg: fontToImg,
imgToFont: imgToFont,
classToStyle: classToStyle,
styleToClass: styleToClass,
+ attachmentThumbnailToLinkImg: attachmentThumbnailToLinkImg,
+ linkImgToAttachmentThumbnail: linkImgToAttachmentThumbnail,
};
});
diff --git a/addons/web_editor/static/src/js/inline.js b/addons/web_editor/static/src/js/inline.js
index ac0dee435ae..8715b5b7f24 100644
--- a/addons/web_editor/static/src/js/inline.js
+++ b/addons/web_editor/static/src/js/inline.js
@@ -53,6 +53,7 @@ snippet_editor.Class.include({
this._super.apply(this, arguments);
var $editable = $('#editable_area');
+ transcoder.linkImgToAttachmentThumbnail($editable);
transcoder.fontToImg($editable);
transcoder.classToStyle($editable);
diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js
index e499e83843d..b93724c7a30 100644
--- a/addons/web_editor/static/src/js/widgets/widgets.js
+++ b/addons/web_editor/static/src/js/widgets/widgets.js
@@ -120,7 +120,13 @@ var ImageDialog = Widget.extend({
this._super.apply(this, arguments);
this.options = options || {};
this.accept = this.options.accept || this.options.document ? "*/*" : "image/*";
- this.domain = this.options.domain || ['|', ['mimetype', '=', false], ['mimetype', this.options.document ? 'not in' : 'in', ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png']]];
+ if (this.options.res_id) {
+ this.domain = ['|',
+ '&', ['res_model', '=', this.options.res_model], ['res_id', '=', this.options.res_id],
+ ['res_model', '=', 'ir.ui.view']];
+ } else {
+ this.domain = [['res_model', '=', 'ir.ui.view']];
+ }
this.parent = parent;
this.old_media = media;
this.media = media;
@@ -171,6 +177,7 @@ var ImageDialog = Widget.extend({
}
},
save: function () {
+ var self = this;
if (this.options.select_images) {
return this.images;
}
@@ -181,34 +188,55 @@ var ImageDialog = Widget.extend({
img = _.find(this.images, function (img) { return img.id === id;});
}
- var media;
- if (!img.is_document) {
- if (this.media.tagName !== "IMG" || !this.old_media) {
- this.add_class = "pull-left";
- this.style = {"width": "100%"};
- }
- if (this.media.tagName !== "IMG") {
- media = document.createElement('img');
- $(this.media).replaceWith(media);
- this.media = media;
- }
- this.media.setAttribute('src', img.src);
- } else {
- if (this.media.tagName !== "A") {
- $('.note-control-selection').hide();
- media = document.createElement('a');
- $(this.media).replaceWith(media);
- this.media = media;
- }
- this.media.setAttribute('href', '/web/content/' + img.id + '?unique=' + img.checksum + '&download=true');
- $(this.media).addClass('o_image').attr('title', img.name).attr('data-mimetype', img.mimetype);
+ var def = $.when();
+ if (!img.access_token) {
+ def = this._rpc({
+ model: 'ir.attachment',
+ method: 'generate_access_token',
+ args: [[img.id]]
+ }).then(function (access_token) {
+ img.access_token = access_token[0];
+ });
}
- $(this.media).attr('alt', img.alt);
- var style = this.style;
- if (style) { $(this.media).css(style); }
+ return def.then(function () {
+ var media;
+ if (!img.is_document) {
+ if (img.access_token && self.options.res_model !== 'ir.ui.view') {
+ img.src += _.str.sprintf('?access_token=%s', img.access_token);
+ }
+ if (self.media.tagName !== "IMG" || !self.old_media) {
+ self.add_class = "pull-left";
+ self.style = {"width": "100%"};
+ }
+ if (self.media.tagName !== "IMG") {
+ media = document.createElement('img');
+ $(self.media).replaceWith(media);
+ self.media = media;
+ }
+ self.media.setAttribute('src', img.src);
+ } else {
+ if (self.media.tagName !== "A") {
+ $('.note-control-selection').hide();
+ media = document.createElement('a');
+ $(self.media).replaceWith(media);
+ self.media = media;
+ }
+ var href = '/web/content/' + img.id + '?';
+ if (img.access_token && self.options.res_model !== 'ir.ui.view') {
+ href += _.str.sprintf('access_token=%s&', img.access_token);
+ }
+ href += 'unique=' + img.checksum + '&download=true';
+ self.media.setAttribute('href', href);
+ $(self.media).addClass('o_image').attr('title', img.name).attr('data-mimetype', img.mimetype);
+ }
- return this.media;
+ $(self.media).attr('alt', img.alt);
+ var style = self.style;
+ if (style) { $(self.media).css(style); }
+
+ return self.media;
+ });
},
clear: function () {
this.media.className = this.media.className.replace(/(^|\s+)((img(\s|$)|img-(?!circle|rounded|thumbnail))[^\s]*)/g, ' ');
@@ -282,7 +310,7 @@ var ImageDialog = Widget.extend({
}
},
fetch_existing: function (needle) {
- var domain = [['res_model', '=', 'ir.ui.view']].concat(this.domain);
+ var domain = this.domain.concat(['|', ['mimetype', '=', false], ['mimetype', this.options.document ? 'not in' : 'in', ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png']]]);
if (needle && needle.length) {
domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
}
@@ -292,7 +320,7 @@ var ImageDialog = Widget.extend({
args: [],
kwargs: {
domain: domain,
- fields: ['name', 'mimetype', 'checksum', 'url', 'type'],
+ fields: ['name', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'access_token'],
order: [{name: 'id', asc: false}],
context: weContext.get(),
}
@@ -305,7 +333,7 @@ var ImageDialog = Widget.extend({
return (r.url || r.id);
});
_.each(this.records, function (record) {
- record.src = record.url || _.str.sprintf('/web/image/%s/%s', record.id, record.name); // Name is added for SEO purposes
+ record.src = record.url || _.str.sprintf('/web/image/%s/%s', record.id, encodeURI(record.name)); // Name is added for SEO purposes
record.is_document = !(/gif|jpe|jpg|png/.test(record.mimetype));
});
this.display_attachments();
@@ -1001,18 +1029,21 @@ var MediaDialog = Dialog.extend({
return this._super.apply(this, arguments);
},
save: function () {
+ var self = this;
+ var args = arguments;
+ var _super = this._super;
if (this.options.select_images) {
- this.final_data = this.active.save();
- this._super.apply(this, arguments);
- return;
+ return $.when(this.active.save()).then(function (data) {
+ self.final_data = data;
+ return _super.apply(self, args);
+ });
}
if (this.rte) {
this.range.select();
this.rte.historyRecordUndo(this.media);
}
- var self = this;
- if (self.media) {
+ if (this.media) {
this.media.innerHTML = "";
if (this.active !== this.imageDialog && this.active !== this.documentDialog) {
this.imageDialog.clear();
@@ -1029,27 +1060,27 @@ var MediaDialog = Dialog.extend({
this.range.insertNode(this.media, true);
this.active.media = this.media;
}
- this.active.save();
- if (this.active.add_class) {
- $(this.active.media).addClass(this.active.add_class);
- }
- var media = this.active.media;
-
- this.final_data = [media, self.old_media];
- $(document.body).trigger("media-saved", this.final_data);
- $(self.old_media).trigger("save", this.final_data);
- $(this.final_data).trigger('input');
-
- // Update editor bar after image edition (in case the image change to icon or other)
- _.defer(function () {
- if (!media.parentNode) return;
- range.createFromNode(media).select();
- click_event(media, "mousedown");
- click_event(media, "mouseup");
+ return $.when(this.active.save()).then(function () {
+ if (self.active.add_class) {
+ $(self.active.media).addClass(self.active.add_class);
+ }
+ var media = self.active.media;
+
+ self.final_data = [media, self.old_media];
+ $(document.body).trigger("media-saved", self.final_data);
+ $(self.old_media).trigger("save", self.final_data);
+ $(self.final_data).trigger('input');
+
+ // Update editor bar after image edition (in case the image change to icon or other)
+ _.defer(function () {
+ if (!media.parentNode) return;
+ range.createFromNode(media).select();
+ click_event(media, "mousedown");
+ click_event(media, "mouseup");
+ });
+ return _super.apply(self, args);
});
-
- this._super.apply(this, arguments);
},
searchTimer: null,
search: function () {
diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml
index e8e6374305e..8f975b0d784 100644
--- a/addons/web_editor/static/src/xml/editor.xml
+++ b/addons/web_editor/static/src/xml/editor.xml
@@ -86,6 +86,8 @@
target="fileframe"
class="form-inline">
<input type="hidden" name="csrf_token" t-att-value="csrf_token"/>
+ <input t-if="widget.options.res_id" type="hidden" name="res_id" t-att-value="widget.options.res_id"/>
+ <input t-if="widget.options.res_model" type="hidden" name="res_model" t-att-value="widget.options.res_model"/>
<div class="well">
<div class="form-group pull-left">
<input type="file" name="upload" t-att-accept="widget.accept" multiple="multiple" style="position: absolute; opacity: 0; width: 1px; height: 1px;"/>
@@ -145,8 +147,9 @@
<div class="existing-attachments">
<div class="row mt16" t-as="row" t-foreach="rows">
<div class="col-sm-2 o_existing_attachment_cell" t-as="attachment" t-foreach="row">
- <i class="fa fa-times o_existing_attachment_remove" t-att-data-id="attachment.id"/>
- <div class="o_attachment_border"><div t-att-data-src="attachment.src" t-att-data-url="attachment.url" t-att-alt="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" t-att-data-mimetype="attachment.mimetype" class="o_image"/></div>
+ <i t-if="attachment.res_model === 'ir.ui.view'" class="fa fa-times o_existing_attachment_remove" title="This file is a public view attachment" t-att-data-id="attachment.id"/>
+ <i t-else="" class="fa fa-times o_existing_attachment_remove" title="This file is attached to the current record" t-att-data-id="attachment.id"/>
+ <div class="o_attachment_border" t-att-style="attachment.res_model === 'ir.ui.view' ? null : 'border: 1px solid #5cb85c;'"><div t-att-data-src="attachment.src" t-att-data-url="attachment.url" t-att-alt="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" t-att-data-mimetype="attachment.mimetype" class="o_image"/></div>
</div>
</div>
</div>
diff --git a/addons/website/models/ir_qweb.py b/addons/website/models/ir_qweb.py
index a20db972c0b..943a603d211 100644
--- a/addons/website/models/ir_qweb.py
+++ b/addons/website/models/ir_qweb.py
@@ -26,7 +26,7 @@ class QWeb(models.AbstractModel):
def _get_asset(self, xmlid, options, css=True, js=True, debug=False, async=False, values=None):
website = getattr(request, 'website', None) if request else None
if website and website.cdn_activated:
- values = dict(values, url_for=website.get_cdn_url)
+ values = dict(values or {}, url_for=website.get_cdn_url)
return super(QWeb, self)._get_asset(xmlid, options, css, js, debug, async, values)
def _website_build_attribute(self, tagName, name, value, options, values):
diff --git a/addons/website_quote/models/sale_order.py b/addons/website_quote/models/sale_order.py
index 6824284b84e..a54e8368c24 100644
--- a/addons/website_quote/models/sale_order.py
+++ b/addons/website_quote/models/sale_order.py
@@ -200,8 +200,8 @@ class SaleOrder(models.Model):
def get_portal_confirmation_action(self):
""" Template override default behavior of pay / sign chosen in sales settings """
- if self.template_id:
- return 'sign' if self.require_payment == 1 else 'pay'
+ if self.require_payment is not None or self.require_payment is not False:
+ return 'pay' if self.require_payment == 1 else 'sign'
return super(SaleOrder, self).get_portal_confirmation_action()
@api.multi
diff --git a/doc/cla/corporate/wt-io-it.md b/doc/cla/corporate/wt-io-it.md
new file mode 100644
index 00000000000..491d580c792
--- /dev/null
+++ b/doc/cla/corporate/wt-io-it.md
@@ -0,0 +1,15 @@
+Austria, 2018-02-06
+
+WT-IO-IT GmbH 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,
+
+Wolfgang Taferner wolfgang.taferner@wt-io-it.at https://github.com/wt-io-it
+
+List of contributors:
+
+Wolfgang Taferner wolfgang.taferner@wt-io-it.at https://github.com/wtaferner
diff --git a/doc/cla/individual/remi-filament.md b/doc/cla/individual/remi-filament.md
index b57242f471f..fb602974ade 100644
--- a/doc/cla/individual/remi-filament.md
+++ b/doc/cla/individual/remi-filament.md
@@ -8,4 +8,4 @@ declaration.
Signed,
-Rémi CAZENAVE remi@le-filament.com https://github.com/remi-filament
+Rémi CAZENAVE 30716308+remi-filament@users.noreply.github.com https://github.com/remi-filament
diff --git a/doc/setup/cdn.rst b/doc/setup/cdn.rst
index 211df06f7f6..c21323b1467 100644
--- a/doc/setup/cdn.rst
+++ b/doc/setup/cdn.rst
@@ -39,7 +39,8 @@ Step 2: Configure the odoo instance with your zone
In the Odoo back end, go to the :guilabel:`Website Settings`: menu, then
activate the CDN support and copy/paste your zone URL in the
-:guilabel:`CDN Base URL` field.
+:guilabel:`CDN Base URL` field. This field is only visible and configurable if
+you have developer mode activated.
.. image:: cdn/odoo_cdn_base_url.png
:class: img-responsive
diff --git a/odoo/addons/base/models/ir_attachment.py b/odoo/addons/base/models/ir_attachment.py
index 19409a3b78b..bae99e919a6 100644
--- a/odoo/addons/base/models/ir_attachment.py
+++ b/odoo/addons/base/models/ir_attachment.py
@@ -8,6 +8,7 @@ import mimetypes
import os
import re
from collections import defaultdict
+import uuid
from odoo import api, fields, models, tools, SUPERUSER_ID, _
from odoo.exceptions import AccessError
@@ -280,7 +281,7 @@ class IrAttachment(models.Model):
public = fields.Boolean('Is public document')
# for external access
- access_token = fields.Char('Access Token')
+ access_token = fields.Char('Access Token', groups="base.group_user")
# the field 'datas' is computed and may use the other fields below
datas = fields.Binary(string='File Content', compute='_compute_datas', inverse='_inverse_datas')
@@ -438,6 +439,14 @@ class IrAttachment(models.Model):
self.browse().check('write', values=values)
return super(IrAttachment, self).create(values)
+ @api.one
+ def generate_access_token(self):
+ if self.access_token:
+ return self.access_token
+ access_token = str(uuid.uuid4())
+ self.write({'access_token': access_token})
+ return access_token
+
@api.model
def action_get(self):
return self.env['ir.actions.act_window'].for_xml_id('base', 'action_attachment')
diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py
index 27d759e5461..751f4aed9ee 100644
--- a/odoo/addons/base/models/ir_http.py
+++ b/odoo/addons/base/models/ir_http.py
@@ -21,7 +21,7 @@ import odoo
from odoo import api, http, models, tools, SUPERUSER_ID
from odoo.exceptions import AccessDenied, AccessError
from odoo.http import request, STATIC_CACHE, content_disposition
-from odoo.tools import pycompat
+from odoo.tools import pycompat, consteq
from odoo.tools.mimetypes import guess_mimetype
from odoo.modules.module import get_resource_path, get_module_path
@@ -269,6 +269,7 @@ class IrHttp(models.AbstractModel):
:param str mimetype: mintype of the field (for headers)
:param str default_mimetype: default mintype if no mintype found
:param str access_token: optional token for unauthenticated access
+ only available for ir.attachment
:param Environment env: by default use request.env
:returns: (status, headers, content)
"""
@@ -277,6 +278,10 @@ class IrHttp(models.AbstractModel):
obj = None
if xmlid:
obj = env.ref(xmlid, False)
+ elif id and model == 'ir.attachment' and access_token:
+ obj = env[model].sudo().browse(int(id))
+ if not consteq(obj.access_token, access_token):
+ return (403, [], None)
elif id and model in env.registry:
obj = env[model].browse(int(id))
diff --git a/odoo/addons/base/models/res_company.py b/odoo/addons/base/models/res_company.py
index b95b502f6a3..c6c5d360b8f 100644
--- a/odoo/addons/base/models/res_company.py
+++ b/odoo/addons/base/models/res_company.py
@@ -144,8 +144,8 @@ class Company(models.Model):
self.ensure_one()
currency_id = self._get_user_currency()
if country_id:
- currency_id = self.env['res.country'].browse(country_id).currency_id.id
- return {'value': {'currency_id': currency_id}}
+ currency_id = self.env['res.country'].browse(country_id).currency_id
+ return {'value': {'currency_id': currency_id.id}}
@api.onchange('country_id')
def _onchange_country_id_wrapper(self):