diff options
| author | Christophe Simonis <chs@odoo.com> | 2018-02-09 16:13:08 +0100 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2018-02-09 16:13:08 +0100 |
| commit | 5f3301e5cbbb2c6e7bff732761eef80e8dd5a33d (patch) | |
| tree | 48ab6bd9bbda92dea713c0c52bca3396ef939d82 | |
| parent | 715e602bf04ae5a0c9704c599f2145b2808b31b7 (diff) | |
| parent | 20e4cd5bcd1816a478aedd441efa90a34cf63334 (diff) | |
[MERGE] forward port branch 11.0 up to 20e4cd5bcd1
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): |
