diff --git a/MANIFEST.in b/MANIFEST.in index f2264ad41be..63a2bbe99fa 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,7 @@ include requirements.txt include LICENSE include README.md +include odoo/addons/mail/static/scripts/odoo-mailgate.py recursive-include odoo *.css recursive-include odoo *.csv recursive-include odoo *.doc diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index e711f4a0dfd..65a2ddf53cc 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -98,7 +98,10 @@ class AccountInvoice(models.Model): currency_clause = [('currency_id', '=', currency_id)] if currency_id == company_currency_id: currency_clause = ['|', ('currency_id', '=', False)] + currency_clause - return self.env['account.journal'].search(domain + currency_clause, limit=1) + return ( + self.env['account.journal'].search(domain + currency_clause, limit=1) + or self.env['account.journal'].search(domain, limit=1) + ) @api.model def _default_currency(self): diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py index 4e6b6adbb90..8aba8cc0a85 100644 --- a/addons/account/models/account_move.py +++ b/addons/account/models/account_move.py @@ -482,8 +482,8 @@ class AccountMove(models.Model): def action_duplicate(self): self.ensure_one() action = self.env.ref('account.action_move_journal_line').read()[0] - action['target'] = 'inline' action['context'] = dict(self.env.context) + action['context']['form_view_initial_mode'] = 'edit' action['context']['view_no_maturity'] = False action['views'] = [(self.env.ref('account.view_move_form').id, 'form')] action['res_id'] = self.copy().id diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py index 5ab6865831a..a1db4fc2c5f 100644 --- a/addons/account/models/account_reconcile_model.py +++ b/addons/account/models/account_reconcile_model.py @@ -467,6 +467,8 @@ class AccountReconcileModel(models.Model): ( line_partner.partner_id = 0 AND + TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')) != '' + AND ( regexp_split_to_array(TRIM(REGEXP_REPLACE(move.name, '[^0-9|^\s]', '', 'g')),'\s+') && regexp_split_to_array(TRIM(REGEXP_REPLACE(st_line.name, '[^0-9|^\s]', '', 'g')), '\s+') diff --git a/addons/account/wizard/account_invoice_send_views.xml b/addons/account/wizard/account_invoice_send_views.xml index 648dcdec65d..49c78cb32d9 100644 --- a/addons/account/wizard/account_invoice_send_views.xml +++ b/addons/account/wizard/account_invoice_send_views.xml @@ -12,6 +12,7 @@ +
diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py index a3080952fcd..44b31a8a129 100644 --- a/addons/account_voucher/models/account_voucher.py +++ b/addons/account_voucher/models/account_voucher.py @@ -305,9 +305,12 @@ class AccountVoucher(models.Model): } # Create one line per tax and fix debit-credit for the move line if there are tax included if (line.tax_ids): - tax_group = line.tax_ids.compute_all(line.price_unit, line.currency_id, line.quantity, line.product_id, self.partner_id) + tax_group = line.tax_ids.compute_all(self._convert(line.price_unit), self.company_id.currency_id, line.quantity, line.product_id, self.partner_id) if move_line['debit']: move_line['debit'] = tax_group['total_excluded'] if move_line['credit']: move_line['credit'] = tax_group['total_excluded'] + Currency = self.env['res.currency'] + company_cur = Currency.browse(company_currency) + current_cur = Currency.browse(current_currency) for tax_vals in tax_group['taxes']: if tax_vals['amount']: tax = self.env['account.tax'].browse([tax_vals['id']]) @@ -328,8 +331,8 @@ class AccountVoucher(models.Model): ctx = {} if self.account_date: ctx['date'] = self.account_date - temp['currency_id'] = current_currency.id - temp['amount_currency'] = company_currency._convert(tax_vals['amount'], current_currency, line.company_id, self.account_date or fields.Date.today(), round=True) + 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) self.env['account.move.line'].create(temp) self.env['account.move.line'].create(move_line) diff --git a/addons/event_sale/models/sale_order.py b/addons/event_sale/models/sale_order.py index 7ca5360166c..7e83422d922 100644 --- a/addons/event_sale/models/sale_order.py +++ b/addons/event_sale/models/sale_order.py @@ -83,7 +83,9 @@ class SaleOrderLine(models.Model): company = self.event_id.company_id or self.env.user.company_id currency = company.currency_id or self.env.user.company_id.currency_id self.price_unit = currency._convert( - self.event_ticket_id.price, self.order_id.currency_id, self.order_id.company_id, self.order_id.date_order or fields.Date.today()) + self.event_ticket_id.price, self.order_id.currency_id, + self.order_id.company_id or self.env.user.company_id, + self.order_id.date_order or fields.Date.today()) # we call this to force update the default name self.product_id_change() diff --git a/addons/gamification/data/gamification_karma_rank_data.xml b/addons/gamification/data/gamification_karma_rank_data.xml index 3833dd24b7e..f86b827cceb 100644 --- a/addons/gamification/data/gamification_karma_rank_data.xml +++ b/addons/gamification/data/gamification_karma_rank_data.xml @@ -15,7 +15,7 @@
Reach the next rank to show the rest of the world you exist!
- +
1 @@ -28,7 +28,7 @@
Reach the next rank to improve your status.
- +
100 @@ -41,7 +41,7 @@
Reach the next rank and become a Master!
- +
500 @@ -54,7 +54,7 @@
Reach the next rank and become a powerful user!
- +
2000 diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py index 37e250779ec..78c325981bb 100644 --- a/addons/hr_expense/models/hr_expense.py +++ b/addons/hr_expense/models/hr_expense.py @@ -55,10 +55,10 @@ class HrExpense(models.Model): tax_ids = fields.Many2many('account.tax', 'expense_tax', 'expense_id', 'tax_id', string='Taxes', states={'done': [('readonly', True)], 'post': [('readonly', True)]}) untaxed_amount = fields.Float("Subtotal", store=True, compute='_compute_amount', digits=dp.get_precision('Account')) total_amount = fields.Monetary("Total", compute='_compute_amount', store=True, currency_field='currency_id', digits=dp.get_precision('Account')) + company_currency_id = fields.Many2one('res.currency', string="Report Company Currency", related='sheet_id.currency_id', store=True, readonly=False) total_amount_company = fields.Monetary("Total (Company Currency)", compute='_compute_total_amount_company', store=True, currency_field='company_currency_id', digits=dp.get_precision('Account')) company_id = fields.Many2one('res.company', string='Company', readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.user.company_id) currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id) - company_currency_id = fields.Many2one('res.currency', string="Report Company Currency", related='sheet_id.currency_id', store=True, readonly=False) analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, oldname='analytic_account') analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags', states={'post': [('readonly', True)], 'done': [('readonly', True)]}) account_id = fields.Many2one('account.account', string='Account', states={'post': [('readonly', True)], 'done': [('readonly', True)]}, default=_default_account_id, help="An expense account is expected") diff --git a/addons/hr_holidays/data/hr_holidays_data.xml b/addons/hr_holidays/data/hr_holidays_data.xml index 9b8b1b78512..6b7ec7f2a6c 100644 --- a/addons/hr_holidays/data/hr_holidays_data.xml +++ b/addons/hr_holidays/data/hr_holidays_data.xml @@ -16,6 +16,7 @@ Home Workings lightgreen + diff --git a/addons/hr_holidays/data/hr_holidays_demo.xml b/addons/hr_holidays/data/hr_holidays_demo.xml index b39754605b5..d75e37f8cff 100644 --- a/addons/hr_holidays/data/hr_holidays_demo.xml +++ b/addons/hr_holidays/data/hr_holidays_demo.xml @@ -8,6 +8,7 @@ fixed_allocation blue hour + @@ -15,6 +16,7 @@ fixed brown both + diff --git a/addons/hr_holidays/models/hr.py b/addons/hr_holidays/models/hr.py index 2a8a5506217..be5294d847a 100644 --- a/addons/hr_holidays/models/hr.py +++ b/addons/hr_holidays/models/hr.py @@ -168,7 +168,7 @@ class Employee(models.Model): employee.leave_date_to = leave_data.get(employee.id, {}).get('leave_date_to') employee.current_leave_state = leave_data.get(employee.id, {}).get('current_leave_state') employee.current_leave_id = leave_data.get(employee.id, {}).get('current_leave_id') - employee.is_absent = leave_data and leave_data.get(employee.id, {}).get('current_leave_state') not in ['cancel', 'refuse', 'draft'] + employee.is_absent = leave_data.get(employee.id) and leave_data.get(employee.id, {}).get('current_leave_state') not in ['cancel', 'refuse', 'draft'] @api.onchange('parent_id') def _onchange_parent_id(self): diff --git a/addons/hr_holidays/models/hr_leave.py b/addons/hr_holidays/models/hr_leave.py index 5d3353c889b..f5616500e57 100644 --- a/addons/hr_holidays/models/hr_leave.py +++ b/addons/hr_holidays/models/hr_leave.py @@ -522,16 +522,25 @@ class HolidaysRequest(models.Model): @api.constrains('holiday_status_id', 'date_to', 'date_from') def _check_leave_type_validity(self): for leave in self: + vstart = leave.holiday_status_id.validity_start + vstop = leave.holiday_status_id.validity_stop + dfrom = leave.date_from + dto = leave.date_to if leave.holiday_status_id.validity_start and leave.holiday_status_id.validity_stop: - vstart = leave.holiday_status_id.validity_start - vstop = leave.holiday_status_id.validity_stop - dfrom = leave.date_from - dto = leave.date_to - if dfrom and dto and (dfrom.date() < vstart or dto.date() > vstop): raise UserError( _('You can take %s only between %s and %s') % ( leave.holiday_status_id.display_name, leave.holiday_status_id.validity_start, leave.holiday_status_id.validity_stop)) + elif leave.holiday_status_id.validity_start: + if dfrom and (dfrom.date() < vstart): + raise UserError( + _('You can take %s from %s') % ( + leave.holiday_status_id.display_name, leave.holiday_status_id.validity_start)) + elif leave.holiday_status_id.validity_stop: + if dto and (dto.date() > vstop): + raise UserError( + _('You can take %s until %s') % ( + leave.holiday_status_id.display_name, leave.holiday_status_id.validity_stop)) @api.model def create(self, values): diff --git a/addons/hr_holidays/models/hr_leave_type.py b/addons/hr_holidays/models/hr_leave_type.py index 73cb99d98d7..a215a9e30cb 100644 --- a/addons/hr_holidays/models/hr_leave_type.py +++ b/addons/hr_holidays/models/hr_leave_type.py @@ -183,7 +183,7 @@ class HolidaysType(models.Model): if request.leave_type_request_unit == 'hour' else request.number_of_days) - for allocation in allocations: + for allocation in allocations.sudo(): status_dict = result[allocation.holiday_status_id.id] if allocation.state == 'validate': # note: add only validated allocation even for the virtual diff --git a/addons/hr_holidays/tests/test_access_rights.py b/addons/hr_holidays/tests/test_access_rights.py index a5b2423821c..fff9a636bf5 100644 --- a/addons/hr_holidays/tests/test_access_rights.py +++ b/addons/hr_holidays/tests/test_access_rights.py @@ -19,6 +19,7 @@ class TestLeavesRights(TestHrHolidaysBase): 'name': 'Unlimited', 'validation_type': 'hr', 'allocation_type': 'no', + 'validity_start': False, }) self.rd_dept.manager_id = False self.hr_dept.manager_id = False diff --git a/addons/hr_holidays/tests/test_accrual_allocations.py b/addons/hr_holidays/tests/test_accrual_allocations.py index c877643cc2e..270599fa26e 100644 --- a/addons/hr_holidays/tests/test_accrual_allocations.py +++ b/addons/hr_holidays/tests/test_accrual_allocations.py @@ -20,12 +20,14 @@ class TestAccrualAllocations(TestHrHolidaysBase): self.accrual_type = LeaveType.create({ 'name': 'accrual', 'allocation_type': 'fixed', + 'validity_start': False, }) self.unpaid_type = LeaveType.create({ 'name': 'unpaid', 'allocation_type': 'no', 'unpaid': True, + 'validity_start': False, }) self.set_employee_create_date(self.employee_emp_id, '2010-02-03 00:00:00') diff --git a/addons/hr_holidays/tests/test_change_department.py b/addons/hr_holidays/tests/test_change_department.py index 6240594e43f..52206b386e4 100644 --- a/addons/hr_holidays/tests/test_change_department.py +++ b/addons/hr_holidays/tests/test_change_department.py @@ -18,6 +18,7 @@ class TestChangeDepartment(TestHrHolidaysBase): 'name': 'NotLimitedHR', 'allocation_type': 'no', 'validation_type': 'hr', + 'validity_start': False, }) def create_holiday(name, start, end): diff --git a/addons/hr_holidays/tests/test_holidays_flow.py b/addons/hr_holidays/tests/test_holidays_flow.py index 2a825f46c95..9ef8600f61a 100644 --- a/addons/hr_holidays/tests/test_holidays_flow.py +++ b/addons/hr_holidays/tests/test_holidays_flow.py @@ -29,11 +29,13 @@ class TestHolidaysFlow(TestHrHolidaysBase): 'name': 'NotLimitedHR', 'allocation_type': 'no', 'validation_type': 'hr', + 'validity_start': False, }) self.holidays_status_manager = HolidayStatusManagerGroup.create({ 'name': 'NotLimitedManager', 'allocation_type': 'no', 'validation_type': 'manager', + 'validity_start': False, }) HolidaysEmployeeGroup = Requests.sudo(self.user_employee_id) @@ -95,13 +97,15 @@ class TestHolidaysFlow(TestHrHolidaysBase): HolidayStatusManagerGroup.create({ 'name': 'WithMeetingType', 'allocation_type': 'no', - 'categ_id': self.env['calendar.event.type'].sudo(self.user_hrmanager_id).create({'name': 'NotLimitedMeetingType'}).id + 'categ_id': self.env['calendar.event.type'].sudo(self.user_hrmanager_id).create({'name': 'NotLimitedMeetingType'}).id, + 'validity_start': False, }) self.holidays_status_limited = HolidayStatusManagerGroup.create({ 'name': 'Limited', 'allocation_type': 'fixed', 'validation_type': 'both', + 'validity_start': False, }) HolidaysEmployeeGroup = Requests.sudo(self.user_employee_id) diff --git a/addons/hr_holidays/tests/test_hr_leave_type.py b/addons/hr_holidays/tests/test_hr_leave_type.py index 953c87a3981..e9f32264b7c 100644 --- a/addons/hr_holidays/tests/test_hr_leave_type.py +++ b/addons/hr_holidays/tests/test_hr_leave_type.py @@ -16,6 +16,7 @@ class TestHrLeaveType(TestHrHolidaysBase): 'name': 'Paid Time Off', 'time_type': 'leave', 'allocation_type': 'no', + 'validity_start': False, }) leave_1 = self.env['hr.leave'].create({ diff --git a/addons/hr_holidays/tests/test_leave_requests.py b/addons/hr_holidays/tests/test_leave_requests.py index f15f9cfb9e5..3ac45bc974c 100644 --- a/addons/hr_holidays/tests/test_leave_requests.py +++ b/addons/hr_holidays/tests/test_leave_requests.py @@ -32,11 +32,13 @@ class TestLeaveRequests(TestHrHolidaysBase): 'name': 'NotLimitedHR', 'allocation_type': 'no', 'validation_type': 'hr', + 'validity_start': False, }) self.holidays_type_2 = LeaveType.create({ 'name': 'Limited', 'allocation_type': 'fixed', 'validation_type': 'hr', + 'validity_start': False, }) self.holidays_type_3 = LeaveType.create({ 'name': 'TimeNotLimited', @@ -173,3 +175,18 @@ class TestLeaveRequests(TestHrHolidaysBase): allocation_form = Form(self.env['hr.leave.allocation'].sudo(self.user_employee)) allocation_form.holiday_status_id = self.holidays_type_1 allocation = allocation_form.save() + + @mute_logger('odoo.models.unlink', 'odoo.addons.mail.models.mail_mail') + def test_employee_is_absent(self): + """ Only the concerned employee should be considered absent """ + self.env['hr.leave'].sudo(self.user_employee_id).create({ + 'name': 'Hol11', + 'employee_id': self.employee_emp_id, + 'holiday_status_id': self.holidays_type_1.id, + 'date_from': (fields.Datetime.now() - relativedelta(days=1)), + 'date_to': fields.Datetime.now() + relativedelta(days=1), + 'number_of_days': 2, + }) + (self.employee_emp | self.employee_hrmanager).mapped('is_absent') # compute in batch + self.assertTrue(self.employee_emp.is_absent, "He should be considered absent") + self.assertFalse(self.employee_hrmanager.is_absent, "He should not be considered absent") diff --git a/addons/hr_holidays/tests/test_out_of_office.py b/addons/hr_holidays/tests/test_out_of_office.py index 4c97f465ce1..0850ff3d76e 100644 --- a/addons/hr_holidays/tests/test_out_of_office.py +++ b/addons/hr_holidays/tests/test_out_of_office.py @@ -17,6 +17,7 @@ class TestOutOfOffice(TestHrHolidaysBase): 'name': 'Legal Leaves', 'time_type': 'leave', 'allocation_type': 'no', + 'validity_start': False, }) def test_leave_ooo(self): @@ -87,6 +88,7 @@ class TestOutOfOfficePerformance(TestHrHolidaysBase): 'name': 'Legal Leaves', 'time_type': 'leave', 'allocation_type': 'no', + 'validity_start': False, }) self.leave_date_end = (datetime.today() + relativedelta(days=3)) self.leave = self.env['hr.leave'].create({ diff --git a/addons/l10n_ca/data/account.account.template.csv b/addons/l10n_ca/data/account.account.template.csv index 552031ee2ee..a940690a711 100644 --- a/addons/l10n_ca/data/account.account.template.csv +++ b/addons/l10n_ca/data/account.account.template.csv @@ -1,6 +1,6 @@ "id","name","code","user_type_id/id","chart_template_id/id","reconcile" "chart1141_en","Stock In Hand","1141","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","False" -"chart1145_en","Stock Delivered But Not Billed","1145","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","False" +"chart1145_en","Stock Delivered But Not Billed","1145","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","True" "chart1151_en","Customers Account","1151","account.data_account_type_receivable","l10n_ca.ca_en_chart_template_en","True" "chart1181_en","GST receivable","1181","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","False" "chart1182_en","PST/QST receivable","1182","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","False" @@ -27,7 +27,7 @@ "chart21424_en","Labour Health and Safety to pay","21424","account.data_account_type_current_liabilities","l10n_ca.ca_en_chart_template_en","False" "chart21425_en","Labour Standards to pay","21425","account.data_account_type_current_liabilities","l10n_ca.ca_en_chart_template_en","False" "chart21426_en","Provincial Income Tax","21426","account.data_account_type_current_liabilities","l10n_ca.ca_en_chart_template_en","False" -"chart2171_en","Stock Received But Not Billed","2171","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","False" +"chart2171_en","Stock Received But Not Billed","2171","account.data_account_type_current_assets","l10n_ca.ca_en_chart_template_en","True" "chart2181_en","Salaries to pay","2181","account.data_account_type_current_liabilities","l10n_ca.ca_en_chart_template_en","False" "chart2183_en","Bonus to pay","2183","account.data_account_type_current_liabilities","l10n_ca.ca_en_chart_template_en","False" "chart2184_en","Retroactive Payment to pay","2184","account.data_account_type_current_liabilities","l10n_ca.ca_en_chart_template_en","False" diff --git a/addons/l10n_do/data/account.account.template.csv b/addons/l10n_do/data/account.account.template.csv index 594e5f02df8..7c63aa26fba 100644 --- a/addons/l10n_do/data/account.account.template.csv +++ b/addons/l10n_do/data/account.account.template.csv @@ -21,7 +21,7 @@ do_niif_11050200,11050200,Inventario de Materia Prima,account.data_account_type_ do_niif_11050300,11050300,Inventario en Tránsito,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105",l10n_do.account_group_1105 do_niif_11050400,11050400,Inventario de Materiales y Suministros,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105",l10n_do.account_group_1105 do_niif_11050500,11050500,Inventario de Combustibles,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105",l10n_do.account_group_1105 -do_niif_11050600,11050600,Bienes Enviados No Facturados,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105",l10n_do.account_group_1105 +do_niif_11050600,11050600,Bienes Enviados No Facturados,account.data_account_type_current_assets,TRUE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1105",l10n_do.account_group_1105 do_niif_11060100,11060100,Deterioro Acum. de Inventario en Almacen,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1106",l10n_do.account_group_1106 do_niif_11060200,11060200,Deterioro Acum. de Materiales y Suministros,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1106",l10n_do.account_group_1106 do_niif_11060300,11060300,Deterioro Acum. de Combustibles,account.data_account_type_current_assets,FALSE,do_chart_template,"account_tag_1,account_tag_11,account_tag_1106",l10n_do.account_group_1106 @@ -99,7 +99,7 @@ do_niif_21020800,21020800,Aporte a Fondo de Pensiones (AFP) por Pagar,account.da do_niif_21020900,21020900,Seguro de Riesgo Laboral (SRL) por Pagar,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102",l10n_do.account_group_2102 do_niif_21021000,21021000,INFOTEP por Pagar,account.data_account_type_payable,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102",l10n_do.account_group_2102 do_niif_21021100,21021100,Tesorería de la Seguridad Social (TSS) por Pagar,account.data_account_type_payable,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102",l10n_do.account_group_2102 -do_niif_21021200,21021200,Bienes Recibidos no Facturados,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102",l10n_do.account_group_2102 +do_niif_21021200,21021200,Bienes Recibidos no Facturados,account.data_account_type_non_current_liabilities,TRUE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2102",l10n_do.account_group_2102 do_niif_21030101,21030101,ITBIS por Venta Servicios,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210301",l10n_do.account_group_210301 do_niif_21030102,21030102,ITBIS por Venta Bienes,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210301",l10n_do.account_group_210301 do_niif_21030103,21030103,ITBIS por Venta Servicios en Nombre de Terceros,account.data_account_type_non_current_liabilities,FALSE,do_chart_template,"account_tag_2,account_tag_21,account_tag_2103,account_tag_210301",l10n_do.account_group_210301 diff --git a/addons/l10n_mn/data/account.chart.template.csv b/addons/l10n_mn/data/account.chart.template.csv index 3c63ba77781..ac4990fc4a1 100644 --- a/addons/l10n_mn/data/account.chart.template.csv +++ b/addons/l10n_mn/data/account.chart.template.csv @@ -1,2 +1,2 @@ -id,property_account_receivable_id/id,property_account_payable_id/id,property_account_expense_categ_id/id,property_account_income_categ_id/id,income_currency_exchange_account_id/id,expense_currency_exchange_account_id/id -mn_chart_1,account_template_1201_0201,account_template_3101_0201,account_template_7002_1101,account_template_5101_0201,account_template_5301_0201,account_template_5302_0201 +id,property_account_receivable_id/id,property_account_payable_id/id,property_account_expense_categ_id/id,property_account_income_categ_id/id,income_currency_exchange_account_id/id,expense_currency_exchange_account_id/id,property_stock_account_input_categ_id/id,property_stock_account_output_categ_id/id,property_stock_valuation_account_id/id +mn_chart_1,account_template_1201_0201,account_template_3101_0201,account_template_6101_0101,account_template_5101_0101,account_template_5301_0201,account_template_5302_0201,account_template_1407_0101,account_template_1408_0101,account_template_1401_0101 diff --git a/addons/l10n_mn/data/account_chart_template_data.xml b/addons/l10n_mn/data/account_chart_template_data.xml index bb64e86c95e..0d24db96803 100644 --- a/addons/l10n_mn/data/account_chart_template_data.xml +++ b/addons/l10n_mn/data/account_chart_template_data.xml @@ -10,6 +10,7 @@ 1109 8 + diff --git a/addons/l10n_ua/data/account.account.template.csv b/addons/l10n_ua/data/account.account.template.csv index 14471cfa320..608880211ed 100644 --- a/addons/l10n_ua/data/account.account.template.csv +++ b/addons/l10n_ua/data/account.account.template.csv @@ -81,8 +81,8 @@ "ua_psbp_27",27,"Продукція сільськогосподарського виробництва","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template" "ua_psbp_280",280,"Транспортно-заготівельні витрати","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template" "ua_psbp_281",281,"Товари на складі","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template" -"ua_psbp_2811",2811,"Товари відвантажені зі складу","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template" -"ua_psbp_2812",2812,"Товари отримані на склад","account.data_account_type_current_liabilities",FALSE,"l10n_ua_psbo_chart_template" +"ua_psbp_2811",2811,"Товари відвантажені зі складу","account.data_account_type_current_assets",TRUE,"l10n_ua_psbo_chart_template" +"ua_psbp_2812",2812,"Товари отримані на склад","account.data_account_type_current_liabilities",TRUE,"l10n_ua_psbo_chart_template" "ua_psbp_282",282,"Товари в торгівлі (за продажною вартістю)","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template" "ua_psbp_283",283,"Товари на комісії","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template" "ua_psbp_284",284,"Тара під товарами","account.data_account_type_current_assets",FALSE,"l10n_ua_psbo_chart_template" @@ -350,7 +350,7 @@ "ua_ias_1101",1101,"Незавершене виробництво","account.data_account_type_current_assets",FALSE,"l10n_ua_ias_chart_template" "ua_ias_1102",1102,"Готова продукція","account.data_account_type_current_assets",FALSE,"l10n_ua_ias_chart_template" "ua_ias_1120",1120,"Дебіторська заборгованість за товари та послуги","account.data_account_type_receivable",TRUE,"l10n_ua_ias_chart_template" -"ua_ias_1121",1121,"Відвантажено зі складу без рахунку-фактури","account.data_account_type_current_assets",FALSE,"l10n_ua_ias_chart_template" +"ua_ias_1121",1121,"Відвантажено зі складу без рахунку-фактури","account.data_account_type_current_assets",TRUE,"l10n_ua_ias_chart_template" "ua_ias_1130",1130,"Аванси партнерам","account.data_account_type_payable",TRUE,"l10n_ua_ias_chart_template" "ua_ias_1131",1131,"Інші оборотні активи","account.data_account_type_payable",TRUE,"l10n_ua_ias_chart_template" "ua_ias_1140",1140,"Податковий кредит по ПДВ","account.data_account_type_current_assets",FALSE,"l10n_ua_ias_chart_template" @@ -363,7 +363,7 @@ "ua_ias_1170",1170,"Довгострокова дебіторська заборгованість","account.data_account_type_non_current_assets",FALSE,"l10n_ua_ias_chart_template" "ua_ias_1180",1180,"Відстрочені податкові активи","account.data_account_type_non_current_assets",FALSE,"l10n_ua_ias_chart_template" "ua_ias_1200",1200,"Кредиторська заборгованість за товари та послуги","account.data_account_type_payable",TRUE,"l10n_ua_ias_chart_template" -"ua_ias_1201",1201,"Отримано на склад без рахунку-фактури","account.data_account_type_current_liabilities",FALSE,"l10n_ua_ias_chart_template" +"ua_ias_1201",1201,"Отримано на склад без рахунку-фактури","account.data_account_type_current_liabilities",TRUE,"l10n_ua_ias_chart_template" "ua_ias_1202",1202,"Податок на прибуток","account.data_account_type_payable",TRUE,"l10n_ua_ias_chart_template" "ua_ias_1203",1203,"ПДВ","account.data_account_type_payable",TRUE,"l10n_ua_ias_chart_template" "ua_ias_1204",1204,"Податкові зобов’язання по ПДВ","account.data_account_type_current_liabilities",FALSE,"l10n_ua_ias_chart_template" diff --git a/addons/mail/models/mail_channel.py b/addons/mail/models/mail_channel.py index 4b860440f6b..76f316eb3c3 100644 --- a/addons/mail/models/mail_channel.py +++ b/addons/mail/models/mail_channel.py @@ -187,9 +187,7 @@ class Channel(models.Model): @api.onchange('public') def _onchange_public(self): - if self.public == 'public': - self.alias_contact = 'everyone' - else: + if self.public != 'public' and self.alias_contact == 'everyone': self.alias_contact = 'followers' @api.onchange('moderator_ids') diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index a11be5335a4..ced23dacc0c 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -437,7 +437,11 @@ var Chatter = Widget.extend({ */ _updateAttachmentCounter: function () { var count = this.record.data.message_attachment_count || 0; - this.$('.o_chatter_attachment_button_count').html(count); + var $element = this.$('.o_chatter_attachment_button_count'); + if (Number($element.html()) !== count) { + this._areAttachmentsLoaded = false; + $element.html(count); + } }, /** * @private diff --git a/addons/mail/static/src/js/form_view.js b/addons/mail/static/src/js/form_view.js index 0e1623d0dcf..6a2a41091aa 100644 --- a/addons/mail/static/src/js/form_view.js +++ b/addons/mail/static/src/js/form_view.js @@ -16,8 +16,9 @@ FormView.include({ init: function () { this._super.apply(this, arguments); - if ('message_ids' in this.fieldsInfo[this.viewType]) { - this.fieldsInfo[this.viewType].message_attachment_count = {}; + var fieldsInfo = this.fieldsInfo[this.viewType]; + if ('message_ids' in fieldsInfo && !('message_attachment_count' in fieldsInfo)) { + fieldsInfo.message_attachment_count = {}; } }, }); diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index ea6b7adbd8b..180fddd5e3b 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -242,6 +242,59 @@ QUnit.test('basic rendering', async function (assert) { form.destroy(); }); +QUnit.test('basic rendering: message_attachment_count can be in view standalone', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + services: this.services, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
', + res_id: 2, + }); + + assert.strictEqual(form.$('.o_form_label').text(), "I'm here", + "The field message_attachment_count must be present according to the view's specs"); + + form.destroy(); +}); + +QUnit.test('basic rendering: message_attachment_count can be in view with chatter', async function (assert) { + assert.expect(1); + + var form = await createView({ + View: FormView, + model: 'partner', + data: this.data, + services: this.services, + arch: '
' + + '' + + '' + + '' + + '' + + '' + + '
' + + '' + + '' + + '' + + '
' + + '
', + res_id: 2, + }); + + assert.strictEqual(form.$('.o_form_label').text(), "I'm here", + "The field message_attachment_count must be present according to the view's specs"); + + form.destroy(); +}); + QUnit.test('Activity Done keep feedback on blur', async function (assert) { assert.expect(3); var done = assert.async(); diff --git a/addons/mrp/data/mrp_data.xml b/addons/mrp/data/mrp_data.xml index 1343cbcf146..c9f6b8e608f 100644 --- a/addons/mrp/data/mrp_data.xml +++ b/addons/mrp/data/mrp_data.xml @@ -9,6 +9,7 @@ 5 1 1 + diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py index 8b30be3da44..e6f44a6182b 100644 --- a/addons/mrp/models/mrp_production.py +++ b/addons/mrp/models/mrp_production.py @@ -506,6 +506,9 @@ class MrpProduction(models.Model): factor = production.product_uom_id._compute_quantity(production.product_qty, production.bom_id.product_uom_id) / production.bom_id.product_qty boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id) for bom_line, line_data in lines: + if bom_line.child_bom_id and bom_line.child_bom_id.type == 'phantom' or\ + bom_line.product_id.type not in ['product', 'consu']: + continue moves.append(production._get_move_raw_values(bom_line, line_data)) return moves @@ -513,10 +516,6 @@ class MrpProduction(models.Model): quantity = line_data['qty'] # alt_op needed for the case when you explode phantom bom and all the lines will be consumed in the operation given by the parent bom line alt_op = line_data['parent_line'] and line_data['parent_line'].operation_id.id or False - if bom_line.child_bom_id and bom_line.child_bom_id.type == 'phantom': - return self.env['stock.move'] - if bom_line.product_id.type not in ['product', 'consu']: - return self.env['stock.move'] source_location = self.location_src_id data = { 'sequence': bom_line.sequence, diff --git a/addons/mrp/tests/test_order.py b/addons/mrp/tests/test_order.py index ddde7e6a064..c7092e539f9 100644 --- a/addons/mrp/tests/test_order.py +++ b/addons/mrp/tests/test_order.py @@ -989,3 +989,36 @@ class TestMrpOrder(TestMrpCommon): self.assertEqual(product_produce.raw_workorder_line_ids[0].product_uom_id, unit, 'Should be the product uom so "unit"') self.assertEqual(product_produce.raw_workorder_line_ids[1].qty_to_consume, 1, 'Should be 1 unit since the tracking is serial and quantity 2.') self.assertEqual(product_produce.raw_workorder_line_ids[1].product_uom_id, unit, 'should be the product uom so "unit"') + + def test_product_type_service_1(self): + # Create finished product + finished_product = self.env['product.product'].create({ + 'name': 'Geyser', + 'type': 'product', + }) + + # Create service type product + product_raw = self.env['product.product'].create({ + 'name': 'raw Geyser', + 'type': 'service', + }) + + # Create bom for finish product + bom = self.env['mrp.bom'].create({ + 'product_id': finished_product.id, + 'product_tmpl_id': finished_product.product_tmpl_id.id, + 'product_uom_id': self.env.ref('uom.product_uom_unit').id, + 'product_qty': 1.0, + 'type': 'normal', + 'bom_line_ids': [(5, 0), (0, 0, {'product_id': product_raw.id})] + }) + + mo_form = Form(self.env['mrp.production']) + mo_form.product_id = finished_product + mo_form.bom_id = bom + mo_form.product_uom_id = self.env.ref('uom.product_uom_unit') + mo_form.product_qty = 1 + mo = mo_form.save() + + # Check Mo is created or not + self.assertTrue(mo, "Mo is created") diff --git a/addons/mrp/wizard/change_production_qty.py b/addons/mrp/wizard/change_production_qty.py index d6ac8296356..6cefbd89261 100644 --- a/addons/mrp/wizard/change_production_qty.py +++ b/addons/mrp/wizard/change_production_qty.py @@ -57,6 +57,9 @@ class ChangeProductionQty(models.TransientModel): boms, lines = production.bom_id.explode(production.product_id, factor, picking_type=production.bom_id.picking_type_id) documents = {} for line, line_data in lines: + if line.child_bom_id and line.child_bom_id.type == 'phantom' or\ + line.product_id.type not in ['product', 'consu']: + continue move, old_qty, new_qty = production._update_raw_move(line, line_data) iterate_key = production._get_document_iterate_key(move) if iterate_key: diff --git a/addons/payment/models/payment_acquirer.py b/addons/payment/models/payment_acquirer.py index 6847cb76461..c15aced1802 100644 --- a/addons/payment/models/payment_acquirer.py +++ b/addons/payment/models/payment_acquirer.py @@ -881,7 +881,7 @@ class PaymentTransaction(models.Model): values.update({ 'partner_name': partner.name, - 'partner_lang': partner.lang or 'en_US', + 'partner_lang': partner.lang or self.env.user.lang, 'partner_email': partner.email, 'partner_zip': partner.zip, 'partner_address': _partner_format_address(partner.street or '', partner.street2 or ''), diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js index 2a1e1490633..c9402bb70c2 100644 --- a/addons/point_of_sale/static/src/js/models.js +++ b/addons/point_of_sale/static/src/js/models.js @@ -1566,6 +1566,7 @@ exports.Orderline = Backbone.Model.extend({ price_display : this.get_display_price(), price_with_tax : this.get_price_with_tax(), price_without_tax: this.get_price_without_tax(), + price_with_tax_before_discount: this.get_price_with_tax_before_discount(), tax: this.get_tax(), product_description: this.get_product().description, product_description_sale: this.get_product().description_sale, @@ -1629,9 +1630,9 @@ exports.Orderline = Backbone.Model.extend({ }, get_display_price_one: function(){ var rounding = this.pos.currency.rounding; - var price_unit = this.get_unit_price() * (1.0 - (this.get_discount() / 100.0)); + var price_unit = this.get_unit_price(); if (this.pos.config.iface_tax_included !== 'total') { - return round_pr(price_unit, rounding); + return round_pr(price_unit * (1.0 - (this.get_discount() / 100.0)), rounding); } else { var product = this.get_product(); var taxes_ids = product.taxes_id; @@ -1662,6 +1663,9 @@ exports.Orderline = Backbone.Model.extend({ get_price_with_tax: function(){ return this.get_all_prices().priceWithTax; }, + get_price_with_tax_before_discount: function () { + return this.get_all_prices().priceWithTaxBeforeDiscount; + }, get_tax: function(){ return this.get_all_prices().tax; }, @@ -1907,6 +1911,7 @@ exports.Orderline = Backbone.Model.extend({ }); var all_taxes = this.compute_all(product_taxes, price_unit, this.get_quantity(), this.pos.currency.rounding); + var all_taxes_before_discount = this.compute_all(product_taxes, this.get_unit_price(), this.get_quantity(), this.pos.currency.rounding); _(all_taxes.taxes).each(function(tax) { taxtotal += tax.amount; taxdetail[tax.id] = tax.amount; @@ -1916,6 +1921,7 @@ exports.Orderline = Backbone.Model.extend({ "priceWithTax": all_taxes.total_included, "priceWithoutTax": all_taxes.total_excluded, "priceSumTaxVoid": all_taxes.total_void, + "priceWithTaxBeforeDiscount": all_taxes_before_discount.total_included, "tax": taxtotal, "taxDetails": taxdetail, }; diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml index 17c47b154ad..49079e59c69 100644 --- a/addons/point_of_sale/static/src/xml/pos.xml +++ b/addons/point_of_sale/static/src/xml/pos.xml @@ -791,8 +791,8 @@ + - @@ -812,7 +812,12 @@ - + + + + + + diff --git a/addons/product/report/product_pricelist.py b/addons/product/report/product_pricelist.py index e6ccde44d8e..abdb705c529 100644 --- a/addons/product/report/product_pricelist.py +++ b/addons/product/report/product_pricelist.py @@ -28,7 +28,8 @@ class report_product_pricelist(models.AbstractModel): } def _get_quantity(self, data): - return sorted([data['form'][key] for key in data['form'] if key.startswith('qty') and data['form'][key]]) + form = data and data.get('form') or {} + return sorted([form[key] for key in form if key.startswith('qty') and form[key]]) def _get_categories(self, pricelist, products, quantities): categ_data = [] diff --git a/addons/project_timesheet_holidays/tests/test_timesheet_holidays.py b/addons/project_timesheet_holidays/tests/test_timesheet_holidays.py index 74b73173972..4b89e197c03 100644 --- a/addons/project_timesheet_holidays/tests/test_timesheet_holidays.py +++ b/addons/project_timesheet_holidays/tests/test_timesheet_holidays.py @@ -46,6 +46,7 @@ class TestTimesheetHolidays(TestCommonTimesheet): 'timesheet_generate': True, 'timesheet_project_id': self.internal_project.id, 'timesheet_task_id': self.internal_task_leaves.id, + 'validity_start': False, }) self.hr_leave_type_no_ts = self.env['hr.leave.type'].create({ 'name': 'Leave Type without timesheet generation', @@ -53,6 +54,7 @@ class TestTimesheetHolidays(TestCommonTimesheet): 'timesheet_generate': False, 'timesheet_project_id': False, 'timesheet_task_id': False, + 'validity_start': False, }) # HR Officer allocates some leaves to the employee 1 diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index 48b10e8a9c6..1c1bfc1db54 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -201,6 +201,9 @@ class SaleOrder(models.Model): @api.depends('pricelist_id', 'date_order', 'company_id') def _compute_currency_rate(self): for order in self: + if not order.company_id: + order.currency_rate = order.currency_id.with_context(date=order.date_order).rate or 1.0 + continue order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.currency_id, order.company_id, order.date_order) def _compute_access_url(self): diff --git a/addons/sale_mrp/tests/test_sale_mrp_flow.py b/addons/sale_mrp/tests/test_sale_mrp_flow.py index 459892631b1..6f8710a34e8 100644 --- a/addons/sale_mrp/tests/test_sale_mrp_flow.py +++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py @@ -1459,3 +1459,48 @@ class TestSaleMrpFlow(common.SavepointCase): move_component_kg = order.picking_ids[0].move_lines - move_component_unit self.assertEquals(move_component_unit.product_uom_qty, 0.5) self.assertEquals(move_component_kg.product_uom_qty, 0.583) + + def test_product_type_service_1(self): + route_manufacture = self.warehouse.manufacture_pull_id.route_id.id + route_mto = self.warehouse.mto_pull_id.route_id.id + self.uom_unit = self.env.ref('uom.product_uom_unit') + + # Create finished product + finished_product = self.env['product.product'].create({ + 'name': 'Geyser', + 'type': 'product', + 'route_ids': [(4, route_mto), (4, route_manufacture)], + }) + + # Create service type product + product_raw = self.env['product.product'].create({ + 'name': 'raw Geyser', + 'type': 'service', + }) + + # Create bom for finish product + bom = self.env['mrp.bom'].create({ + 'product_id': finished_product.id, + 'product_tmpl_id': finished_product.product_tmpl_id.id, + 'product_uom_id': self.env.ref('uom.product_uom_unit').id, + 'product_qty': 1.0, + 'type': 'normal', + 'bom_line_ids': [(5, 0), (0, 0, {'product_id': product_raw.id})] + }) + + # Create sale order + sale_form = Form(self.env['sale.order']) + sale_form.partner_id = self.env.ref('base.res_partner_1') + with sale_form.order_line.new() as line: + line.name = finished_product.name + line.product_id = finished_product + line.product_uom_qty = 1.0 + line.product_uom = self.uom_unit + line.price_unit = 10.0 + sale_order = sale_form.save() + + sale_order.action_confirm() + + mo = self.env['mrp.production'].search([('product_id', '=', finished_product.id)]) + + self.assertTrue(mo, 'Manufacturing order created.') diff --git a/addons/survey_crm/i18n/az.po b/addons/survey_crm/i18n/az.po deleted file mode 100644 index 868166d06d8..00000000000 --- a/addons/survey_crm/i18n/az.po +++ /dev/null @@ -1,26 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * survey_crm -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server saas~11.5\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-09-18 09:49+0000\n" -"PO-Revision-Date: 2018-08-24 09:26+0000\n" -"Language-Team: Azerbaijani (https://www.transifex.com/odoo/teams/41243/az/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Language: az\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#. module: survey_crm -#: model:ir.model,name:survey_crm.model_survey_mail_compose_message -msgid "Email composition wizard for Survey" -msgstr "" - -#. module: survey_crm -#: model:ir.actions.act_window,name:survey_crm.action_partner_survey_mail_crm -msgid "Partner Survey Mailing" -msgstr "" diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index e5384c9b167..9d95ae485a9 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -1048,7 +1048,7 @@ class Binary(http.Controller): filename_field=filename_field, download=download, mimetype=mimetype, default_mimetype='image/png', access_token=access_token) - if status == 301 or (status != 200 and download): + if status in [301, 304] or (status != 200 and download): return request.env['ir.http']._response_by_status(status, headers, content) if not content: content = base64.b64encode(self.placeholder(image=placeholder)) diff --git a/addons/web/static/lib/moment/locale/ar.js b/addons/web/static/lib/moment/locale/ar.js index d263e4c4f8d..dbe4e7dd940 100644 --- a/addons/web/static/lib/moment/locale/ar.js +++ b/addons/web/static/lib/moment/locale/ar.js @@ -58,18 +58,18 @@ var pluralize = function (u) { }; }; var months = [ - 'كانون الثاني يناير', - 'شباط فبراير', - 'آذار مارس', - 'نيسان أبريل', - 'أيار مايو', - 'حزيران يونيو', - 'تموز يوليو', - 'آب أغسطس', - 'أيلول سبتمبر', - 'تشرين الأول أكتوبر', - 'تشرين الثاني نوفمبر', - 'كانون الأول ديسمبر' + 'يناير', + 'فبراير', + 'مارس', + 'أبريل', + 'مايو', + 'يونيو', + 'يوليو', + 'أغسطس', + 'سبتمبر', + 'أكتوبر', + 'نوفمبر', + 'ديسمبر' ]; var ar = moment.defineLocale('ar', { diff --git a/addons/web/static/src/js/views/control_panel/search/add_new_favorite_menu.js b/addons/web/static/src/js/views/control_panel/search/add_new_favorite_menu.js index 0a2498c69df..421fec3e04b 100644 --- a/addons/web/static/src/js/views/control_panel/search/add_new_favorite_menu.js +++ b/addons/web/static/src/js/views/control_panel/search/add_new_favorite_menu.js @@ -12,6 +12,7 @@ var AddNewFavoriteMenu = Widget.extend({ events: _.extend({}, Widget.prototype.events, { 'click .o_save_favorite': '_onSaveFavoriteClick', 'click .o_add_favorite.o_menu_header': '_onMenuHeaderClick', + 'click input[type="checkbox"]': '_onCheckboxClick', 'keyup .o_save_name input': '_onKeyUp', }), /** @@ -107,6 +108,33 @@ var AddNewFavoriteMenu = Widget.extend({ // Handlers //-------------------------------------------------------------------------- + /** + * Additional behavior when a checkbox is clicked upon + * Namely, some checkbox are mutually exclusive + * this function allows this + * + * @private + * @param {jQueryEvent} ev + */ + _onCheckboxClick: function (ev) { + function mutuallyExclusive (elem) { + if (!elem.id) return false; + return ['use_by_default', 'share_all_users'].some(function (str) { + return elem.id.indexOf(str); + }); + } + var $checkboxes = this.$('input[type="checkbox"]'); + var currentCheckBox = ev.target; + + if (mutuallyExclusive(currentCheckBox)) { + for (var i=0; i < $checkboxes.length; i++) { + var checkbox = $checkboxes[i]; + if (checkbox !== currentCheckBox && mutuallyExclusive(checkbox)) { + checkbox.checked = false; + } + } + } + }, /** * @private * @param {jQueryEvent} ev diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js index ac48ab769ff..fbc08e93b1e 100644 --- a/addons/web/static/src/js/views/pivot/pivot_controller.js +++ b/addons/web/static/src/js/views/pivot/pivot_controller.js @@ -53,7 +53,6 @@ var PivotController = AbstractController.extend({ * @override */ start: function () { - this.$el.toggleClass('o_enable_linking', this.enableLinking); this.$fieldSelection = this.$('.o_field_selection'); core.bus.on('click', this, function () { this.$fieldSelection.empty(); diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js index 101f3734392..29c3424e141 100644 --- a/addons/web/static/src/js/views/pivot/pivot_renderer.js +++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js @@ -283,7 +283,7 @@ var PivotRenderer = AbstractRenderer.extend({ $cell = $('') .data('id', rows[i].id) .data('col_id', rows[i].col_ids[Math.floor(j / nbrMeasures)]) - .toggleClass('o_empty', !value) + .toggleClass('o_empty', _.isUndefined(value)) .addClass('o_pivot_cell_value text-right'); if (value !== undefined) { $cell.append($('
', {class: 'o_value'}).html(formatter(value, measure))); diff --git a/addons/web/static/src/js/widgets/pie_chart.js b/addons/web/static/src/js/widgets/pie_chart.js index ddeca1fdcec..a9b0bc01e45 100644 --- a/addons/web/static/src/js/widgets/pie_chart.js +++ b/addons/web/static/src/js/widgets/pie_chart.js @@ -34,6 +34,11 @@ var PieChart = Widget.extend({ title: node.attrs.title || modifiers.title || modifiers.measure, }); + var pieChartContext = JSON.parse(JSON.stringify(record.context)); + delete pieChartContext.graph_mode; + delete pieChartContext.graph_measure; + delete pieChartContext.graph_groupbys; + this.subViewParams = { modelName: record.model, withControlPanel: false, @@ -41,7 +46,7 @@ var PieChart = Widget.extend({ mode: 'pie', }; this.subViewParams.searchQuery = { - context: record.context, + context: pieChartContext, domain: domain, groupBy: [], }; diff --git a/addons/web/static/src/scss/pivot_view.scss b/addons/web/static/src/scss/pivot_view.scss index 83f8fbc58e7..6bc25ee114e 100644 --- a/addons/web/static/src/scss/pivot_view.scss +++ b/addons/web/static/src/scss/pivot_view.scss @@ -87,4 +87,9 @@ right: 6px; top: 9px; } + + .o_pivot_field_menu .dropdown-item.disabled { + color: $text-muted; + cursor: default; + } } diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 564bbc8582e..c925809fab5 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -532,7 +532,7 @@
-
+
diff --git a/addons/web/static/tests/views/control_panel_tests.js b/addons/web/static/tests/views/control_panel_tests.js index 2e0a369ab4a..7d033e5f1e4 100644 --- a/addons/web/static/tests/views/control_panel_tests.js +++ b/addons/web/static/tests/views/control_panel_tests.js @@ -418,6 +418,47 @@ QUnit.module('Views', { controlPanel.destroy(); }); + QUnit.test('Favorites Use by Default and Share are exclusive', async function (assert) { + assert.expect(11); + var controlPanel = await createControlPanel({ + model: 'partner', + arch: "", + data: this.data, + searchMenuTypes: ['favorite'], + }); + testUtils.dom.click(controlPanel.$('.o_favorites_menu_button')); + testUtils.dom.click(controlPanel.$('button.o_add_favorite')); + var $checkboxes = controlPanel.$('input[type="checkbox"]'); + + assert.strictEqual($checkboxes.length, 2, + '2 checkboxes are present') + + assert.notOk($checkboxes[0].checked, 'Start: None of the checkboxes are checked (1)'); + assert.notOk($checkboxes[1].checked, 'Start: None of the checkboxes are checked (2)'); + + testUtils.dom.click($checkboxes.eq(0)); + assert.ok($checkboxes[0].checked, 'The first checkbox is checked'); + assert.notOk($checkboxes[1].checked, 'The second checkbox is not checked'); + + testUtils.dom.click($checkboxes.eq(1)); + assert.notOk($checkboxes[0].checked, + 'Clicking on the second checkbox checks it, and unchecks the first (1)'); + assert.ok($checkboxes[1].checked, + 'Clicking on the second checkbox checks it, and unchecks the first (2)'); + + testUtils.dom.click($checkboxes.eq(0)); + assert.ok($checkboxes[0].checked, + 'Clicking on the first checkbox checks it, and unchecks the second (1)'); + assert.notOk($checkboxes[1].checked, + 'Clicking on the first checkbox checks it, and unchecks the second (2)'); + + testUtils.dom.click($checkboxes.eq(0)); + assert.notOk($checkboxes[0].checked, 'End: None of the checkboxes are checked (1)'); + assert.notOk($checkboxes[1].checked, 'End: None of the checkboxes are checked (2)'); + + controlPanel.destroy(); + }); + QUnit.test('load filter', async function (assert) { assert.expect(1); diff --git a/addons/web/static/tests/views/pivot_tests.js b/addons/web/static/tests/views/pivot_tests.js index acd8b11b635..0907e471b39 100644 --- a/addons/web/static/tests/views/pivot_tests.js +++ b/addons/web/static/tests/views/pivot_tests.js @@ -106,7 +106,7 @@ QUnit.module('Views', { }, }); - assert.hasClass(pivot.$el,'o_enable_linking', + assert.hasClass(pivot.$('.o_pivot'),'o_enable_linking', "root node should have classname 'o_enable_linking'"); assert.strictEqual(pivot.$('td.o_pivot_cell_value:contains(32)').length, 1, "should contain a pivot cell with the sum of all records"); @@ -269,7 +269,7 @@ QUnit.module('Views', { } }); - assert.hasClass(pivot.$el,'o_enable_linking', + assert.hasClass(pivot.$('.o_pivot'),'o_enable_linking', "root node should have classname 'o_enable_linking'"); await testUtils.dom.click(pivot.$('.o_pivot_cell_value:contains(12)')); // should trigger a do_action @@ -293,7 +293,7 @@ QUnit.module('Views', { }, }); - assert.doesNotHaveClass(pivot.$el, 'o_enable_linking', + assert.doesNotHaveClass(pivot.$('.o_pivot'), 'o_enable_linking', "root node should not have classname 'o_enable_linking'"); assert.containsOnce(pivot, '.o_pivot_cell_value', "should have one cell"); diff --git a/addons/web/tests/test_image.py b/addons/web/tests/test_image.py index e17073d0fb7..d26528b88e3 100644 --- a/addons/web/tests/test_image.py +++ b/addons/web/tests/test_image.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import io +import base64 from PIL import Image @@ -42,3 +43,23 @@ class TestImage(HttpCase): response = self.url_open('/web/image/fake/0/image_no_size') image = Image.open(io.BytesIO(response.content)) self.assertEqual(image.size, (256, 256)) + + def test_02_content_image_Etag_304(self): + """This test makes sure that the 304 response is properly returned if the ETag is properly set""" + + attachment = self.env['ir.attachment'].create({ + 'datas': b"R0lGODdhAQABAIAAAP///////ywAAAAAAQABAAACAkQBADs=", + 'name': 'testEtag.gif', + 'public': True, + 'datas_fname': 'testEtag.gif', + 'mimetype': 'image/gif', + }) + response = self.url_open('/web/image/%s' % attachment.id) + self.assertEqual(response.status_code, 200) + self.assertEqual(base64.b64encode(response.content), attachment.datas) + + etag = response.headers.get('ETag') + + response2 = self.url_open('/web/image/%s' % attachment.id, headers={"If-None-Match": etag}) + self.assertEqual(response2.status_code, 304) + self.assertEqual(len(response2.content), 0) diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js index aacdfff9e0c..fa642338399 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js @@ -68,6 +68,8 @@ Wysiwyg.include({ } $(document.body).off('.' + this.id); + this.$iframe.parents().removeClass('o_wysiwyg_no_transform'); + this.$target.insertBefore(this.$iframe); delete window.top[this._onUpdateIframeId]; @@ -100,6 +102,10 @@ Wysiwyg.include({ } self.$iframe.toggleClass('o_fullscreen'); self.$iframe.contents().find('body').toggleClass('o_fullscreen'); + + // Hack to avoid a parent of the fullscreen iframe to have a + // transform (otherwise the position: fixed won't work) + self.$iframe.parents().toggleClass('o_wysiwyg_no_transform'); }, isFullscreen: function () { if (!self.$iframe) { diff --git a/addons/web_editor/static/src/scss/wysiwyg_iframe.scss b/addons/web_editor/static/src/scss/wysiwyg_iframe.scss index d019fdce753..1124d903353 100644 --- a/addons/web_editor/static/src/scss/wysiwyg_iframe.scss +++ b/addons/web_editor/static/src/scss/wysiwyg_iframe.scss @@ -10,6 +10,10 @@ iframe.wysiwyg_iframe.o_fullscreen { border: 0; } +.o_wysiwyg_no_transform { + transform: none !important; +} + body.o_in_iframe { background-color: $o-view-background-color; overflow: hidden; diff --git a/addons/website/static/src/scss/compatibility/bs3_for_12_0.scss b/addons/website/static/src/scss/compatibility/bs3_for_12_0.scss index 4f93429aa58..179584b78b9 100644 --- a/addons/website/static/src/scss/compatibility/bs3_for_12_0.scss +++ b/addons/website/static/src/scss/compatibility/bs3_for_12_0.scss @@ -9,10 +9,12 @@ $-compat-gray-map: ( 'gray': '700', 'gray-light': '600', 'gray-lighter': '200', -); +) !default; @each $old, $new in $-compat-gray-map { - @include bg-variant(".bg-#{$old}", gray($new)); - @include text-emphasis-variant(".text-#{$old}", gray($new)); + @if not map-has-key($grays, $old) { + @include bg-variant(".bg-#{$old}", gray($new)); + @include text-emphasis-variant(".text-#{$old}", gray($new)); + } } // Restore media ? @@ -210,9 +212,13 @@ $-compat-breakpoints: ( .hide { display: none !important; } -.show { - display: block !important; -} +// The 'show' class could be supported if defined here and like that, +// unfortunately, BS4 still defines a 'show' class for other purposes which +// conflict with this (tab-pane, fade effects, ...). Adding more complex rules +// won't solve the problem as they would change css rules priorities. +// .show { +// display: block !important; +// } .hidden { display: none !important; } diff --git a/addons/website/static/src/scss/website.wysiwyg.scss b/addons/website/static/src/scss/website.wysiwyg.scss index a4d8cea8729..d3fb73c5591 100644 --- a/addons/website/static/src/scss/website.wysiwyg.scss +++ b/addons/website/static/src/scss/website.wysiwyg.scss @@ -1,84 +1,11 @@ +//------------------------------------------------------------------------------ +// Modifications applied on normal website DOM while being in the editor. +//------------------------------------------------------------------------------ + body.editor_enable { overflow: hidden; padding-top: 0 !important; - // TOP BAR (EDIT) - #web_editor-top-edit { - height: $o-navbar-height; - background-color: $o-we-color-dark; - z-index: $zindex-modal - 9; - position: fixed; - top: 0px; - right: 0px; - width: auto; - white-space: nowrap; - - background-color: rgba(0, 0, 0, 0); - transition: background-color 400ms $o-we-md-ease 0s; - font-family: $o-we-font-family; - - form.navbar-form { - height: $o-navbar-height; - z-index: 1060; - margin: 0; - padding: 0; - position: absolute; - top: -1px; - right: 0; - transition: right 0.4s $o-we-md-ease 0s; - border-left: 1px solid $o-we-color-divider; - background-color: inherit; - - .btn-group { - height: 100%; - } - - .btn { - height: 100%; - margin: 0; - padding: 10px; - line-height: 1.2; - font-size: 13px; - font-family: $o-we-font-family; - - transition: all 0.3s ease 0s; - border: none; - border-radius: 0; - - .fa { - margin-right: $grid-gutter-width/4; - - &.fa-times { - color: $o-we-color-danger; - } - } - - &.btn-primary { - @include button-variant($o-brand-odoo, $o-brand-odoo); - } - - &.btn-secondary { - @include button-variant($o-we-color-dark, $o-we-color-dark); - - &:hover { - background: rgba(black, 0.5); - } - } - - &:focus, - &:active, - &:focus:active { - outline: none; - } - } - - .dropdown-menu { - left: auto; - right: 0; - } - } - } - #wrapwrap { height: calc(100vh - #{$o-navbar-height}); padding-top: 0px; @@ -87,555 +14,636 @@ body.editor_enable { -webkit-user-select: none; } } +} - // Translations - .oe_translate_examples li { - margin: 10px; - padding: 4px; - } +//------------------------------------------------------------------------------ +// Editor components style. +//------------------------------------------------------------------------------ - html[lang]>body.editor_enable [data-oe-translation-state] { - background: rgba($o-we-content-to-translate-color, 0.5) !important; +// TOP BAR (EDIT) +#web_editor-top-edit { + height: $o-navbar-height; + background-color: $o-we-color-dark; + z-index: $zindex-modal - 9; + position: fixed; + top: 0px; + right: 0px; + width: auto; + white-space: nowrap; - &[data-oe-translation-state="translated"] { - background: rgba($o-we-translated-content-color, 0.5) !important; + background-color: rgba(0, 0, 0, 0); + transition: background-color 400ms $o-we-md-ease 0s; + font-family: $o-we-font-family; + + form.navbar-form { + height: $o-navbar-height; + z-index: 1060; + margin: 0; + padding: 0; + position: absolute; + top: -1px; + right: 0; + transition: right 0.4s $o-we-md-ease 0s; + border-left: 1px solid $o-we-color-divider; + background-color: inherit; + + .btn-group { + height: 100%; } - &.o_dirty { - background: rgba($o-we-translated-content-color, 0.25) !important; - } - } - - // NOTE EDITOR - - // SNIPPET PANEL - #oe_snippets { - @include o-w-preserve-btn; - @include o-position-absolute(0, auto, 0, -$o-we-sidebar-width); - width: $o-we-sidebar-width; - - border-right: 1px solid $o-we-color-divider; - transition: left 400ms $o-we-md-ease 0s; - background-image: linear-gradient(45deg, $o-we-color-normal, darken($o-we-color-normal, 10%)); - - #snippets_menu { - color: #999999; - background: $o-we-color-dark; + .btn { + height: 100%; + margin: 0; + padding: 10px; + line-height: 1.2; + font-size: 13px; font-family: $o-we-font-family; - height: $o-navbar-height; - line-height: $o-navbar-height; - } - #o_scroll { - .o_panel_header { - color: #999999; - } - } - } + transition: all 0.3s ease 0s; + border: none; + border-radius: 0; - .oe_snippet { - background-color: $o-we-color-normal; + .fa { + margin-right: $grid-gutter-width/4; - .oe_snippet_thumbnail { - .oe_snippet_thumbnail_img { - border: 1px solid $o-we-color-normal; - } - - img.oe_snippet_thumbnail_img {} - - .oe_snippet_thumbnail_title { - border: 1px solid $o-we-color-dark; - color: $o-we-color-text-light; - } - - &:hover .oe_snippet_thumbnail_title { - color: $o-we-color-text-lighter; - } - } - } - - // SNIPPET OPTIONS - .colorpicker { - .o_colorpicker_sections { - .note-color-palette>div { - color: $o-we-color-text-light; - } - } - - .o_colorpicker_section { - &:after { - content: ""; - display: table; - clear: both; - } - - >button { - - &:hover, - &.selected { - box-shadow: 0px 0px 2px 2px $o-we-color-light; - } - - &[data-event="foreColor"] { - background-color: $o-we-color-normal; - - &:before { - background-color: rgba(white, 0.3); - } - } - } - } - - .note-palette-title { - color: $o-we-color-text-light; - } - - .palette-reset { - .note-color-reset { - color: desaturate(rgba($o-we-color-danger, 0.6), 40%); - - &:hover { + &.fa-times { color: $o-we-color-danger; } } - } - } - // DROPZONES - #wrapwrap .oe_drop_zone { - &.oe_insert { - height: $o-we-dropzone-size; - margin: (-$o-we-dropzone-size/2) 0; - - &:after { - border-bottom: $o-we-dropzone-border; + &.btn-primary { + @include button-variant($o-brand-odoo, $o-brand-odoo); } - &.oe_vertical { - width: $o-we-dropzone-size; - margin: 0 (-$o-we-dropzone-size/2); + &.btn-secondary { + @include button-variant($o-we-color-dark, $o-we-color-dark); - &:after { - border-right: $o-we-dropzone-border; - } - } - } - } - - // MANIPULATORS - #oe_manipulators { - - // SNIPPET MANIPULATORS - .oe_overlay { - - // OVERLAY OPTIONS - >.oe_overlay_options { - font-family: $o-we-font-family; - - >.btn-group { - .btn { - height: $o-we-overlay-option-size; - border: 1px solid $o-we-color-dark; - line-height: #{$o-we-overlay-option-size - 2px}; - - color: $o-we-color-text-light; - background-color: $o-we-color-light; - - &:hover { - background-color: lighten($o-we-color-dark, 10%); - } - - &.oe_snippet_remove { - background-color: $o-we-color-danger; - border-color: darken($o-we-color-danger, 20%); - - &:hover { - background-color: darken($o-we-color-danger, 20%); - } - } - - >.fa { - color: $o-we-color-text-light; - } - } - - // CUSTOMIZE MENU BUTTON - >.oe_options { - .btn { - background-color: $o-we-color-dark; - color: $o-we-color-text-light; - - &:hover, - &:active, - &:focus { - background-color: lighten($o-we-color-dark, 5%); - border-color: lighten($o-we-color-dark, 10%); - } - - &:before, - &:after { - background-color: $o-we-color-text-light; - } - } - - // Open menu - &.show .btn { - // background-color: $o-we-color-light; - } - - // CUSTOMIZE MENU - .dropdown-menu { - border: 1px solid $o-we-color-dark; - background-color: $o-we-color-light; - - .dropdown-item { - color: $o-we-color-text-light; - - &:hover { - background-color: fade-out($o-we-color-dark, 0.5); - } - - &.active { - background-color: fade-out($o-we-color-dark, 0.5); - } - } - - .dropdown-header { - color: wheat; - - &.o_parent_editor_header { - color: wheat; - } - } - - .dropdown-submenu { - &::before { - border-left-color: $o-we-color-text-light; - } - } - } - } + &:hover { + background: rgba(black, 0.5); } } - &.o_top_cover>.oe_overlay_options { - top: $o-we-handle-border-width; - right: $o-we-handle-border-width; - } - - } - } - - // Animations - @keyframes fadeInDownSmall { - 0% { - opacity: 0; - transform: translate(0, -5px); - } - - 100% { - opacity: 1; - transform: translate(0, 0); - } - } - - @keyframes fadeInOut { - 0% { - opacity: 0; - } - - 25% { - opacity: 1; - } - - 75% { - opacity: 1; - } - - 100% { - opacity: 0; - } - } - - @keyframes inputHighlighter { - from { - background: $o-brand-primary; - } - - to { - width: 0; - background: transparent; - } - } - - // MODALS - body .modal { - - // BACKGROUND IMAGE OPTIONS - .o_bg_img_opt_modal .o_bg_img_opt { - margin: 15px 0; - - .help-control { - @include o-position-absolute(15px, 15px); - } - - .help { - margin: 15px 0; - - .simulator { - position: relative; - float: left; - margin-right: 15px; - width: 120px; - height: 100px; - - .bg { - @include o-position-absolute(0, 0, 0, 0); - border: 1px dotted #888787; - background-image: url(/web/image); - background-position: center center; - background-size: cover; - } - - .el { - @include o-position-absolute(10px, 0, 10px, 0); - border: 1px solid #222; - } - - &.contain_bg { - .bg { - @include o-position-absolute(0, 12px, 20px, 12px); - } - - .el { - @include o-position-absolute(0, 0, 20px, 0); - } - } - } - - +* { - clear: left; - } - } - - .o_bg_img_opt_cover_edition { - @include o-we-preview-box; - padding: 0 25px 25px 25px; - - h6 { - color: white; - font-weight: bold; - } - - .o_bg_img_opt_object { - position: relative; - background: white; - min-height: 10px; - @include o-we-preview-content; - - >img { - cursor: crosshair; - border-top: 1px solid #5A5A5A; - border-bottom: 1px solid #111; - } - - &:hover .o_focus_point:before { - opacity: 0.5; - } - } - - .o_bg_img_opt_ui_info { - animation: fadeInOut 2s ease forwards; - @include o-position-absolute($bottom: 1px, $right: 0); - display: block; - padding: 3px 13px; - background-color: rgba(255, 255, 255, 0.8); - color: #333; - text-align: center; - font-weight: bold; - pointer-events: none; - - span { - font-weight: normal; - } - - .o_x { - margin-right: 10px - } - } - - .grid { - @include o-position-absolute($top: 33.33%, $left: 0); - display: block; - width: 100%; - height: 1px; - background: fade-out(lighten($o-brand-primary, 30%), 0.5); - pointer-events: none; - - &.grid-2 { - top: 66.66% - } - - &.grid-3 { - top: 0; - left: 33.33%; - width: 1px; - height: 100% - } - - &.grid-4 { - top: 0; - left: 66.66%; - width: 1px; - height: 100% - } - } - - .o_focus_point { - @include o-position-absolute($top: 0, $left: 0); - width: 30px; - height: 30px; - border: 2px solid white; - border-radius: 100%; - margin-top: -15px; - margin-left: -15px; - box-shadow: 0 0 1px #333; - pointer-events: none; - - &.o_with_transition { - transition: all 0.2s ease 0s; - } - - &:before { - pointer-events: none; - content: ""; - display: block; - width: 100px; - height: 100px; - margin-top: -37px; - margin-left: -37px; - border: 1px solid #EAEAEA; - background: rgba(247, 76, 76, 0); - border-radius: 100em; - opacity: 1; - box-shadow: 0 0 0 100em rgba(0, 0, 0, 0.33); - transition: opacity 0.2s ease 0s; - } - } - } - } - } - - .wysiwyg_multizone.note-editor { - &>.note-toolbar-wrapper .note-toolbar { - height: $o-navbar-height; - - .btn { - height: $o-navbar-height !important; // force this because of themes - } - } - - &>.note-statusbar { - display: none; - } - - &>.note-editing-area { - overflow: visible; - } - } - - .wysiwyg_multizone.note-popover.popover { - border-width: 0 1px; - - .popover-content .btn { - height: $o-navbar-height; - } - } - - .wysiwyg_multizone.note-popover, - .wysiwyg_multizone.note-editor>.note-toolbar-wrapper .note-toolbar { - // force this because of themes - background-color: $o-we-color-dark !important; - - .btn { - background-color: $o-we-color-dark !important; - color: $o-we-color-text-normal !important; - border: none !important; - padding: 0.0625rem 0.3125rem !important; - - >.fa { - color: $o-we-color-text-normal !important; - } - - &:hover { - color: $o-we-color-text-light !important; - - >.fa { - color: $o-we-color-text-light !important; - } - } - - &.active { - background-color: $o-we-color-dark !important; - color: $o-we-color-text-light !important; - - >.fa { - color: $o-we-color-text-light !important; - } - } - } - - >.btn-group { - margin-top: 0 !important; - - &.show, - .btn-group.show { - .dropdown-toggle { - border-top: 2px solid white !important; - background-color: $o-we-color-darker !important; - color: white !important; - box-shadow: none !important; - - >.fa, - >span { - color: white !important; - } - } + &:focus, + &:active, + &:focus:active { + outline: none; } } .dropdown-menu { - margin: 0 !important; - border-radius: 0 !important; - background-color: $o-we-color-darker !important; - color: $o-we-color-text-normal !important; - white-space: normal !important; + left: auto; + right: 0; + } + } +} - .dropdown-item { - width: 100% !important; - color: $o-we-color-text-light !important; +// Translations +.oe_translate_examples li { + margin: 10px; + padding: 4px; +} - >.fa { - color: $o-we-color-text-light !important; +html[lang] > body.editor_enable [data-oe-translation-state] { + background: rgba($o-we-content-to-translate-color, 0.5) !important; + + &[data-oe-translation-state="translated"] { + background: rgba($o-we-translated-content-color, 0.5) !important; + } + + &.o_dirty { + background: rgba($o-we-translated-content-color, 0.25) !important; + } +} + +// NOTE EDITOR + +// SNIPPET PANEL +#oe_snippets { + @include o-w-preserve-btn; + @include o-position-absolute(0, auto, 0, -$o-we-sidebar-width); + width: $o-we-sidebar-width; + + border-right: 1px solid $o-we-color-divider; + transition: left 400ms $o-we-md-ease 0s; + background-image: linear-gradient(45deg, $o-we-color-normal, darken($o-we-color-normal, 10%)); + + #snippets_menu { + color: #999999; + background: $o-we-color-dark; + font-family: $o-we-font-family; + height: $o-navbar-height; + line-height: $o-navbar-height; + } + + #o_scroll { + .o_panel_header { + color: #999999; + } + } +} + +.oe_snippet { + background-color: $o-we-color-normal; + + .oe_snippet_thumbnail { + .oe_snippet_thumbnail_img { + border: 1px solid $o-we-color-normal; + } + + img.oe_snippet_thumbnail_img {} + + .oe_snippet_thumbnail_title { + border: 1px solid $o-we-color-dark; + color: $o-we-color-text-light; + } + + &:hover .oe_snippet_thumbnail_title { + color: $o-we-color-text-lighter; + } + } +} + +// SNIPPET OPTIONS +.colorpicker { + .o_colorpicker_sections { + .note-color-palette>div { + color: $o-we-color-text-light; + } + } + + .o_colorpicker_section { + &:after { + content: ""; + display: table; + clear: both; + } + + >button { + + &:hover, + &.selected { + box-shadow: 0px 0px 2px 2px $o-we-color-light; + } + + &[data-event="foreColor"] { + background-color: $o-we-color-normal; + + &:before { + background-color: rgba(white, 0.3); } + } + } + } - &:hover { - background-color: $o-we-tab-active-bg !important; - color: white !important; + .note-palette-title { + color: $o-we-color-text-light; + } + + .palette-reset { + .note-color-reset { + color: desaturate(rgba($o-we-color-danger, 0.6), 40%); + + &:hover { + color: $o-we-color-danger; + } + } + } +} + +// DROPZONES +#wrapwrap .oe_drop_zone { + &.oe_insert { + height: $o-we-dropzone-size; + margin: (-$o-we-dropzone-size/2) 0; + + &:after { + border-bottom: $o-we-dropzone-border; + } + + &.oe_vertical { + width: $o-we-dropzone-size; + margin: 0 (-$o-we-dropzone-size/2); + + &:after { + border-right: $o-we-dropzone-border; + } + } + } +} + +// MANIPULATORS +#oe_manipulators { + + // SNIPPET MANIPULATORS + .oe_overlay { + + // OVERLAY OPTIONS + >.oe_overlay_options { + font-family: $o-we-font-family; + + >.btn-group { + .btn { + height: $o-we-overlay-option-size; + border: 1px solid $o-we-color-dark; + line-height: #{$o-we-overlay-option-size - 2px}; + + color: $o-we-color-text-light; + background-color: $o-we-color-light; + + &:hover { + background-color: lighten($o-we-color-dark, 10%); + } + + &.oe_snippet_remove { + background-color: $o-we-color-danger; + border-color: darken($o-we-color-danger, 20%); + + &:hover { + background-color: darken($o-we-color-danger, 20%); + } + } >.fa { - color: white !important; + color: $o-we-color-text-light; + } + } + + // CUSTOMIZE MENU BUTTON + >.oe_options { + .btn { + background-color: $o-we-color-dark; + color: $o-we-color-text-light; + + &:hover, + &:active, + &:focus { + background-color: lighten($o-we-color-dark, 5%); + border-color: lighten($o-we-color-dark, 10%); + } + + &:before, + &:after { + background-color: $o-we-color-text-light; + } + } + + // Open menu + &.show .btn { + // background-color: $o-we-color-light; + } + + // CUSTOMIZE MENU + .dropdown-menu { + border: 1px solid $o-we-color-dark; + background-color: $o-we-color-light; + + .dropdown-item { + color: $o-we-color-text-light; + + &:hover { + background-color: fade-out($o-we-color-dark, 0.5); + } + + &.active { + background-color: fade-out($o-we-color-dark, 0.5); + } + } + + .dropdown-header { + color: wheat; + + &.o_parent_editor_header { + color: wheat; + } + } + + .dropdown-submenu { + &::before { + border-left-color: $o-we-color-text-light; + } + } } } } } - .note-para .dropdown-menu { - min-width: 147px !important; + &.o_top_cover>.oe_overlay_options { + top: $o-we-handle-border-width; + right: $o-we-handle-border-width; } - .note-style .dropdown-item * { - color: $o-we-color-text-light !important; + } +} + +// Animations +@keyframes fadeInDownSmall { + 0% { + opacity: 0; + transform: translate(0, -5px); + } + + 100% { + opacity: 1; + transform: translate(0, 0); + } +} + +@keyframes fadeInOut { + 0% { + opacity: 0; + } + + 25% { + opacity: 1; + } + + 75% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +@keyframes inputHighlighter { + from { + background: $o-brand-primary; + } + + to { + width: 0; + background: transparent; + } +} + +// MODALS +body .modal { + + // BACKGROUND IMAGE OPTIONS + .o_bg_img_opt_modal .o_bg_img_opt { + margin: 15px 0; + + .help-control { + @include o-position-absolute(15px, 15px); + } + + .help { + margin: 15px 0; + + .simulator { + position: relative; + float: left; + margin-right: 15px; + width: 120px; + height: 100px; + + .bg { + @include o-position-absolute(0, 0, 0, 0); + border: 1px dotted #888787; + background-image: url(/web/image); + background-position: center center; + background-size: cover; + } + + .el { + @include o-position-absolute(10px, 0, 10px, 0); + border: 1px solid #222; + } + + &.contain_bg { + .bg { + @include o-position-absolute(0, 12px, 20px, 12px); + } + + .el { + @include o-position-absolute(0, 0, 20px, 0); + } + } + } + + +* { + clear: left; + } + } + + .o_bg_img_opt_cover_edition { + @include o-we-preview-box; + padding: 0 25px 25px 25px; + + h6 { + color: white; + font-weight: bold; + } + + .o_bg_img_opt_object { + position: relative; + background: white; + min-height: 10px; + @include o-we-preview-content; + + >img { + cursor: crosshair; + border-top: 1px solid #5A5A5A; + border-bottom: 1px solid #111; + } + + &:hover .o_focus_point:before { + opacity: 0.5; + } + } + + .o_bg_img_opt_ui_info { + animation: fadeInOut 2s ease forwards; + @include o-position-absolute($bottom: 1px, $right: 0); + display: block; + padding: 3px 13px; + background-color: rgba(255, 255, 255, 0.8); + color: #333; + text-align: center; + font-weight: bold; + pointer-events: none; + + span { + font-weight: normal; + } + + .o_x { + margin-right: 10px + } + } + + .grid { + @include o-position-absolute($top: 33.33%, $left: 0); + display: block; + width: 100%; + height: 1px; + background: fade-out(lighten($o-brand-primary, 30%), 0.5); + pointer-events: none; + + &.grid-2 { + top: 66.66% + } + + &.grid-3 { + top: 0; + left: 33.33%; + width: 1px; + height: 100% + } + + &.grid-4 { + top: 0; + left: 66.66%; + width: 1px; + height: 100% + } + } + + .o_focus_point { + @include o-position-absolute($top: 0, $left: 0); + width: 30px; + height: 30px; + border: 2px solid white; + border-radius: 100%; + margin-top: -15px; + margin-left: -15px; + box-shadow: 0 0 1px #333; + pointer-events: none; + + &.o_with_transition { + transition: all 0.2s ease 0s; + } + + &:before { + pointer-events: none; + content: ""; + display: block; + width: 100px; + height: 100px; + margin-top: -37px; + margin-left: -37px; + border: 1px solid #EAEAEA; + background: rgba(247, 76, 76, 0); + border-radius: 100em; + opacity: 1; + box-shadow: 0 0 0 100em rgba(0, 0, 0, 0.33); + transition: opacity 0.2s ease 0s; + } + } } } } + +.wysiwyg_multizone.note-editor { + &>.note-toolbar-wrapper .note-toolbar { + height: $o-navbar-height; + + .btn { + height: $o-navbar-height !important; // force this because of themes + } + } + + &>.note-statusbar { + display: none; + } + + &>.note-editing-area { + overflow: visible; + } +} + +.wysiwyg_multizone.note-popover.popover { + border-width: 0 1px; + + .popover-content .btn { + height: $o-navbar-height; + } +} + +.wysiwyg_multizone.note-popover, +.wysiwyg_multizone.note-editor>.note-toolbar-wrapper .note-toolbar { + // force this because of themes + background-color: $o-we-color-dark !important; + + .btn { + background-color: $o-we-color-dark !important; + color: $o-we-color-text-normal !important; + border: none !important; + padding: 0.0625rem 0.3125rem !important; + + >.fa { + color: $o-we-color-text-normal !important; + } + + &:hover { + color: $o-we-color-text-light !important; + + >.fa { + color: $o-we-color-text-light !important; + } + } + + &.active { + background-color: $o-we-color-dark !important; + color: $o-we-color-text-light !important; + + >.fa { + color: $o-we-color-text-light !important; + } + } + } + + >.btn-group { + margin-top: 0 !important; + + &.show, + .btn-group.show { + .dropdown-toggle { + border-top: 2px solid white !important; + background-color: $o-we-color-darker !important; + color: white !important; + box-shadow: none !important; + + >.fa, + >span { + color: white !important; + } + } + } + } + + .dropdown-menu { + margin: 0 !important; + border-radius: 0 !important; + background-color: $o-we-color-darker !important; + color: $o-we-color-text-normal !important; + white-space: normal !important; + + .dropdown-item { + width: 100% !important; + color: $o-we-color-text-light !important; + + >.fa { + color: $o-we-color-text-light !important; + } + + &:hover { + background-color: $o-we-tab-active-bg !important; + color: white !important; + + >.fa { + color: white !important; + } + } + } + } + + .note-para .dropdown-menu { + min-width: 147px !important; + } + + .note-style .dropdown-item * { + color: $o-we-color-text-light !important; + } +} diff --git a/addons/website_mass_mailing/views/snippets_templates.xml b/addons/website_mass_mailing/views/snippets_templates.xml index f0fee84ad53..7bc1c4ac2d6 100644 --- a/addons/website_mass_mailing/views/snippets_templates.xml +++ b/addons/website_mass_mailing/views/snippets_templates.xml @@ -3,6 +3,8 @@ diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 0429f21de5f..112b209bfcd 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -608,9 +608,12 @@ class WebsiteSale(http.Controller): new_values['customer'] = True new_values['team_id'] = request.website.salesteam_id and request.website.salesteam_id.id new_values['user_id'] = request.website.salesperson_id and request.website.salesperson_id.id - new_values['company_id'] = request.website.company_id.id new_values['website_id'] = request.website.id + if order.partner_id.id == request.website.user_id.sudo().partner_id.id: + # If the partner is public, we assign the company of the website + new_values['company_id'] = request.website.company_id.id + lang = request.lang if request.lang in request.website.mapped('language_ids.code') else None if lang: new_values['lang'] = lang diff --git a/addons/website_sale_comparison/static/src/js/website_sale_comparison.js b/addons/website_sale_comparison/static/src/js/website_sale_comparison.js index 306ee1a44f9..48eb44494c4 100644 --- a/addons/website_sale_comparison/static/src/js/website_sale_comparison.js +++ b/addons/website_sale_comparison/static/src/js/website_sale_comparison.js @@ -1,6 +1,7 @@ odoo.define('website_sale_comparison.comparison', function (require) { 'use strict'; +var concurrency = require('web.concurrency'); var core = require('web.core'); var publicWidget = require('web.public.widget'); var utils = require('web.utils'); @@ -30,6 +31,7 @@ var ProductComparison = publicWidget.Widget.extend(VariantMixin, { this.product_data = {}; this.comparelist_product_ids = JSON.parse(utils.get_cookie('comparelist_product_ids') || '[]'); this.product_compare_limit = 4; + this.guard = new concurrency.Mutex(); }, /** * @override @@ -154,6 +156,9 @@ var ProductComparison = publicWidget.Widget.extend(VariantMixin, { * @private */ _addNewProducts: function (product_id) { + this.guard.exec(this._addNewProductsImpl.bind(this, product_id)); + }, + _addNewProductsImpl: function (product_id) { var self = this; $('.o_product_feature_panel').addClass('d-md-block'); if (!_.contains(self.comparelist_product_ids, product_id)) { @@ -161,8 +166,9 @@ var ProductComparison = publicWidget.Widget.extend(VariantMixin, { if (_.has(self.product_data, product_id)){ self._updateContent(); } else { - self._loadProducts([product_id]).then(function () { + return self._loadProducts([product_id]).then(function () { self._updateContent(); + self._updateCookie(); }); } } @@ -189,6 +195,9 @@ var ProductComparison = publicWidget.Widget.extend(VariantMixin, { * @private */ _removeFromComparelist: function (e) { + this.guard.exec(this._removeFromComparelistImpl.bind(this, e)); + }, + _removeFromComparelistImpl: function (e) { this.comparelist_product_ids = _.without(this.comparelist_product_ids, $(e.currentTarget).data('product_product_id')); $(e.currentTarget).parents('.o_product_row').remove(); this._updateCookie(); diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py index f4a02f8937a..dfcc4e4e7de 100644 --- a/addons/website_slides/controllers/main.py +++ b/addons/website_slides/controllers/main.py @@ -379,6 +379,16 @@ class WebsiteSlides(WebsiteProfile): step=self._slides_per_page, url_args=pager_args, scope=page_count if page_count < self._pager_max_pages else self._pager_max_pages) + query_string = None + if category: + query_string = "?search_category=%s" % category.id + elif tag: + query_string = "?search_tag=%s" % tag.id + elif slide_type: + query_string = "?search_slide_type=%s" % slide_type + elif uncategorized: + query_string = "?search_uncategorized=1" + values = { 'channel': channel, 'active_tab': kw.get('active_tab', 'home'), @@ -387,6 +397,7 @@ class WebsiteSlides(WebsiteProfile): 'search_tag': tag, 'search_slide_type': slide_type, 'search_uncategorized': uncategorized, + 'query_string': query_string, 'slide_types': slide_types, 'sorting': actual_sorting, 'search': search, @@ -506,7 +517,7 @@ class WebsiteSlides(WebsiteProfile): @http.route('''/slides/slide/''', type='http', auth="public", website=True) def slide_view(self, slide, **kwargs): - if not slide.channel_id.can_access_from_current_website(): + if not slide.channel_id.can_access_from_current_website() or not slide.active: raise werkzeug.exceptions.NotFound() self._set_viewed_slide(slide) @@ -517,6 +528,15 @@ class WebsiteSlides(WebsiteProfile): # sidebar: update with user channel progress values['channel_progress'] = self._get_channel_progress(slide.channel_id, include_quiz=True) + # Allows to have breadcrumb for the previously used filter + values.update({ + 'search_category': slide.category_id.id if kwargs.get('search_category') else None, + 'search_tag': request.env['slide.tag'].browse(int(kwargs.get('search_tag'))) if kwargs.get('search_tag') else None, + 'slide_types': dict(request.env['slide.slide']._fields['slide_type']._description_selection(request.env)) if kwargs.get('search_slide_type') else None, + 'search_slide_type': kwargs.get('search_slide_type'), + 'search_uncategorized': kwargs.get('search_uncategorized') + }) + if kwargs.get('fullscreen') == '1': return request.render("website_slides.slide_fullscreen", values) return request.render("website_slides.slide_main", values) diff --git a/addons/website_slides/models/slide_slide.py b/addons/website_slides/models/slide_slide.py index b0567dcc627..851ec30fd4d 100644 --- a/addons/website_slides/models/slide_slide.py +++ b/addons/website_slides/models/slide_slide.py @@ -135,7 +135,7 @@ class Slide(models.Model): ('video', 'Video'), ('quiz', "Quiz")], string='Type', required=True, - default='document', readonly=True, + default='document', help="The document type will be set automatically based on the document URL and properties (e.g. height and width for presentation and document).") index_content = fields.Text('Transcript') datas = fields.Binary('Content', attachment=True) @@ -553,11 +553,16 @@ class Slide(models.Model): return result def _find_document_data_from_url(self, url): - expr = re.compile(r'^.*((youtu.be/)|(v/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*') - arg = expr.match(url) - document_id = arg and arg.group(7) or False - if document_id: - return ('youtube', document_id) + url_obj = urls.url_parse(url) + if url_obj.ascii_host == 'youtu.be': + return ('youtube', url_obj.path[1:] if url_obj.path else False) + elif url_obj.ascii_host in ('youtube.com', 'www.youtube.com', 'm.youtube.com'): + v_query_value = url_obj.decode_query().get('v') + if v_query_value: + return ('youtube', v_query_value) + split_path = url_obj.path.split('/') + if len(split_path) >= 3 and split_path[1] in ('v', 'embed'): + return ('youtube', split_path[2]) expr = re.compile(r'(^https:\/\/docs.google.com|^https:\/\/drive.google.com).*\/d\/([^\/]*)') arg = expr.match(url) diff --git a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js index 6fa054b6647..8a648a3cb3b 100644 --- a/addons/website_slides/static/src/js/slides_course_fullscreen_player.js +++ b/addons/website_slides/static/src/js/slides_course_fullscreen_player.js @@ -5,6 +5,7 @@ odoo.define('website_slides.fullscreen', function (require) { var publicWidget = require('web.public.widget'); var core = require('web.core'); + var config = require('web.config'); var QWeb = core.qweb; var session = require('web.session'); @@ -321,6 +322,9 @@ odoo.define('website_slides.fullscreen', function (require) { start: function (){ var self = this; this.on('change:slide', this, this._onChangeSlide); + if (config.device.size_class > config.device.SIZES.MD) { + this._toggleSidebar(); + } return this._super.apply(this, arguments).then(function () { self._onChangeSlide(); // trigger manually once DOM ready, since slide content is not rendered server side }); @@ -540,12 +544,20 @@ odoo.define('website_slides.fullscreen', function (require) { this.sidebar.goNext(); }, /** - * Show or Hide the sidebar + * Called when the sidebar toggle is clicked -> toggles the sidebar visibility. * * @private */ _onClickToggleSidebar: function (ev){ ev.preventDefault(); + this._toggleSidebar(); + }, + /** + * Toggles sidebar visibility. + * + * @private + */ + _toggleSidebar: function () { this.$('.o_wslides_fs_sidebar').toggleClass('o_wslides_fs_sidebar_hidden'); this.$('.o_wslides_fs_toggle_sidebar').toggleClass('active'); }, diff --git a/addons/website_slides/static/src/js/slides_share.js b/addons/website_slides/static/src/js/slides_share.js index 3f921a2229a..d89cdfcb4bf 100644 --- a/addons/website_slides/static/src/js/slides_share.js +++ b/addons/website_slides/static/src/js/slides_share.js @@ -49,48 +49,12 @@ publicWidget.registry.websiteSlidesShare = publicWidget.Widget.extend({ * @param {Object} parent */ start: function (parent) { - var self = this; var defs = [this._super.apply(this, arguments)]; defs.push(new ShareMail(this).attachTo($('.oe_slide_js_share_email'))); - if ($('div#statistic').length) { - this.slideURL = $('div#statistic').attr('slide-url'); - this.socialURLs = { - linkedin: 'https://www.linkedin.com/countserv/count/share?url=', - twitter: 'https://cdn.api.twitter.com/1/urls/count.json?url=', - facebook: 'https://graph.facebook.com/?id=', - }; - } - - _.each(this.socialURLs, function (value, key) { - self._updateStatistics(key, self.slideURL); - }); - return Promise.all(defs); }, - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @private - * @param {string} socialSite - * @param {string} slide_url - */ - _updateStatistics: function (socialSite, slideURL) { - var self = this; - $.ajax({ - url: self.socialURLs[socialSite] + slideURL, - dataType: 'jsonp', - success: function (data) { - var shareCount = (socialSite === 'facebook' ? data.shares : data.count) || 0; - $('#' + socialSite + '-badge').text(shareCount); - $('#total-share').text(parseInt($('#total-share').text()) + parseInt($('#' + socialSite + '-badge').text())); - }, - }); - }, - //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- @@ -100,14 +64,11 @@ publicWidget.registry.websiteSlidesShare = publicWidget.Widget.extend({ * @param {Object} ev */ _onSlidesSocialShare: function (ev) { - var self = this; ev.preventDefault(); - var key = $(ev.currentTarget).attr('social-key'); var popUpURL = $(ev.currentTarget).attr('href'); var popUp = window.open(popUpURL, 'Share Dialog', 'width=626,height=436'); $(window).on('focus', function () { if (popUp.closed) { - self._updateStatistics(key, self.slide_url); $(window).off('focus'); } }); diff --git a/addons/website_slides/tests/__init__.py b/addons/website_slides/tests/__init__.py index 69bfc7de9e2..466b2b82674 100644 --- a/addons/website_slides/tests/__init__.py +++ b/addons/website_slides/tests/__init__.py @@ -5,3 +5,5 @@ from . import common from . import test_security from . import test_statistics from . import test_karma + +from . import test_from_url \ No newline at end of file diff --git a/addons/website_slides/tests/test_from_url.py b/addons/website_slides/tests/test_from_url.py new file mode 100644 index 00000000000..dc9a1d6768e --- /dev/null +++ b/addons/website_slides/tests/test_from_url.py @@ -0,0 +1,30 @@ +import odoo.tests + + +class TestFromURL(odoo.tests.TransactionCase): + def test_youtube_urls(self): + urls = { + 'W0JQcpGLSFw': [ + 'https://youtu.be/W0JQcpGLSFw', + 'https://www.youtube.com/watch?v=W0JQcpGLSFw', + 'https://www.youtube.com/watch?v=W0JQcpGLSFw&list=PL1-aSABtP6ACZuppkBqXFgzpNb2nVctZx', + ], + 'vmhB-pt7EfA': [ # id starts with v, it is important + 'https://youtu.be/vmhB-pt7EfA', + 'https://www.youtube.com/watch?feature=youtu.be&v=vmhB-pt7EfA', + 'https://www.youtube.com/watch?v=vmhB-pt7EfA&list=PL1-aSABtP6ACZuppkBqXFgzpNb2nVctZx&index=7', + ], + 'hlhLv0GN1hA': [ + 'https://www.youtube.com/v/hlhLv0GN1hA', + 'https://www.youtube.com/embed/hlhLv0GN1hA', + 'https://m.youtube.com/watch?v=hlhLv0GN1hA' + ], + } + + model = self.env['slide.slide'] + for id, urls in urls.items(): + for url in urls: + with self.subTest(url=url, id=id): + document = model._find_document_data_from_url(url) + self.assertEqual(document[0], 'youtube') + self.assertEqual(document[1], id) diff --git a/addons/website_slides/views/slide_slide_views.xml b/addons/website_slides/views/slide_slide_views.xml index b2dd1585ff0..1a2e45d5119 100644 --- a/addons/website_slides/views/slide_slide_views.xml +++ b/addons/website_slides/views/slide_slide_views.xml @@ -71,13 +71,13 @@ - + + attrs="{'required': [('slide_type', 'in', ('video'))], 'invisible': [('slide_type', 'not in', ('document', 'presentation', 'video'))]}" /> - @@ -93,6 +93,7 @@ +