diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-08-06 18:29:20 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-08-06 18:48:24 +0200 |
| commit | 7d04dc1c7c51dfd18951912628b14bee446a2b01 (patch) | |
| tree | e89601e493bea092239df55a5ef406b2e05d7d7b | |
| parent | b091014686d86ad793e2d1cbf0e7ffea8fbde361 (diff) | |
| parent | 9e74730a26b09fa4e516bc0d76550f7c7c502fac (diff) | |
[MERGE] forward port branch saas-12.2 up to 9e74730a26b
79 files changed, 668 insertions, 99 deletions
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 28f508ea1c0..b82b8756a1c 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -2012,7 +2012,10 @@ class AccountInvoiceLine(models.Model): return if not self.product_id: fpos = self.invoice_id.fiscal_position_id - default_tax = self.invoice_id.type in ('out_invoice', 'out_refund') and self.invoice_id.company_id.account_sale_tax_id or self.invoice_id.company_id.account_purchase_tax_id + if self.invoice_id.type in ('out_invoice', 'out_refund'): + default_tax = self.invoice_id.company_id.account_sale_tax_id + else: + default_tax = self.invoice_id.company_id.account_purchase_tax_id self.invoice_line_tax_ids = fpos.map_tax(self.account_id.tax_ids or default_tax, partner=self.partner_id).ids elif not self.price_unit: self._set_taxes() diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py index 614aba000ab..f6e312b2110 100644 --- a/addons/account/report/account_invoice_report.py +++ b/addons/account/report/account_invoice_report.py @@ -189,10 +189,9 @@ class ReportInvoiceWithPayment(models.AbstractModel): @api.model def _get_report_values(self, docids, data=None): - report = self.env['ir.actions.report']._get_report_from_name('account.report_invoice_with_payments') return { 'doc_ids': docids, - 'doc_model': report.model, - 'docs': self.env[report.model].browse(docids), + 'doc_model': 'account.invoice', + 'docs': self.env['account.invoice'].browse(docids), 'report_type': data.get('report_type') if data else '', } diff --git a/addons/account/static/src/js/reconciliation/reconciliation_model.js b/addons/account/static/src/js/reconciliation/reconciliation_model.js index 985bcd15496..f7f29efb011 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_model.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_model.js @@ -932,7 +932,7 @@ var StatementModel = BasicModel.extend({ 'link': prop.id, 'tax_ids': [tax.id], 'amount': tax.amount, - 'label': tax.name, + 'label': prop.label ? prop.label + " " + tax.name : tax.name, 'date': prop.date, 'account_id': tax.account_id ? [tax.account_id, null] : prop.account_id, 'analytic': tax.analytic, diff --git a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js index e6be300714d..7103e3f7d71 100644 --- a/addons/account/static/src/js/reconciliation/reconciliation_renderer.js +++ b/addons/account/static/src/js/reconciliation/reconciliation_renderer.js @@ -794,6 +794,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, { */ _onSelectMoveLine: function (event) { var $el = $(event.target); + $el.prop('disabled', true); this._destroyPopover($el); var moveLineId = $el.closest('.mv_line').data('line-id'); this.trigger_up('add_proposition', {'data': moveLineId}); diff --git a/addons/account/static/tests/reconciliation_tests.js b/addons/account/static/tests/reconciliation_tests.js index d2f52cb382d..e565ec91ce2 100644 --- a/addons/account/static/tests/reconciliation_tests.js +++ b/addons/account/static/tests/reconciliation_tests.js @@ -1299,14 +1299,14 @@ QUnit.module('account', { await testUtils.dom.click(widget.$('.create .quick_add button:contains(ATOS)')); assert.strictEqual(widget.$('.accounting_view tbody .cell_label, .accounting_view tbody .cell_right').text().replace(/[\n\r\s$,]+/g, ' '), - " ATOS Banque 1145.62 ATOS Frais 26.71 Tax 20.00% 229.12 Tax 10.00% include 2.67 ", "should display 4 lines"); + " ATOS Banque 1145.62 ATOS Frais 26.71 ATOS Banque Tax 20.00% 229.12 ATOS Frais Tax 10.00% include 2.67 ", "should display 4 lines"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '), "Open balance229.12", "should display the 'Open balance' line with value in left column"); await testUtils.fields.editAndTrigger(widget.$('.create .create_amount input'), '100',['input']); assert.strictEqual(widget.$('.accounting_view tbody').text().replace(/[\n\r\s$,]+/g, ' '), - " 101120 New ATOS Banque 1075.00 101130 New ATOS Frais 90.91 101120 New Tax 20.00% 215.00 101300 New Tax 10.00% include 9.09 ", + " 101120 New ATOS Banque 1075.00 101130 New ATOS Frais 90.91 101120 New ATOS Banque Tax 20.00% 215.00 101300 New ATOS Frais Tax 10.00% include 9.09 ", "should update the value of the 4 lines (because the line must have 100% of the value)"); assert.strictEqual(widget.$('.accounting_view tfoot .cell_label, .accounting_view tfoot .cell_left').text().replace(/[\n\r\s$,]+/g, ' '), "Open balance215.00", "should change the 'Open balance' line because the 20.00% tax is not an include tax"); diff --git a/addons/account/wizard/account_invoice_send_views.xml b/addons/account/wizard/account_invoice_send_views.xml index eac9adda58e..cae3b296b63 100644 --- a/addons/account/wizard/account_invoice_send_views.xml +++ b/addons/account/wizard/account_invoice_send_views.xml @@ -14,7 +14,6 @@ <field name="invoice_ids" invisible="1"/> <field name="email_from" invisible="1" /> <field name="mail_server_id" invisible="1"/> - <field name="reply_to" invisible="1"/> <div name="option_print"> <field name="is_print" /> <b><label for="is_print"/></b> diff --git a/addons/account_analytic_default/models/account_analytic_default.py b/addons/account_analytic_default/models/account_analytic_default.py index 34052a51560..707e9920ac0 100644 --- a/addons/account_analytic_default/models/account_analytic_default.py +++ b/addons/account_analytic_default/models/account_analytic_default.py @@ -73,8 +73,9 @@ class AccountInvoiceLine(models.Model): fields.Date.today(), company_id=self.company_id.id ) - self.account_analytic_id = rec.analytic_id.id - self.analytic_tag_ids = rec.analytic_tag_ids.ids + if rec: + self.account_analytic_id = rec.analytic_id.id + self.analytic_tag_ids = rec.analytic_tag_ids.ids return res def _set_additional_fields(self, invoice): diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index 3cb54bf3b43..a34367ddedd 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -347,10 +347,13 @@ class AccountVoucher(models.Model): } if company_currency != current_currency: ctx = {} + sign = temp['credit'] and -1 or 1 + amount_currency = company_cur._convert(tax_vals['amount'], current_cur, line.company_id, + self.account_date or fields.Date.today(), round=True) if self.account_date: ctx['date'] = self.account_date temp['currency_id'] = current_currency - temp['amount_currency'] = company_cur._convert(tax_vals['amount'], current_cur, line.company_id, self.account_date or fields.Date.today(), round=True) + temp['amount_currency'] = sign * abs(amount_currency) self.env['account.move.line'].create(temp) # When global rounding is activated, we must wait until all tax lines are computed to diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml index e6857e7f4ad..cf2480aa1f5 100644 --- a/addons/crm/views/crm_lead_views.xml +++ b/addons/crm/views/crm_lead_views.xml @@ -92,7 +92,7 @@ <field name="zip" placeholder="ZIP" class="o_address_zip"/> <field name="country_id" placeholder="Country" class="o_address_country" options='{"no_open": True, "no_create": True}'/> </div> - <field name="website" widget="url" placeholder="e.g. www.odoo.com"/> + <field name="website" widget="url" placeholder="e.g. https://www.odoo.com"/> </group> <group> <label for="contact_name"/> @@ -604,7 +604,7 @@ <field name="zip" placeholder="ZIP" class="o_address_zip"/> <field name="country_id" placeholder="Country" class="o_address_country" options='{"no_open": True, "no_create": True}'/> </div> - <field name="website" widget="url" placeholder="e.g. www.odoo.com"/> + <field name="website" widget="url" placeholder="e.g. https://www.odoo.com"/> </group> <group class="mt48"> diff --git a/addons/digest/views/digest_templates.xml b/addons/digest/views/digest_templates.xml index 0010ad6a1c0..e7cd42d5df5 100644 --- a/addons/digest/views/digest_templates.xml +++ b/addons/digest/views/digest_templates.xml @@ -6,7 +6,7 @@ <div class="col-lg-6 offset-lg-3"> <h3>Digest Subscriptions</h3> <div class="alert alert-success text-center" role="status"> - <p>You have been successfully unsubscribed from <string t-field="digest.name"/></p> + <p>You have been successfully unsubscribed from <strong t-field="digest.name"/></p> </div> </div> </div> diff --git a/addons/google_drive/models/res_config_settings.py b/addons/google_drive/models/res_config_settings.py index 49d7ff7d86d..eff3d6c3f6f 100644 --- a/addons/google_drive/models/res_config_settings.py +++ b/addons/google_drive/models/res_config_settings.py @@ -28,10 +28,12 @@ class ResConfigSettings(models.TransientModel): params = self.env['ir.config_parameter'].sudo() authorization_code_before = params.get_param('google_drive_authorization_code') authorization_code = self.google_drive_authorization_code - refresh_token = False - if authorization_code and authorization_code != authorization_code_before: - refresh_token = self.env['google.service'].generate_refresh_token('drive', authorization_code) - params.set_param('google_drive_refresh_token', refresh_token) + if authorization_code != authorization_code_before: + refresh_token = ( + self.env['google.service'].generate_refresh_token('drive', authorization_code) + if authorization_code else False + ) + params.set_param('google_drive_refresh_token', refresh_token) @api.multi def action_setup_token(self): diff --git a/addons/hr/security/hr_security.xml b/addons/hr/security/hr_security.xml index 8d435e67463..30958ef43d1 100644 --- a/addons/hr/security/hr_security.xml +++ b/addons/hr/security/hr_security.xml @@ -47,5 +47,12 @@ <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> </record> + <record id="hr_employee_comp_rule" model="ir.rule"> + <field name="name">Employee multi company rule</field> + <field name="model_id" ref="model_hr_employee"/> + <field name="global" eval="True"/> + <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> + </record> + </data> </odoo> diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 97bf33abc7f..a9bc53d1bae 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -554,6 +554,8 @@ class HolidaysRequest(models.Model): if not values.get('department_id'): values.update({'department_id': self.env['hr.employee'].browse(employee_id).department_id.id}) holiday = super(HolidaysRequest, self.with_context(mail_create_nosubscribe=True)).create(values) + if self._context.get('import_file'): + holiday._onchange_leave_dates() if not self._context.get('leave_fast_create'): holiday.add_follower(employee_id) if holiday.validation_type == 'hr': diff --git a/addons/hr_holidays/models/hr_leave_allocation.py b/addons/hr_holidays/models/hr_leave_allocation.py index 91bf5658093..50f71e713a0 100644 --- a/addons/hr_holidays/models/hr_leave_allocation.py +++ b/addons/hr_holidays/models/hr_leave_allocation.py @@ -367,7 +367,8 @@ class HolidaysAllocation(models.Model): holiday.message_subscribe(partner_ids=(holiday.employee_id.parent_id.user_id.partner_id | holiday.employee_id.leave_manager_id.partner_id).ids) if 'employee_id' in values: holiday._onchange_employee() - holiday.activity_update() + if not self._context.get('import_file'): + holiday.activity_update() return holiday @api.multi diff --git a/addons/hr_timesheet/views/res_config_settings_views.xml b/addons/hr_timesheet/views/res_config_settings_views.xml index 6588b0b2d14..2cb4437556a 100644 --- a/addons/hr_timesheet/views/res_config_settings_views.xml +++ b/addons/hr_timesheet/views/res_config_settings_views.xml @@ -105,5 +105,5 @@ </record> <menuitem id="hr_timesheet_config_settings_menu_action" name="Settings" parent="hr_timesheet_menu_configuration" - action="hr_timesheet_config_settings_action" sequence="0"/> + action="hr_timesheet_config_settings_action" sequence="0" groups="base.group_system"/> </odoo> diff --git a/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report.py b/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report.py index 572ba6d5ce9..166e6f23065 100644 --- a/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report.py +++ b/addons/hr_timesheet_attendance/report/hr_timesheet_attendance_report.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, tools class TimesheetAttendance(models.Model): @@ -17,7 +17,7 @@ class TimesheetAttendance(models.Model): @api.model_cr def init(self): - self._cr.execute("DROP VIEW IF EXISTS hr_timesheet_attendance_report") + tools.drop_view_if_exists(self.env.cr, self._table) self._cr.execute("""CREATE OR REPLACE VIEW %s AS ( SELECT max(id) AS id, diff --git a/addons/l10n_es/data/account.account.template-common.csv b/addons/l10n_es/data/account.account.template-common.csv index 21c952cf79c..4e5121d061e 100644 --- a/addons/l10n_es/data/account.account.template-common.csv +++ b/addons/l10n_es/data/account.account.template-common.csv @@ -276,7 +276,7 @@ "account_common_523","Proveedores de inmovilizado a corto plazo","523","account.data_account_type_payable","l10n_es.account_chart_template_common","l10n_es.account_group_523","True" "account_common_524","Acreedores por arrendamiento financiero a corto plazo","524","account.data_account_type_current_liabilities","l10n_es.account_chart_template_common","l10n_es.account_group_524","False" "account_common_525","Efectos a pagar a corto plazo","525","account.data_account_type_current_liabilities","l10n_es.account_chart_template_common","l10n_es.account_group_525","False" -"account_common_526","Dividendo activo a pagar","526","account.data_account_type_current_liabilities","l10n_es.account_chart_template_common","l10n_es.account_group_526","False" +"account_common_526","Dividendo activo a pagar","526","account.data_account_type_payable","l10n_es.account_chart_template_common","l10n_es.account_group_526","True" "account_common_527","Intereses a corto plazo de deudas con entidades de crédito","527","account.data_account_type_current_liabilities","l10n_es.account_chart_template_common","l10n_es.account_group_527","False" "account_common_528","Intereses a corto plazo de deudas","528","account.data_account_type_current_liabilities","l10n_es.account_chart_template_common","l10n_es.account_group_528","False" "account_common_5290","Provisión a corto plazo por retribuciones al personal","5290","account.data_account_type_current_liabilities","l10n_es.account_chart_template_common","l10n_es.account_group_5290","True" diff --git a/addons/l10n_es/data/account_tax_data.xml b/addons/l10n_es/data/account_tax_data.xml index 9461c865a72..dca71560db1 100644 --- a/addons/l10n_es/data/account_tax_data.xml +++ b/addons/l10n_es/data/account_tax_data.xml @@ -445,6 +445,7 @@ <field name="description">P_IVA21_SP_IN</field> <field name="type_tax_use">purchase</field> <field name="chart_template_id" ref="l10n_es.account_chart_template_common"/> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_10_11'), ref('mod_303_14_15_purchase'), ref('mod_303_36_37'), ref('mod_303_40_41')])]"/> <field name="amount_type">percent</field> <field name="amount" eval="21"/> <field name="tax_group_id" ref="tax_group_iva_21"/> @@ -550,6 +551,7 @@ <field name="type_tax_use">purchase</field> <field name="amount_type">percent</field> <field name="amount" eval="21"/> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_38_39'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/> <field name="chart_template_id" ref="l10n_es.account_chart_template_common"/> <field name="tax_group_id" ref="tax_group_iva_21"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), @@ -716,6 +718,7 @@ <field name="chart_template_id" ref="l10n_es.account_chart_template_common"/> <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_12_13'), ref('mod_303_14_15_purchase'), ref('mod_303_28_29'), ref('mod_303_40_41')])]"/> <field name="name">IVA 4% Adquisición de servicios extracomunitarios</field> <field name="tax_group_id" ref="tax_group_iva_4"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), @@ -767,6 +770,7 @@ <field name="description">P_IVA10_SP_EX</field> <field name="chart_template_id" ref="l10n_es.account_chart_template_common"/> <field name="amount_type">percent</field> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_12_13'), ref('mod_303_14_15_purchase'), ref('mod_303_28_29'), ref('mod_303_40_41')])]"/> <field name="type_tax_use">purchase</field> <field name="name">IVA 10% Adquisición de servicios extracomunitarios</field> <field name="tax_group_id" ref="tax_group_iva_10"/> @@ -820,6 +824,7 @@ <field name="type_tax_use">purchase</field> <field name="amount" eval="21"/> <field name="chart_template_id" ref="l10n_es.account_chart_template_common"/> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_12_13'), ref('mod_303_14_15_purchase'), ref('mod_303_28_29'), ref('mod_303_40_41')])]"/> <field name="amount_type">percent</field> <field name="tax_group_id" ref="tax_group_iva_21"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), @@ -869,6 +874,7 @@ <field name="amount" eval="4"/> <field name="description">P_IVA4_IC_BC</field> <field name="chart_template_id" ref="l10n_es.account_chart_template_common"/> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_36_37'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/> <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="name">IVA 4% Adquisición Intracomunitario. Bienes corrientes</field> @@ -924,6 +930,7 @@ <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="name">IVA 4% Adquisición Intracomunitario. Bienes de inversión</field> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_38_39'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/> <field name="tax_group_id" ref="tax_group_iva_4"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), (0,0, { @@ -972,6 +979,7 @@ <field name="amount" eval="10"/> <field name="description">P_IVA10_IC_BC</field> <field name="chart_template_id" ref="l10n_es.account_chart_template_common"/> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_36_37'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/> <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="name">IVA 10% Adquisición Intracomunitario. Bienes corrientes</field> @@ -1026,6 +1034,7 @@ <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="name">IVA 10% Adquisición Intracomunitario. Bienes de inversión</field> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_38_39'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/> <field name="tax_group_id" ref="tax_group_iva_10"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), (0,0, { @@ -3068,6 +3077,7 @@ <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="name">IVA 10% Adquisición de servicios intracomunitarios</field> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_10_11'), ref('mod_303_14_15_purchase'), ref('mod_303_36_37'), ref('mod_303_40_41')])]"/> <field name="tax_group_id" ref="tax_group_iva_10"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), (0,0, { @@ -3119,6 +3129,7 @@ <field name="amount_type">percent</field> <field name="type_tax_use">purchase</field> <field name="name">IVA 4% Adquisición de servicios intracomunitarios</field> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_10_11'), ref('mod_303_14_15_purchase'), ref('mod_303_36_37'), ref('mod_303_40_41')])]"/> <field name="tax_group_id" ref="tax_group_iva_4"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), (0,0, { @@ -3392,6 +3403,7 @@ <field name="description">P_IVA4_ISP</field> <field name="name">IVA 4% Compra con Inversión del Sujeto Pasivo Nacional</field> <field name="type_tax_use">purchase</field> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_28_29'), ref('mod_303_40_41'), ref('mod_303_12_13'), ref('mod_303_14_15_purchase')])]"/> <field name="amount_type">percent</field> <field name="amount" eval="4"/> <field name="chart_template_id" ref="l10n_es.account_chart_template_common"/> @@ -3443,6 +3455,7 @@ <field name="description">P_IVA10_ISP</field> <field name="name">IVA 10% Compra con Inversión del Sujeto Pasivo Nacional</field> <field name="type_tax_use">purchase</field> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_28_29'), ref('mod_303_40_41'), ref('mod_303_12_13'), ref('mod_303_14_15_purchase')])]"/> <field name="amount_type">percent</field> <field name="amount" eval="10"/> <field name="chart_template_id" ref="l10n_es.account_chart_template_common"/> @@ -3496,6 +3509,7 @@ <field name="type_tax_use">purchase</field> <field name="amount_type">percent</field> <field name="amount" eval="21"/> + <field name="tag_ids" eval="[(6, False, [ref('mod_303_28_29'), ref('mod_303_40_41'), ref('mod_303_12_13'), ref('mod_303_14_15_purchase')])]"/> <field name="chart_template_id" ref="l10n_es.account_chart_template_common"/> <field name="tax_group_id" ref="tax_group_iva_21"/> <field name="invoice_repartition_line_ids" eval="[(5, 0, 0), diff --git a/addons/l10n_mn/__manifest__.py b/addons/l10n_mn/__manifest__.py index ba60ad4da0f..bd1a8436eb6 100644 --- a/addons/l10n_mn/__manifest__.py +++ b/addons/l10n_mn/__manifest__.py @@ -5,6 +5,7 @@ "name" : "Mongolia - Accounting", "version" : "1.0", 'category': 'Localization', + "author" : "BumanIT LLC, Odoo S.A", "description": """ This is the module to manage the accounting chart for Mongolia. =============================================================== diff --git a/addons/l10n_mx/data/account.account.template.csv b/addons/l10n_mx/data/account.account.template.csv index b9cd4a41d36..316cab62337 100644 --- a/addons/l10n_mx/data/account.account.template.csv +++ b/addons/l10n_mx/data/account.account.template.csv @@ -8,7 +8,7 @@ "cuenta115_02","Materia prima y materiales","115.02.01","account.data_account_type_current_assets","l10n_mx.mx_coa","l10n_mx.account_tag_115_02","False" "cuenta115_03","Producción en proceso","115.03.01","account.data_account_type_current_assets","l10n_mx.mx_coa","l10n_mx.account_tag_115_03","False" "cuenta115_04","Productos terminados","115.04.01","account.data_account_type_current_assets","l10n_mx.mx_coa","l10n_mx.account_tag_115_04","False" -"cuenta115_05","Mercancías en tránsito","115.05.01","account.data_account_type_current_assets","l10n_mx.mx_coa","l10n_mx.account_tag_115_05","False" +"cuenta115_05","Mercancías en tránsito","115.05.01","account.data_account_type_current_assets","l10n_mx.mx_coa","l10n_mx.account_tag_115_05","True" "cuenta115_06","Mercancías en poder de terceros","115.06.01","account.data_account_type_current_assets","l10n_mx.mx_coa","l10n_mx.account_tag_115_06","False" "cuenta118_01","IVA acreditable pagado","118.01.01","account.data_account_type_current_assets","l10n_mx.mx_coa","l10n_mx.account_tag_118_01","False" "cuenta119_01","IVA pendiente de pago","119.01.01","account.data_account_type_current_assets","l10n_mx.mx_coa","l10n_mx.account_tag_119_01","False" diff --git a/addons/lunch/security/lunch_security.xml b/addons/lunch/security/lunch_security.xml index 1da0445b001..ba58056ff3d 100644 --- a/addons/lunch/security/lunch_security.xml +++ b/addons/lunch/security/lunch_security.xml @@ -40,6 +40,26 @@ <field name="groups" eval="[(4,ref('lunch.group_lunch_user'))]"/> </record> + <record id="lunch_order_rule_write" model="ir.rule"> + <field name="name">lunch.order: Don't change confirmed order</field> + <field name="model_id" ref="lunch.model_lunch_order"/> + <field name="domain_force">[('state', '!=', 'confirmed'), ('user_id', '=', user.id)]</field> + <field name="perm_read" eval="0"/> + <field name="perm_create" eval="0"/> + <field name="perm_unlink" eval="0"/> + <field name="groups" eval="[(4, ref('base.group_user'))]"/> + </record> + + <record id="lunch_order_rule_write_manager" model="ir.rule"> + <field name="name">manager can do whatever</field> + <field name="model_id" ref="lunch.model_lunch_order"/> + <field name="domain_force">[(1, '=', 1)]</field> + <field name="perm_read" eval="0"/> + <field name="perm_create" eval="0"/> + <field name="perm_unlink" eval="0"/> + <field name="groups" eval="[(4, ref('lunch.group_lunch_manager'))]"/> + </record> + <data noupdate="1"> <record id="base.default_user" model="res.users"> <field name="groups_id" eval="[(4,ref('lunch.group_lunch_manager'))]"/> diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index bf0b0141604..16e0b261291 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -313,6 +313,12 @@ class MailActivity(models.Model): @api.model def create(self, values): activity = super(MailActivity, self).create(values) + need_sudo = False + try: # in multicompany, reading the partner might break + partner_id = activity.user_id.partner_id.id + except exceptions.AccessError: + need_sudo = True + partner_id = activity.user_id.sudo().partner_id.id # send a notification to assigned user; in case of manually done activity also check # target has rights on document otherwise we prevent its creation. Automated activities @@ -321,9 +327,12 @@ class MailActivity(models.Model): if not activity.automated: activity._check_access_assignation() if not self.env.context.get('mail_activity_quick_update', False): - activity.action_notify() + if need_sudo: + activity.sudo().action_notify() + else: + activity.action_notify() - self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id]) + self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[partner_id]) if activity.date_deadline <= fields.Date.today(): self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index 9ed782b40a8..dd92247078f 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -431,7 +431,7 @@ class MailThread(models.AbstractModel): # ------------------------------------------------------ def with_lang(self): - if 'lang' not in self._context: + if not self._context.get("lang"): return self.with_context(lang=self.env.user.lang) return self diff --git a/addons/mail/static/src/js/emojis.js b/addons/mail/static/src/js/emojis.js index 54b995f3bb6..f8684cde541 100644 --- a/addons/mail/static/src/js/emojis.js +++ b/addons/mail/static/src/js/emojis.js @@ -73,6 +73,7 @@ var data = { "<3": "❤️", ":heart": "❤️", // alt. "</3": "💔", + "</3": "💔", ":heart_eyes": "😍", ":turban": "👳", ":+1": "👍", diff --git a/addons/mail/static/src/js/services/mail_notification_manager.js b/addons/mail/static/src/js/services/mail_notification_manager.js index c22276867b1..8b9b10b2de5 100644 --- a/addons/mail/static/src/js/services/mail_notification_manager.js +++ b/addons/mail/static/src/js/services/mail_notification_manager.js @@ -506,7 +506,7 @@ MailManager.include({ } this._removeChannel(channel); this._mailBus.trigger('unsubscribe_from_channel', data.id); - this.do_notify(_("Unsubscribed"), message); + this.do_notify(_t("Unsubscribed"), message); } }, /** diff --git a/addons/mail/static/src/scss/composer.scss b/addons/mail/static/src/scss/composer.scss index 802236afd73..b1e78345d1f 100644 --- a/addons/mail/static/src/scss/composer.scss +++ b/addons/mail/static/src/scss/composer.scss @@ -227,6 +227,8 @@ .o_mail_emoji_container { @include o-position-absolute(auto, 0, 100%, 0); border-bottom-width: 1px; + max-height: calc(100vh - 100%); + overflow-y: auto; } } .o_composer_input { diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml index ea742a29fda..af89eb6b82d 100644 --- a/addons/mail/views/mail_templates.xml +++ b/addons/mail/views/mail_templates.xml @@ -145,7 +145,7 @@ </template> <template id="message_origin_link"> - <p>This <t t-esc="self._description.lower()"/> has been <span t-if="edit">modified</span><span t-if="not edit">created</span> from: + <p>This <t t-esc="self.env['ir.model']._get(self._name).name.lower()"/> has been <span t-if="edit">modified</span><span t-if="not edit">created</span> from: <t t-foreach="origin" t-as="o"> <a href="#" t-att-data-oe-model="o._name" t-att-data-oe-id="o.id"> <t t-esc="o.display_name"/></a><span t-if="origin.ids[-1:] != o.ids">, </span> </t> diff --git a/addons/mail/wizard/mail_compose_message.py b/addons/mail/wizard/mail_compose_message.py index a003f2e42c6..95ae6372c41 100644 --- a/addons/mail/wizard/mail_compose_message.py +++ b/addons/mail/wizard/mail_compose_message.py @@ -306,7 +306,6 @@ class MailComposer(models.TransientModel): 'attachment_ids': [attach.id for attach in self.attachment_ids], 'author_id': self.author_id.id, 'email_from': self.email_from, - 'reply_to': self.reply_to, 'record_name': self.record_name, 'no_auto_thread': self.no_auto_thread, 'mail_server_id': self.mail_server_id.id, diff --git a/addons/mass_mailing/static/src/scss/themes/theme_basic.scss b/addons/mass_mailing/static/src/scss/themes/theme_basic.scss index 00f3a6304bc..d65ea077f5f 100644 --- a/addons/mass_mailing/static/src/scss/themes/theme_basic.scss +++ b/addons/mass_mailing/static/src/scss/themes/theme_basic.scss @@ -1,3 +1,32 @@ + +// TODO remove in master +// Themes scss have to use bg-variant and text-emphasis-variant mixins of BS4 +// to properly work. That use was introduced as a fix in 12.0... but the BS4 +// mixins are unavailable in the mailing assets. As a stable fix cannot +// introduce static code which requires an XML update to not crash, the mixins +// were duplicated here instead. +@mixin bg-variant($parent, $color) { + #{$parent} { + background-color: $color !important; + } + a#{$parent}, + button#{$parent} { + &:hover, &:focus { + background-color: darken($color, 10%) !important; + } + } +} +@mixin text-emphasis-variant($parent, $color) { + #{$parent} { + color: $color !important; + } + a#{$parent} { + &:hover, &:focus { + color: darken($color, 10%) !important; + } + } +} + // ============================ // Mass Mailing "Theme Basic" // ============================ diff --git a/addons/mass_mailing/static/src/scss/themes/theme_default.scss b/addons/mass_mailing/static/src/scss/themes/theme_default.scss index afd4057cfc9..4d49a7ef22c 100644 --- a/addons/mass_mailing/static/src/scss/themes/theme_default.scss +++ b/addons/mass_mailing/static/src/scss/themes/theme_default.scss @@ -24,19 +24,10 @@ $o-mm-def-btn-bg : $o-mm-def-color-gamma; $o-mm-def-btn-text : $o-mm-def-color-beta; // ===== Colors ===== -.bg-beta { - background-color: $o-mm-def-color-zeta; - color: $o-mm-def-color-beta; -} -.text-beta { - color: $o-mm-def-color-zeta; -} -.bg-delta { - background-color: $o-mm-def-color-eta; -} -.bg-gray-lighter { - background-color: $o-mm-def-color-alpha; -} +@include bg-variant(".bg-beta", $o-mm-def-color-zeta); +@include text-emphasis-variant(".text-beta", $o-mm-def-color-zeta); +@include bg-variant(".bg-delta", $o-mm-def-color-eta); +@include bg-variant(".bg-gray-lighter", $o-mm-def-color-alpha); td:not([align]) { // Default browser style but needed so that alignment works on some mail diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index b2417cd83b0..8fb58741a9f 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -205,7 +205,7 @@ class MrpProduction(models.Model): def _compute_picking_ids(self): for order in self: order.picking_ids = self.env['stock.picking'].search([ - ('group_id', '=', order.procurement_group_id.id), + ('group_id', '=', order.procurement_group_id.id), ('group_id', '!=', False), ]) order.delivery_count = len(order.picking_ids) diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py index bcf2d0aa461..ec31b842a7f 100644 --- a/addons/mrp/wizard/change_production_qty.py +++ b/addons/mrp/wizard/change_production_qty.py @@ -62,7 +62,8 @@ class ChangeProductionQty(models.TransientModel): continue move = production.move_raw_ids.filtered(lambda x: x.bom_line_id.id == line.id and x.state not in ('done', 'cancel')) if move: - old_qty = move[0].product_uom_qty + move = move[0] + old_qty = move.product_uom_qty else: old_qty = 0 iterate_key = production._get_document_iterate_key(move) diff --git a/addons/partner_autocomplete/models/res_partner.py b/addons/partner_autocomplete/models/res_partner.py index 9d357fec6d8..1f5a9942c93 100644 --- a/addons/partner_autocomplete/models/res_partner.py +++ b/addons/partner_autocomplete/models/res_partner.py @@ -95,7 +95,7 @@ class ResPartner(models.Model): }) try: return jsonrpc(url=url, params=params, timeout=timeout), False - except (ConnectionError, HTTPError, exceptions.AccessError) as exception: + except (ConnectionError, HTTPError, exceptions.AccessError, exceptions.UserError) as exception: _logger.error('Autocomplete API error: %s' % str(exception)) return False, str(exception) except InsufficientCreditError as exception: diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index b4411b1430b..cdd15360bb8 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -335,7 +335,7 @@ class PosOrder(models.Model): res.append({'data_type': 'tax', 'values': data}) return res - def _create_account_move_line(self, session=None): + def _prepare_account_move_and_lines(self, session=None): # Tricky, via the workflow, we only have one id in the ids variable """Create a account move line of order grouped by products or not.""" @@ -484,6 +484,17 @@ class PosOrder(models.Model): if self and order.company_id.anglo_saxon_accounting: add_anglosaxon_lines(grouped_data) + return { + 'grouped_data': grouped_data, + 'move': move, + } + + def _create_account_move_line(self, session=None): + vals = self._prepare_account_move_and_lines(session) + + grouped_data = vals['grouped_data'] + move = vals['move'] + all_lines = [] for group_key, group_data in grouped_data.items(): for value in group_data: @@ -1162,6 +1173,7 @@ class PosOrderLine(models.Model): class PosOrderLineLot(models.Model): _name = "pos.pack.operation.lot" _description = "Specify product lot/serial number in pos order line" + _rec_name = "lot_name" pos_order_line_id = fields.Many2one('pos.order.line') order_id = fields.Many2one('pos.order', related="pos_order_line_id.order_id", readonly=False) diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 53f3c5f3c25..31a8f88c4af 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -2619,9 +2619,36 @@ exports.Order = Backbone.Model.extend({ }), 0), this.pos.currency.rounding); }, get_total_tax: function() { - return round_pr(this.orderlines.reduce((function(sum, orderLine) { - return sum + orderLine.get_tax(); - }), 0), this.pos.currency.rounding); + if (this.pos.company.tax_calculation_rounding_method === "round_globally") { + // As always, we need: + // 1. For each tax, sum their amount across all order lines + // 2. Round that result + // 3. Sum all those rounded amounts + var groupTaxes = {}; + this.orderlines.each(function (line) { + var taxDetails = line.get_tax_details(); + var taxIds = Object.keys(taxDetails); + for (var t = 0; t<taxIds.length; t++) { + var taxId = taxIds[t]; + if (!(taxId in groupTaxes)) { + groupTaxes[taxId] = 0; + } + groupTaxes[taxId] += taxDetails[taxId]; + } + }); + + var sum = 0; + var taxIds = Object.keys(groupTaxes); + for (var j = 0; j<taxIds.length; j++) { + var taxAmount = groupTaxes[taxIds[j]]; + sum += round_pr(taxAmount, this.pos.currency.rounding); + } + return sum; + } else { + return round_pr(this.orderlines.reduce((function(sum, orderLine) { + return sum + orderLine.get_tax(); + }), 0), this.pos.currency.rounding); + } }, get_total_paid: function() { return round_pr(this.paymentlines.reduce((function(sum, paymentLine) { diff --git a/addons/point_of_sale/views/pos_order_view.xml b/addons/point_of_sale/views/pos_order_view.xml index 6bccce47ce5..088bbd77f37 100644 --- a/addons/point_of_sale/views/pos_order_view.xml +++ b/addons/point_of_sale/views/pos_order_view.xml @@ -55,6 +55,7 @@ <field name="price_subtotal_incl" invisible="1" widget="monetary" force_save="1"/> <field name="tax_ids_after_fiscal_position" widget="many2many_tags" string="Taxes"/> <field name="tax_ids" widget="many2many_tags" invisible="1"/> + <field name="pack_lot_ids" widget="many2many_tags" groups="stock.group_production_lot"/> <field name="notice"/> </group> </form> diff --git a/addons/portal/models/mail_message.py b/addons/portal/models/mail_message.py index afe97f1b8ca..4a0d09580fb 100644 --- a/addons/portal/models/mail_message.py +++ b/addons/portal/models/mail_message.py @@ -20,6 +20,11 @@ class MailMessage(models.Model): message_values = self.read(fields_list) message_tree = dict((m.id, m) for m in self.sudo()) self._message_read_dict_postprocess(message_values, message_tree) + IrAttachmentSudo = self.env['ir.attachment'].sudo() + for message in message_values: + for attachment in message.get('attachment_ids', []): + if not attachment.get('access_token'): + attachment['access_token'] = IrAttachmentSudo.browse(attachment['id']).generate_access_token()[0] return message_values @api.model diff --git a/addons/portal/static/src/xml/portal_chatter.xml b/addons/portal/static/src/xml/portal_chatter.xml index f090965bde2..7d80fc30410 100644 --- a/addons/portal/static/src/xml/portal_chatter.xml +++ b/addons/portal/static/src/xml/portal_chatter.xml @@ -72,7 +72,7 @@ <div class="o_portal_chatter_attachments"> <div class="col-lg-2 col-md-3 text-center" t-foreach='message.attachment_ids' t-as='attachment'> - <a t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank"> + <a t-attf-href="/web/content/#{attachment.id}?download=true&access_token=#{attachment.access_token}" target="_blank"> <div class='oe_attachment_embedded o_image' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/100x80"/> <div class='oe_attachment_name'><t t-raw='attachment.name'/></div> </a> diff --git a/addons/portal/views/portal_templates.xml b/addons/portal/views/portal_templates.xml index 9ded0d59297..99ea33b7289 100644 --- a/addons/portal/views/portal_templates.xml +++ b/addons/portal/views/portal_templates.xml @@ -399,7 +399,7 @@ <input type="text" name="city" t-attf-class="form-control #{error.get('city') and 'is-invalid' or ''}" t-att-value="city or partner.city" /> </div> <div t-attf-class="form-group #{error.get('zip') and 'o_has_error' or ''} col-xl-6"> - <label class="col-form-label" for="zipcode">Zip / Postal Code</label> + <label class="col-form-label label-optional" for="zipcode">Zip / Postal Code</label> <input type="text" name="zipcode" t-attf-class="form-control #{error.get('zip') and 'is-invalid' or ''}" t-att-value="zipcode or partner.zip" /> </div> <div t-attf-class="form-group #{error.get('country_id') and 'o_has_error' or ''} col-xl-6"> diff --git a/addons/project/views/project_views.xml b/addons/project/views/project_views.xml index b97443b410f..d9969a3e205 100644 --- a/addons/project/views/project_views.xml +++ b/addons/project/views/project_views.xml @@ -537,7 +537,6 @@ <field name="priority"/> <field name="stage_id" options='{"group_by_tooltip": {"legend_priority": "Use of stars"}}'/> <field name="user_id"/> - <field name="user_email"/> <field name="sequence"/> <field name="date_deadline"/> <field name="message_needaction_counter"/> diff --git a/addons/purchase_stock/models/stock_rule.py b/addons/purchase_stock/models/stock_rule.py index 90d7476546f..6c0fa5a0df8 100644 --- a/addons/purchase_stock/models/stock_rule.py +++ b/addons/purchase_stock/models/stock_rule.py @@ -270,7 +270,7 @@ class StockRule(models.Model): 'partner_id': partner.id, 'picking_type_id': self.picking_type_id.id, 'company_id': company_id.id, - 'currency_id': partner.with_context(force_company=company_id.id).property_purchase_currency_id.id or self.env.user.company_id.currency_id.id, + 'currency_id': partner.with_context(force_company=company_id.id).property_purchase_currency_id.id or company_id.currency_id.id, 'dest_address_id': values.get('partner_id', False), 'origin': ', '.join(origins), 'payment_term_id': partner.with_context(force_company=company_id.id).property_supplier_payment_term_id.id, diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 4731e22f335..06d1a37ae40 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -1031,7 +1031,8 @@ class SaleOrderLine(models.Model): for line in self: fpos = line.order_id.fiscal_position_id or line.order_id.partner_id.property_account_position_id # If company_id is set, always filter taxes by the company - taxes = line.product_id.taxes_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id) + line_company_id = line.company_id or line.order_id.company_id + taxes = line.product_id.taxes_id.filtered(lambda r: not line_company_id or r.company_id == line_company_id) line.tax_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_shipping_id) if fpos else taxes @api.model diff --git a/addons/sale/tests/test_sale_order.py b/addons/sale/tests/test_sale_order.py index cd115e5bbab..b5dcb29f475 100644 --- a/addons/sale/tests/test_sale_order.py +++ b/addons/sale/tests/test_sale_order.py @@ -239,6 +239,59 @@ class TestSaleOrder(TestCommonSaleNoChart): self.sale_order.amount_untaxed + self.sale_order.amount_tax, 'Taxes should be applied') + def test_so_create_multicompany(self): + # Preparing test Data + user_demo = self.env.ref('base.user_demo') + company_1 = self.env.ref('base.main_company') + company_2 = self.env['res.company'].create({ + 'name': 'company 2', + 'parent_id': company_1.id, + }) + user_demo.write({ + 'groups_id': [(4, self.env.ref('sales_team.group_sale_manager').id, False)], + 'company_ids': [(6, False, [company_1.id])], + 'company_id': company_1.id, + }) + + so_partner = self.env.ref('base.res_partner_2') + so_partner.write({ + 'property_account_position_id': False, + }) + + tax_company_1 = self.env['account.tax'].create({ + 'name': 'T1', + 'amount': 90, + 'company_id': company_1.id, + }) + + tax_company_2 = self.env['account.tax'].create({ + 'name': 'T2', + 'amount': 90, + 'company_id': company_2.id, + }) + + product_shared = self.env['product.template'].create({ + 'name': 'shared product', + 'taxes_id': [(6, False, [tax_company_1.id, tax_company_2.id])], + }) + + # Use case + so_1 = self.env['sale.order'].sudo(user_demo.id).create({ + 'partner_id': so_partner.id, + 'company_id': company_1.id, + }) + so_1.invalidate_cache() + + # This is what is done when importing the csv lines (on sale.order): + # id,order_line/product_id + # __export__.sale_order_37_1bb960ba,Product name + so_1.write({ + 'order_line': [(0, False, {'product_id': product_shared.product_variant_id.id, 'order_id': so_1.id})], + }) + + self.assertEqual(set(so_1.order_line.tax_id.ids), set([tax_company_1.id]), + 'Only taxes from the right company are put by default') + def test_reconciliation_with_so(self): # create SO so = self.env['sale.order'].create({ diff --git a/addons/sale/views/sale_portal_templates.xml b/addons/sale/views/sale_portal_templates.xml index 036b285f9fa..10a8eb7d593 100644 --- a/addons/sale/views/sale_portal_templates.xml +++ b/addons/sale/views/sale_portal_templates.xml @@ -464,12 +464,11 @@ <div t-if="line.discount >= 0" t-field="line.price_unit" - t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}' t-att-style="line.discount and 'text-decoration: line-through' or None" t-att-class="(line.discount and 'text-danger' or '') + ' text-right'" /> <div t-if="line.discount"> - <t t-esc="(1-line.discount / 100.0) * line.price_unit" t-options='{"widget": "monetary", "display_currency": sale_order.pricelist_id.currency_id}'/> + <t t-esc="(1-line.discount / 100.0) * line.price_unit" t-options='{"widget": "float", "decimal_precision": "Product Price"}'/> </div> </td> <td t-if="display_discount" t-attf-class="text-right {{ 'd-none d-sm-table-cell' if report_type == 'html' else '' }}"> diff --git a/addons/sale_purchase/models/sale_order.py b/addons/sale_purchase/models/sale_order.py index 8960907f9ed..0b85753411f 100644 --- a/addons/sale_purchase/models/sale_order.py +++ b/addons/sale_purchase/models/sale_order.py @@ -114,8 +114,8 @@ class SaleOrderLine(models.Model): decreased_values = {} if 'product_uom_qty' in values: precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') - increased_lines = self.sudo().filtered(lambda r: r.purchase_line_count and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1) - decreased_lines = self.sudo().filtered(lambda r: r.purchase_line_count and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == 1) + increased_lines = self.sudo().filtered(lambda r: r.product_id.service_to_purchase and r.purchase_line_count and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1) + decreased_lines = self.sudo().filtered(lambda r: r.product_id.service_to_purchase and r.purchase_line_count and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == 1) increased_values = {line.id: line.product_uom_qty for line in increased_lines} decreased_values = {line.id: line.product_uom_qty for line in decreased_lines} diff --git a/addons/stock/models/res_config_settings.py b/addons/stock/models/res_config_settings.py index 3eca680cce7..f5116ebf7e6 100644 --- a/addons/stock/models/res_config_settings.py +++ b/addons/stock/models/res_config_settings.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import api, fields, models, _ +from odoo.exceptions import UserError class ResConfigSettings(models.TransientModel): @@ -64,6 +65,13 @@ class ResConfigSettings(models.TransientModel): def _onchange_group_stock_production_lot(self): if not self.group_stock_production_lot: self.group_lot_on_delivery_slip = False + tracked_products = self.env['product.template'].search([('tracking', 'in', ['lot', 'serial']),]) + if not self.group_stock_production_lot and tracked_products: + names = ", ".join(tracked_products.mapped('display_name') if len(tracked_products) <= 10 + else tracked_products[:10].mapped('display_name') + ["..."]) + raise UserError(_("You should not remove the 'lots and serial numbers' " + "option while the following products are still tracked by lot " + "or serial number:\n %s") % names) @api.onchange('group_stock_adv_location') def onchange_adv_location(self): diff --git a/addons/stock/security/stock_security.xml b/addons/stock/security/stock_security.xml index a851beb0cfc..8c915b4969e 100644 --- a/addons/stock/security/stock_security.xml +++ b/addons/stock/security/stock_security.xml @@ -99,7 +99,7 @@ <field name="name">stock_move multi-company</field> <field name="model_id" search="[('model','=','stock.move')]" model="ir.model"/> <field name="global" eval="True"/> - <field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> + <field name="domain_force">['|','|',('company_id','=',False),('company_id','child_of',[user.company_id.id]),('location_dest_id.company_id', '=', False)]</field> </record> <record model="ir.rule" id="stock_move_line_rule"> diff --git a/addons/stock/views/product_views.xml b/addons/stock/views/product_views.xml index c624721d40a..64be1d7a250 100644 --- a/addons/stock/views/product_views.xml +++ b/addons/stock/views/product_views.xml @@ -194,6 +194,7 @@ <record model="ir.ui.view" id="product_form_view_procurement_button"> <field name="name">product.product.procurement</field> <field name="model">product.product</field> + <field name="groups_id" eval="[(4, ref('stock.group_stock_user'))]"/> <field name="inherit_id" ref="product.product_normal_form_view"/> <field name="arch" type="xml"> <data> @@ -285,6 +286,7 @@ <record model="ir.ui.view" id="product_template_form_view_procurement_button"> <field name="name">product.template_procurement</field> <field name="model">product.template</field> + <field name="groups_id" eval="[(4, ref('stock.group_stock_user'))]"/> <field name="inherit_id" ref="product.product_template_only_form_view"/> <field name="arch" type="xml"> <data> diff --git a/addons/stock_dropshipping/__manifest__.py b/addons/stock_dropshipping/__manifest__.py index de26d991087..9d092675cf2 100644 --- a/addons/stock_dropshipping/__manifest__.py +++ b/addons/stock_dropshipping/__manifest__.py @@ -21,7 +21,7 @@ internal transfer document is needed. """, 'depends': ['sale_purchase', 'sale_stock'], - 'data': ['data/stock_data.xml'], + 'data': ['data/stock_data.xml', 'views/sale_order_views.xml'], 'installable': True, 'auto_install': False, } diff --git a/addons/stock_dropshipping/tests/test_dropship.py b/addons/stock_dropshipping/tests/test_dropship.py index f70897f9eb8..9048fb2d009 100644 --- a/addons/stock_dropshipping/tests/test_dropship.py +++ b/addons/stock_dropshipping/tests/test_dropship.py @@ -45,7 +45,7 @@ class TestDropship(common.TransactionCase): self.assertAlmostEqual(po_line.product_qty, 1.00) # Update qty on SO and check PO - so.order_line.product_uom_qty = 2.00 + so.write({'order_line': [[1, so.order_line.id, {'product_uom_qty': 2.00}]]}) self.assertAlmostEqual(po_line.product_qty, 2.00) # Create a new so line diff --git a/addons/stock_dropshipping/views/sale_order_views.xml b/addons/stock_dropshipping/views/sale_order_views.xml new file mode 100644 index 00000000000..b06b48ccab9 --- /dev/null +++ b/addons/stock_dropshipping/views/sale_order_views.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<odoo> + <data> + <record id="view_order_form_inherit_sale_stock" model="ir.ui.view"> + <field name="name">sale.order.form.sale.dropshipping</field> + <field name="model">sale.order</field> + <field name="inherit_id" ref="sale.view_order_form"/> + <field name="arch" type="xml"> + <xpath expr="//page/field[@name='order_line']/form//field[@name='product_updatable']" position="after"> + <field name="purchase_line_count" invisible="1"/> + </xpath> + <xpath expr="//page/field[@name='order_line']/form//field[@name='product_id']" position="attributes"> + <attribute name="attrs">{'readonly': ['|', ('product_updatable', '=', False), ('purchase_line_count', '>', 0)], 'required': [('display_type', '=', False)],}</attribute> + </xpath> + <xpath expr="//page/field[@name='order_line']/tree/field[@name='product_updatable']" position="after"> + <field name="purchase_line_count" invisible="1"/> + </xpath> + <xpath expr="//page/field[@name='order_line']/tree/field[@name='product_id']" position="attributes"> + <attribute name="attrs">{'readonly': ['|', ('product_updatable', '=', False), ('purchase_line_count', '>', 0)], 'required': [('display_type', '=', False)],}</attribute> + </xpath> + </field> + </record> + </data> +</odoo> diff --git a/addons/test_mail/tests/test_performance.py b/addons/test_mail/tests/test_performance.py index be51e1be5d9..b4420570a1c 100644 --- a/addons/test_mail/tests/test_performance.py +++ b/addons/test_mail/tests/test_performance.py @@ -485,7 +485,7 @@ class TestHeavyMailPerformance(TransactionCase): 'user_id': self.env.uid, }) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id) - with self.assertQueryCount(__system__=60, emp=77): # com runbot: 61 - 76 // test_mail only: 60 - 72 + with self.assertQueryCount(__system__=61, emp=77): # com runbot: 61 - 76 // test_mail only: 60 - 72 rec.write({'user_id': self.user_portal.id}) self.assertEqual(rec.message_partner_ids, self.partners | self.env.user.partner_id | self.user_portal.partner_id) # write tracking message diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 9d95ae485a9..0f0bf9da958 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -198,11 +198,11 @@ def concat_xml(file_list): :param list(str) file_list: list of files to check :returns: (concatenation_result, checksum) - :rtype: (str, str) + :rtype: (bytes, str) """ checksum = hashlib.new('sha1') if not file_list: - return '', checksum.hexdigest() + return b'', checksum.hexdigest() root = None for fname in file_list: diff --git a/addons/web/static/src/js/fields/basic_fields.js b/addons/web/static/src/js/fields/basic_fields.js index b3bf09ab88d..437543ef184 100644 --- a/addons/web/static/src/js/fields/basic_fields.js +++ b/addons/web/static/src/js/fields/basic_fields.js @@ -2526,6 +2526,7 @@ var JournalDashboardGraph = AbstractField.extend({ * Called when the field is detached from the DOM. */ on_detach_callback: function () { + this.chart.tooltip.hidden(true); this._isInDOM = false; }, diff --git a/addons/web/static/src/js/views/basic/basic_model.js b/addons/web/static/src/js/views/basic/basic_model.js index ca9c03ce7d6..1a7f0b5f079 100644 --- a/addons/web/static/src/js/views/basic/basic_model.js +++ b/addons/web/static/src/js/views/basic/basic_model.js @@ -796,6 +796,7 @@ var BasicModel = AbstractModel.extend({ }); _.each(fields, function (field) { var dataPoint; + record.data[field.name] = null; if (field.type === 'many2one') { if (field.value) { var id = _.isArray(field.value) ? field.value[0] : field.value; diff --git a/addons/web/static/src/js/views/calendar/calendar_controller.js b/addons/web/static/src/js/views/calendar/calendar_controller.js index b3a04ec9a2b..bbfcb02def5 100644 --- a/addons/web/static/src/js/views/calendar/calendar_controller.js +++ b/addons/web/static/src/js/views/calendar/calendar_controller.js @@ -55,6 +55,7 @@ var CalendarController = AbstractController.extend({ this.readonlyFormViewId = params.readonlyFormViewId; this.mapping = params.mapping; this.context = params.context; + this.previousOpen = null; // The quickCreating attribute ensures that we don't do several create this.quickCreating = false; }, @@ -271,7 +272,8 @@ var CalendarController = AbstractController.extend({ title += ': ' + this.renderer.arch.attrs.string; } if (this.eventOpenPopup) { - new dialogs.FormViewDialog(self, { + if (this.previousOpen) { this.previousOpen.close(); } + this.previousOpen = new dialogs.FormViewDialog(self, { res_model: this.modelName, context: context, title: title, @@ -283,7 +285,8 @@ var CalendarController = AbstractController.extend({ } self.reload(); }, - }).open(); + }); + this.previousOpen.open(); } else { this.do_action({ type: 'ir.actions.act_window', diff --git a/addons/web/static/src/js/views/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js index aed795a6b9b..ed5af4c96bb 100644 --- a/addons/web/static/src/js/views/list/list_renderer.js +++ b/addons/web/static/src/js/views/list/list_renderer.js @@ -708,7 +708,7 @@ var ListRenderer = BasicRenderer.extend({ } if (field.type === 'float' || field.type === 'integer' || field.type === 'monetary') { - $th.css({ textAlign: 'right' }); + $th.addClass('o_list_number_th'); } if (config.debug) { diff --git a/addons/web/static/src/scss/list_view.scss b/addons/web/static/src/scss/list_view.scss index 9ccf32cde2e..ab3707e6ddf 100644 --- a/addons/web/static/src/scss/list_view.scss +++ b/addons/web/static/src/scss/list_view.scss @@ -19,6 +19,9 @@ &:not(.o_column_sortable):hover { cursor: default; } + &.o_list_number_th { + text-align: right; + } } } tbody > tr > td:not(.o_list_record_selector) { diff --git a/addons/web/static/tests/views/calendar_tests.js b/addons/web/static/tests/views/calendar_tests.js index e217e47f3d1..d8a34561d87 100644 --- a/addons/web/static/tests/views/calendar_tests.js +++ b/addons/web/static/tests/views/calendar_tests.js @@ -5,6 +5,7 @@ var AbstractStorageService = require('web.AbstractStorageService'); var CalendarView = require('web.CalendarView'); var CalendarRenderer = require('web.CalendarRenderer'); var Dialog = require('web.Dialog'); +var ViewDialogs = require('web.view_dialogs'); var fieldUtils = require('web.field_utils'); var mixins = require('web.mixins'); var RamStorage = require('web.RamStorage'); @@ -475,6 +476,63 @@ QUnit.module('Views', { calendar.destroy(); }); + QUnit.test('open multiple event form at the same time', async function (assert) { + assert.expect(2); + + var prom = testUtils.makeTestPromise(); + var counter = 0; + testUtils.mock.patch(ViewDialogs.FormViewDialog, { + open: function () { + counter++; + this.options = _.omit(this.options, 'fields_view'); // force loadFieldView + return this._super.apply(this, arguments); + }, + loadFieldView: function () { + var self = this; + var args = arguments; + var _super = this._super; + return prom.then(function () { + return _super.apply(self, args); + }); + }, + }); + + var event = $.Event(); + var calendar = await createCalendarView({ + View: CalendarView, + model: 'event', + data: this.data, + arch: + '<calendar class="o_calendar_test" '+ + 'string="Events" ' + + 'event_open_popup="true" '+ + 'date_start="start" '+ + 'quick_add="False" '+ + 'date_stop="stop" '+ + 'all_day="allday" '+ + 'mode="month" '+ + 'readonly_form_view_id="1">'+ + '<field name="name"/>'+ + '</calendar>', + archs: archs, + viewOptions: { + initialDate: initialDate, + }, + }); + + var $cell = calendar.$('.fc-day-grid .fc-row:eq(2) .fc-day:eq(2)'); + for (var i = 0; i < 5; i++) { + await testUtils.dom.triggerMouseEvent($cell, "mousedown"); + await testUtils.dom.triggerMouseEvent($cell, "mouseup"); + } + prom.resolve(); + await testUtils.nextTick(); + assert.equal(counter, 5, "there should had been 5 attemps to open a modal"); + assert.containsOnce($('body'), '.modal', "there should be only one open modal"); + + calendar.destroy(); + }); + QUnit.test('create event with timezone in week mode European locale', async function (assert) { assert.expect(5); diff --git a/addons/website/models/ir_ui_view.py b/addons/website/models/ir_ui_view.py index 946ed9fe7f8..e81b6ebaf35 100644 --- a/addons/website/models/ir_ui_view.py +++ b/addons/website/models/ir_ui_view.py @@ -133,12 +133,8 @@ class View(models.Model): ('website_id', '!=', None), ]) for specific_parent_view in specific_parent_views: - record.copy({ - # Set key to avoid copy() to generate an unique key as - # we want the specific view to have the same key - 'key': record.key, + record.with_context(website_id=specific_parent_view.website_id.id).write({ 'inherit_id': specific_parent_view.id, - 'website_id': specific_parent_view.website_id.id, }) return records diff --git a/addons/website/models/website_menu.py b/addons/website/models/website_menu.py index 202f6e19039..4afb9bd63e5 100644 --- a/addons/website/models/website_menu.py +++ b/addons/website/models/website_menu.py @@ -149,7 +149,12 @@ class Menu(models.Model): if menu_id.page_id: menu_id.page_id = None else: - page = self.env['website.page'].search(['|', ('url', '=', menu['url']), ('url', '=', '/' + menu['url'])], limit=1) + domain = self.env["website"].website_domain(website_id) + [ + "|", + ("url", "=", menu["url"]), + ("url", "=", "/" + menu["url"]), + ] + page = self.env["website.page"].search(domain, limit=1) if page: menu['page_id'] = page.id menu['url'] = page.url diff --git a/addons/website/static/src/js/widgets/theme.js b/addons/website/static/src/js/widgets/theme.js index 352c79b6427..01befb7c925 100644 --- a/addons/website/static/src/js/widgets/theme.js +++ b/addons/website/static/src/js/widgets/theme.js @@ -532,6 +532,34 @@ var ThemeCustomizeDialog = Dialog.extend({ _setActive: function () { var self = this; + // First enforce that all input groups have only one element checked as + // it is supposed to be (it might not be the case on initialization, for + // exemple if we had data-xmlid="A" and data-xmlid="A,B" and if A and B + // are active, the 2 related inputs would be checked). + var $radioXMLInputs = this.$inputs.filter('[type="radio"][data-xmlid]'); + var optionNames = _.uniq(_.map($radioXMLInputs, function (option) { + return option.name; + })); + _.each(optionNames, function (optionName) { + var $inputs = $radioXMLInputs.filter('[name="' + optionName + '"]:checked'); + if ($inputs.length > 1) { + $inputs.prop('checked', false); + + var maxNbXMLIDs = -1; + var $maxInput = null; + _.each($inputs, function (input) { + var $input = $(input); + var xmlID = $input.data('xmlid'); + var nbXMLIDs = xmlID ? xmlID.split(',').length : 0; + if (nbXMLIDs >= maxNbXMLIDs) { + maxNbXMLIDs = nbXMLIDs; + $maxInput = $input; + } + }); + $maxInput.prop('checked', true); + } + }); + // Look at all options to see if they are enabled or disabled var $enable = this.$inputs.filter(':checked'); diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss index e384e8df34b..661aa693227 100644 --- a/addons/website/static/src/scss/website.scss +++ b/addons/website/static/src/scss/website.scss @@ -534,8 +534,8 @@ table.table_desc tr td { // Default &.s_carousel_default { // Controls - chevron - .carousel-control-prev-icon:before { content: "\f053"; margin-left: 1.5rem; } - .carousel-control-next-icon:before { content: "\f054"; margin-right: 1.5rem; } + .carousel-control-prev-icon:before { content: "\f053" #{"/*rtl:'\f054'*/"}; margin-left: 1.5rem; } + .carousel-control-next-icon:before { content: "\f054" #{"/*rtl:'\f053'*/"}; margin-right: 1.5rem; } .carousel-control-prev-icon:before, .carousel-control-next-icon:before { background-color: rgba(0,0,0,0); diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py index b8262bb11b8..38b3311ecbd 100644 --- a/addons/website/tests/test_views.py +++ b/addons/website/tests/test_views.py @@ -811,6 +811,43 @@ class TestCowViewSaving(common.TransactionCase): all_title_updated = specific_view.website_meta_title == self.base_view.website_meta_title == "A bug got fixed by updating this field" self.assertEqual(all_title_updated, True, "Update on top level generic views should also be applied on specific views") + def test_module_new_inherit_view_on_parent_already_forked_xpath_replace(self): + """ Deeper, more specific test of above behavior. + A module install should add/update the COW view (if allowed fields, + eg not modified or prohibited (website_id, inherit_id..)). + This test ensure it does not crash if the child view is a primary view. + """ + View = self.env['ir.ui.view'] + + # Simulate layout views + base_view = View.create({ + 'name': 'Main Frontend Layout', + 'type': 'qweb', + 'arch': '<t t-call="web.layout"><t t-set="head_website"/></t>', + 'key': '_portal.frontend_layout', + }).with_context(load_all_views=True) + + inherit_view = View.create({ + 'name': 'Main layout', + 'mode': 'extension', + 'inherit_id': base_view.id, + 'arch': '<xpath expr="//t[@t-set=\'head_website\']" position="replace"><t t-call-assets="web_editor.summernote" t-js="false" groups="website.group_website_publisher"/></xpath>', + 'key': '_website.layout', + }) + + # Trigger cow on website_sale hierarchy for website 1 + base_view.with_context(website_id=1).write({'name': 'Main Frontend Layout (W1)'}) + + # Simulate website_sale_comparison install, that's the real test, it + # should not crash. + View._load_records([dict(xml_id='_website_forum.layout', values={ + 'name': 'Forum Layout', + 'mode': 'primary', + 'inherit_id': inherit_view.id, + 'arch': '<xpath expr="//t[@t-call-assets=\'web_editor.summernote\'][@t-js=\'false\']" position="attributes"><attribute name="groups"/></xpath>', + 'key': '_website_forum.layout', + })]) + def test_multiple_inherit_level(self): """ Test multi-level inheritance: Base diff --git a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml index d67c0fb5edc..fe2837ec080 100644 --- a/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml +++ b/addons/website_crm_partner_assign/views/website_crm_partner_assign_templates.xml @@ -283,7 +283,7 @@ </div> <div class="form-group"> <label class="col-form-label hdd4" for="description">Description</label> - <textarea row="3" name="description" class="form-control description"></textarea> + <textarea rows="3" name="description" class="form-control description"></textarea> </div> </main> <footer class="modal-footer"> @@ -691,7 +691,7 @@ </div> <div class="form-group"> <label class="col-form-label" for="activity_summary">Details Next Activity</label> - <textarea row="3" name="activity_summary" class="form-control activity_summary"><t t-esc="user_activity.summary"/></textarea> + <textarea rows="3" name="activity_summary" class="form-control activity_summary"><t t-esc="user_activity.summary"/></textarea> </div> <div class="form-group"> <label class="col-form-label" for="activity_date_deadline">Next Activity Date</label> diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js index 88dd7e222a7..832d29936ab 100644 --- a/addons/website_forum/static/src/js/website_forum.js +++ b/addons/website_forum/static/src/js/website_forum.js @@ -24,6 +24,9 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({ 'click .vote_up:not(.karma_required), .vote_down:not(.karma_required)': '_onVotePostClick', 'click .o_js_validation_queue a[href*="/validate"]': '_onValidationQueueClick', 'click .accept_answer:not(.karma_required)': '_onAcceptAnswerClick', + 'click .validate_answer [data-href]': '_onAcceptAnswerClick', + 'mouseenter .validate_answer [data-href]': '_onRemoveValidAnswerMouse', + 'mouseleave .validate_answer [data-href]': '_onRemoveValidAnswerMouse', 'click .favourite_question': '_onFavoriteQuestionClick', 'click .comment_delete': '_onDeleteCommentClick', 'click .notification_close': '_onCloseNotificationClick', @@ -350,6 +353,8 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({ */ _onAcceptAnswerClick: function (ev) { ev.preventDefault(); + var self = this; + var $acceptAnswerLinks = this.$('.accept_answer'); var $link = $(ev.currentTarget); this._rpc({ route: $link.data('href'), @@ -368,19 +373,41 @@ publicWidget.registry.websiteForum = publicWidget.Widget.extend({ $link.parent().append($warning); } } else { + $acceptAnswerLinks.removeClass('oe_answer_true') + .addClass('oe_answer_false'); $link.toggleClass('oe_answer_true', !!data) .toggleClass('oe_answer_false', !data); + + // TODO in master, review the utility of this function... + self._onCheckAnswerStatus(ev); + + // If we are removing an accepted answer, reload the page as the + // design is quite different with or without an accepted answer. + if ($link.closest('.validate_answer').length) { + window.location.reload(); + } } }); - this._onCheckAnswerStatus(ev); + }, + /** + * @private + * @param {Event} ev + */ + _onRemoveValidAnswerMouse: function (ev) { + var hover = (ev.type === 'mouseenter'); + $(ev.currentTarget).find('.fa') + .toggleClass('fa-times-circle text-danger', hover) + .toggleClass('fa-check-circle text-success', !hover); }, /** * @private * @param {Event} ev */ _onCheckAnswerStatus: function (ev) { - var $link = $(ev.currentTarget); - $link.toggleClass('text-success', !$link.hasClass('oe_answer_true')); + _.each(this.$('.accept_answer'), function (link) { + var $link = $(link); + $link.toggleClass('text-success', $link.hasClass('oe_answer_true')); + }); }, /** * @private diff --git a/addons/website_forum/static/src/scss/website_forum.scss b/addons/website_forum/static/src/scss/website_forum.scss index a5b0a4f1db4..59ed45536ef 100644 --- a/addons/website_forum/static/src/scss/website_forum.scss +++ b/addons/website_forum/static/src/scss/website_forum.scss @@ -47,16 +47,23 @@ $bronze: #eea91e; } .validate_answer { - @include o-position-absolute(0,0,0,0); + @include o-position-absolute(0, auto, 0, 0); + width: 4px; height: 100%; - .bg-success { - @include o-position-absolute(0,0,0,0); - height: 100%; - width: 4px; + z-index: 1; // On top of card content if an alert is displayed inside + + > .bg-success { + @include o-position-absolute(0, 0, 0, 0); } - span { - @include o-position-absolute(10px,0,0,-9px); + > span { + @include o-position-absolute(10px, 50%); + transform: translateX(50%); font-size: 20px; + + &[data-href] { + cursor: pointer; + } + i { padding: 2px 3px; border-radius: 100%; @@ -85,6 +92,10 @@ $bronze: #eea91e; .favourite_question { color: $gray-400 !important; + + &.forum_favourite_question { + color: $gold !important; + } } .oe_grey { @@ -186,10 +197,6 @@ img.o_forum_avatar_big { width: auto; } -.forum_favourite_question { - color: $gold !important; -} - .o_forum_user_bio_expand { position: absolute; display: none; diff --git a/addons/website_forum/views/website_forum.xml b/addons/website_forum/views/website_forum.xml index 45160c618ef..d5c505a8e81 100644 --- a/addons/website_forum/views/website_forum.xml +++ b/addons/website_forum/views/website_forum.xml @@ -800,7 +800,10 @@ <div class="card-body"> <div t-if="answer.is_correct" class="validate_answer"> <div class="bg-success"></div> - <span title="Question has accepted answer" role="img" aria-label="Question has accepted answer"><i class="fa fa-check-circle text-success bg-white"/></span> + <span title="Question has accepted answer" role="img" aria-label="Question has accepted answer" + t-attf-data-href="/forum/#{slug(question.forum_id)}/post/#{slug(answer)}/toggle_correct"> + <i class="fa fa-check-circle text-success bg-white"/> + </span> </div> <div class="row"> <div class="col-1"> diff --git a/addons/website_sale/tests/test_sale_process.py b/addons/website_sale/tests/test_sale_process.py index a0fc55c4106..9cf6f5c5613 100644 --- a/addons/website_sale/tests/test_sale_process.py +++ b/addons/website_sale/tests/test_sale_process.py @@ -130,3 +130,19 @@ class TestWebsiteSaleCheckoutAddress(odoo.tests.TransactionCase): self.default_address_values['partner_id'] = new_partner.id self.WebsiteSaleController.address(**self.default_address_values) self.assertEqual(new_partner.company_id, self.website.company_id, "Public user edited billing (the partner itself) should not get its company modified.") + + def test_04_apply_empty_pl(self): + ''' Ensure empty pl code reset the applied pl ''' + so = self._create_so(self.env.user.partner_id.id) + eur_pl = self.env['product.pricelist'].create({ + 'name': 'EUR_test', + 'website_id': self.website.id, + 'code': 'EUR_test', + }) + + with MockRequest(self.env, website=self.website, sale_order_id=so.id): + self.WebsiteSaleController.pricelist('EUR_test') + self.assertEqual(so.pricelist_id, eur_pl, "Ensure EUR_test is applied") + + self.WebsiteSaleController.pricelist('') + self.assertNotEqual(so.pricelist_id, eur_pl, "Pricelist should be removed when sending an empty pl code") diff --git a/addons/website_sale_wishlist/models/product_wishlist.py b/addons/website_sale_wishlist/models/product_wishlist.py index 1bcbe57baef..0a67caa597c 100644 --- a/addons/website_sale_wishlist/models/product_wishlist.py +++ b/addons/website_sale_wishlist/models/product_wishlist.py @@ -33,7 +33,7 @@ class ProductWishlist(models.Model): else: wish = self.search([("partner_id", "=", self.env.user.partner_id.id)]) - return wish.filtered('product_id.product_tmpl_id.website_published') + return wish.sudo().filtered('product_id.product_tmpl_id.website_published') @api.model def _add_to_wishlist(self, pricelist_id, currency_id, website_id, price, product_id, partner_id=False): diff --git a/doc/cla/individual/Tatider.md b/doc/cla/individual/Tatider.md new file mode 100644 index 00000000000..b2468a491de --- /dev/null +++ b/doc/cla/individual/Tatider.md @@ -0,0 +1,11 @@ +Finland, 2019-07-31 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Tatiana Deribina t.i.deribina@gmail.com https://github.com/Tatider diff --git a/doc/cla/individual/anhvu-sg.md b/doc/cla/individual/anhvu-sg.md new file mode 100644 index 00000000000..125880cbcd7 --- /dev/null +++ b/doc/cla/individual/anhvu-sg.md @@ -0,0 +1,11 @@ +Vietnam, 2019-07-27 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Anh Vu <vuna2004@gmail.com> https://github.com/anhvu-sg diff --git a/doc/cla/individual/karol1985.md b/doc/cla/individual/karol1985.md new file mode 100644 index 00000000000..b595b686368 --- /dev/null +++ b/doc/cla/individual/karol1985.md @@ -0,0 +1,11 @@ +Poland, 2019-07-31 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Karol Rybak karolrybak85@gmail.com https://github.com/karol1985 diff --git a/doc/cla/individual/stfnndrsn.md b/doc/cla/individual/stfnndrsn.md new file mode 100644 index 00000000000..021aec0aca8 --- /dev/null +++ b/doc/cla/individual/stfnndrsn.md @@ -0,0 +1,11 @@ +Denmark, 2019-07-24 + + I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + + I declare that I am authorized and able to make this agreement and sign this +declaration. + + Signed, + + Stefan Andersen <stfnandersen@gmail.com> https://github.com/stfnndrsn diff --git a/odoo/addons/base/models/ir_ui_view.py b/odoo/addons/base/models/ir_ui_view.py index e8d955137af..95bfc52c48e 100644 --- a/odoo/addons/base/models/ir_ui_view.py +++ b/odoo/addons/base/models/ir_ui_view.py @@ -674,9 +674,19 @@ actual arch. if node.getparent() is None: source = copy.deepcopy(spec[0]) else: + replaced_node_tag = None for child in spec: if child.get('position') == 'move': child = extract(child) + + if self._context.get('inherit_branding') and not replaced_node_tag and child.tag is not etree.Comment: + # To make a correct branding, we need to + # - know exactly which node has been replaced + # - store it before anything else has altered the Tree + # Do it exactly here :D + child.set('meta-oe-xpath-replacing', node.tag) + replaced_node_tag = node.tag # We just store the replaced node tag on the first child of the xpath replacing it + node.addprevious(child) node.getparent().remove(node) elif pos == 'attributes': @@ -1297,6 +1307,11 @@ actual arch. if child.get('data-oe-xpath'): # injected by view inheritance, skip otherwise # generated xpath is incorrect + # Also, if a node is known to have been replaced during applying xpath + # increment its index to compute an accurate xpath for susequent nodes + replaced_node_tag = child.attrib.pop('meta-oe-xpath-replacing', None) + if replaced_node_tag: + indexes[replaced_node_tag] += 1 self.distribute_branding(child) else: indexes[child.tag] += 1 diff --git a/odoo/addons/base/rng/gantt_view.rng b/odoo/addons/base/rng/gantt_view.rng index dbc216df10c..73d18b98b7c 100644 --- a/odoo/addons/base/rng/gantt_view.rng +++ b/odoo/addons/base/rng/gantt_view.rng @@ -58,7 +58,7 @@ </rng:optional> </rng:interleave> - <rng:empty/> + <rng:empty/> </rng:element> </rng:define> <rng:start> diff --git a/odoo/addons/base/tests/test_views.py b/odoo/addons/base/tests/test_views.py index 6c371741855..e7f0d850ee2 100644 --- a/odoo/addons/base/tests/test_views.py +++ b/odoo/addons/base/tests/test_views.py @@ -674,6 +674,115 @@ class TestTemplating(ViewCase): second.get('data-oe-id'), "second should come from the extension view") + def test_branding_inherit_replace_node(self): + view1 = self.View.create({ + 'name': "Base view", + 'type': 'qweb', + 'arch': """<hello> + <world></world> + <world><t t-esc="hello"/></world> + <world></world> + </hello> + """ + }) + self.View.create({ + 'name': "Extension", + 'type': 'qweb', + 'inherit_id': view1.id, + 'arch': """<xpath expr="/hello/world[1]" position="replace"> + <world>Is a ghetto</world> + <world>Wonder when I'll find paradise</world> + </xpath> + """ + }) + + arch_string = view1.with_context(inherit_branding=True).read_combined(['arch'])['arch'] + + arch = etree.fromstring(arch_string) + self.View.distribute_branding(arch) + + # First world - has been replaced by inheritance + [initial] = arch.xpath('/hello[1]/world[1]') + self.assertEqual( + '/xpath/world[1]', + initial.get('data-oe-xpath'), + 'Inherited nodes have correct xpath') + + # Second world added by inheritance + [initial] = arch.xpath('/hello[1]/world[2]') + self.assertEqual( + '/xpath/world[2]', + initial.get('data-oe-xpath'), + 'Inherited nodes have correct xpath') + + # Third world - is not editable + [initial] = arch.xpath('/hello[1]/world[3]') + self.assertFalse( + initial.get('data-oe-xpath'), + 'node containing t-esc is not branded') + + # The most important assert + # Fourth world - should have a correct oe-xpath, which is 3rd in main view + [initial] = arch.xpath('/hello[1]/world[4]') + self.assertEqual( + '/hello[1]/world[3]', + initial.get('data-oe-xpath'), + "The node's xpath position should be correct") + + def test_branding_inherit_replace_node2(self): + view1 = self.View.create({ + 'name': "Base view", + 'type': 'qweb', + 'arch': """<hello> + <world></world> + <world><t t-esc="hello"/></world> + <world></world> + </hello> + """ + }) + self.View.create({ + 'name': "Extension", + 'type': 'qweb', + 'inherit_id': view1.id, + 'arch': """<xpath expr="/hello/world[1]" position="replace"> + <war>Is a ghetto</war> + <world>Wonder when I'll find paradise</world> + </xpath> + """ + }) + + arch_string = view1.with_context(inherit_branding=True).read_combined(['arch'])['arch'] + + arch = etree.fromstring(arch_string) + self.View.distribute_branding(arch) + + [initial] = arch.xpath('/hello[1]/war[1]') + self.assertEqual( + '/xpath/war', + initial.get('data-oe-xpath'), + 'Inherited nodes have correct xpath') + + # First world: from inheritance + [initial] = arch.xpath('/hello[1]/world[1]') + self.assertEqual( + '/xpath/world', + initial.get('data-oe-xpath'), + 'Inherited nodes have correct xpath') + + # Second world - is not editable + [initial] = arch.xpath('/hello[1]/world[2]') + self.assertFalse( + initial.get('data-oe-xpath'), + 'node containing t-esc is not branded') + + # The most important assert + # Third world - should have a correct oe-xpath, which is 3rd in main view + [initial] = arch.xpath('/hello[1]/world[3]') + self.assertEqual( + '/hello[1]/world[3]', + initial.get('data-oe-xpath'), + "The node's xpath position should be correct") + def test_branding_primary_inherit(self): view1 = self.View.create({ 'name': "Base view", diff --git a/odoo/addons/base/views/res_company_views.xml b/odoo/addons/base/views/res_company_views.xml index db4664ae7b8..afd80df1ced 100644 --- a/odoo/addons/base/views/res_company_views.xml +++ b/odoo/addons/base/views/res_company_views.xml @@ -30,7 +30,7 @@ </div> </group> <group> - <field name="website" widget="url" placeholder="e.g. www.odoo.com"/> + <field name="website" widget="url" placeholder="e.g. https://www.odoo.com"/> <field name="phone"/> <field name="email"/> <field name="vat"/> diff --git a/odoo/addons/base/views/res_partner_views.xml b/odoo/addons/base/views/res_partner_views.xml index a11f39104cf..fe046607001 100644 --- a/odoo/addons/base/views/res_partner_views.xml +++ b/odoo/addons/base/views/res_partner_views.xml @@ -117,7 +117,7 @@ <field name="zip" placeholder="ZIP" class="o_address_zip"/> <field name="country_id" placeholder="Country" class="o_address_country" options='{"no_open": True, "no_create": True}'/> </div> - <field name="website" widget="url" placeholder="e.g. www.odoo.com"/> + <field name="website" widget="url" placeholder="e.g. https://www.odoo.com"/> </group> <group> </group> @@ -263,7 +263,7 @@ <field name="mobile" widget="phone"/> <field name="user_ids" invisible="1"/> <field name="email" widget="email" context="{'gravatar_image': True}" attrs="{'required': [('user_ids','!=', [])]}"/> - <field name="website" widget="url" placeholder="e.g. www.odoo.com"/> + <field name="website" widget="url" placeholder="e.g. https://www.odoo.com"/> <field name="title" options='{"no_open": True}' attrs="{'invisible': [('is_company', '=', True)]}"/> <field name="lang"/> |
