summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Simonis <chs@odoo.com>2019-06-25 14:23:48 +0200
committerChristophe Simonis <chs@odoo.com>2019-06-25 14:23:48 +0200
commitcf51b5f25dbe37f6f749b949250497f6257957c3 (patch)
treef218ca8ece0c0ffa3ea9e6381e41111ab7c662ea
parent1c542adf863f6de731fda24a9ddc712337bceb61 (diff)
parent793933a1287d08c174d7ba00ed4762d1807b4923 (diff)
[MERGE] forward port branch saas-12.3 up to 793933a1287
-rw-r--r--MANIFEST.in1
-rw-r--r--addons/account/models/account_invoice.py5
-rw-r--r--addons/account/models/account_move.py2
-rw-r--r--addons/account/models/account_reconcile_model.py2
-rw-r--r--addons/account/wizard/account_invoice_send_views.xml1
-rw-r--r--addons/account_voucher/models/account_voucher.py9
-rw-r--r--addons/event_sale/models/sale_order.py4
-rw-r--r--addons/gamification/data/gamification_karma_rank_data.xml8
-rw-r--r--addons/hr_expense/models/hr_expense.py2
-rw-r--r--addons/hr_holidays/data/hr_holidays_data.xml1
-rw-r--r--addons/hr_holidays/data/hr_holidays_demo.xml2
-rw-r--r--addons/hr_holidays/models/hr_employee.py2
-rw-r--r--addons/hr_holidays/models/hr_leave.py19
-rw-r--r--addons/hr_holidays/models/hr_leave_type.py2
-rw-r--r--addons/hr_holidays/tests/test_access_rights.py1
-rw-r--r--addons/hr_holidays/tests/test_accrual_allocations.py2
-rw-r--r--addons/hr_holidays/tests/test_change_department.py1
-rw-r--r--addons/hr_holidays/tests/test_holidays_flow.py6
-rw-r--r--addons/hr_holidays/tests/test_hr_leave_type.py1
-rw-r--r--addons/hr_holidays/tests/test_leave_requests.py17
-rw-r--r--addons/hr_holidays/tests/test_out_of_office.py2
-rw-r--r--addons/l10n_be/data/account_tax_template_data.xml8
-rw-r--r--addons/l10n_ca/data/account.account.template.csv4
-rw-r--r--addons/l10n_de/data/account_account_tags_data.xml2
-rw-r--r--addons/l10n_do/data/account.account.template.csv4
-rw-r--r--addons/l10n_mn/data/account.chart.template.csv4
-rw-r--r--addons/l10n_mn/data/account_chart_template_data.xml1
-rw-r--r--addons/l10n_ua/data/account.account.template.csv8
-rw-r--r--addons/mail/models/mail_channel.py4
-rw-r--r--addons/mail/static/src/js/chatter.js6
-rw-r--r--addons/mail/static/src/js/form_view.js5
-rw-r--r--addons/mail/static/tests/chatter_tests.js53
-rw-r--r--addons/mrp/data/mrp_data.xml1
-rw-r--r--addons/mrp/models/mrp_production.py7
-rw-r--r--addons/mrp/tests/test_order.py33
-rw-r--r--addons/mrp/wizard/change_production_qty.py3
-rw-r--r--addons/payment/models/payment_acquirer.py2
-rw-r--r--addons/point_of_sale/static/src/js/models.js10
-rw-r--r--addons/point_of_sale/static/src/xml/pos.xml9
-rw-r--r--addons/product/report/product_pricelist.py3
-rw-r--r--addons/project_timesheet_holidays/tests/test_timesheet_holidays.py2
-rw-r--r--addons/sale/models/sale.py3
-rw-r--r--addons/sale_mrp/tests/test_sale_mrp_flow.py46
-rw-r--r--addons/survey_crm/i18n/az.po26
-rw-r--r--addons/web/controllers/main.py2
-rw-r--r--addons/web/static/lib/moment/locale/ar.js24
-rw-r--r--addons/web/static/src/js/views/control_panel/search/add_new_favorite_menu.js28
-rw-r--r--addons/web/static/src/js/views/list/list_renderer.js1
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_controller.js1
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_renderer.js2
-rw-r--r--addons/web/static/src/js/widgets/pie_chart.js7
-rw-r--r--addons/web/static/src/scss/list_view.scss8
-rw-r--r--addons/web/static/src/scss/pivot_view.scss5
-rw-r--r--addons/web/static/src/xml/base.xml2
-rw-r--r--addons/web/static/tests/views/control_panel_tests.js41
-rw-r--r--addons/web/static/tests/views/pivot_tests.js6
-rw-r--r--addons/web/tests/test_image.py21
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js2
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/helper.js21
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js6
-rw-r--r--addons/web_editor/static/src/scss/wysiwyg_iframe.scss4
-rw-r--r--addons/website/static/src/js/editor/wysiwyg_multizone.js6
-rw-r--r--addons/website/static/src/scss/compatibility/bs3_for_12_0.scss18
-rw-r--r--addons/website/static/src/scss/website.wysiwyg.scss922
-rw-r--r--addons/website_mass_mailing/views/snippets_templates.xml2
-rw-r--r--addons/website_sale/controllers/main.py5
-rw-r--r--addons/website_sale_comparison/static/src/js/website_sale_comparison.js22
-rw-r--r--addons/website_slides/controllers/main.py22
-rw-r--r--addons/website_slides/models/slide_slide.py17
-rw-r--r--addons/website_slides/static/src/js/slides_course_fullscreen_player.js14
-rw-r--r--addons/website_slides/static/src/js/slides_share.js39
-rw-r--r--addons/website_slides/tests/__init__.py2
-rw-r--r--addons/website_slides/tests/test_from_url.py30
-rw-r--r--addons/website_slides/views/slide_slide_views.xml7
-rw-r--r--addons/website_slides/views/website_slides_templates_course.xml42
-rw-r--r--addons/website_slides/views/website_slides_templates_homepage.xml2
-rw-r--r--addons/website_slides/views/website_slides_templates_lesson.xml85
-rw-r--r--addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml11
-rw-r--r--addons/website_slides/views/website_slides_templates_profile.xml6
-rw-r--r--doc/cla/corporate/bumanit.md15
-rw-r--r--doc/cla/individual/jakubhruby.md11
-rw-r--r--odoo/addons/base/data/res.country.state.csv30
-rw-r--r--odoo/addons/base/data/res_currency_data.xml1
-rw-r--r--odoo/addons/base/models/ir_actions.py5
-rw-r--r--odoo/addons/base/models/ir_http.py8
-rw-r--r--odoo/addons/base/models/ir_logging.py8
-rw-r--r--odoo/addons/base/models/ir_model.py1
-rw-r--r--odoo/addons/test_access_rights/tests/test_feedback.py6
-rw-r--r--odoo/netsvc.py5
-rw-r--r--odoo/tests/common.py8
90 files changed, 1149 insertions, 716 deletions
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 4a9cd324718..dbdb3b075a2 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 6b9469f7a18..24a3709a3a6 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -484,8 +484,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 b3f12709e8a..4966d498f1b 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 4f8888e8f54..08d00755a8b 100644
--- a/addons/account/wizard/account_invoice_send_views.xml
+++ b/addons/account/wizard/account_invoice_send_views.xml
@@ -12,6 +12,7 @@
<field name="composition_mode" invisible="1"/>
<field name="composer_id" invisible="1"/>
<field name="invoice_ids" invisible="1"/>
+ <field name="email_from" invisible="1" />
<div name="option_print">
<field name="is_print" />
<b><label for="is_print"/></b>
diff --git a/addons/account_voucher/models/account_voucher.py b/addons/account_voucher/models/account_voucher.py
index 4fa88c8de43..50192b50bf4 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 d705ab57881..6d3c2aa33ab 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.company
currency = company.currency_id or self.env.company.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 @@
<field name="description_motivational" type="html">
<div class="media align-items-center">
<div class="media-body">Reach the next rank to show the rest of the world you exist!</div>
- <img class="ml-3 img img-fluid" style="max-height: 72px;" src="gamification/static/img/rank_student_badge.svg"/>
+ <img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_student_badge.svg"/>
</div>
</field>
<field name="karma_min">1</field>
@@ -28,7 +28,7 @@
<field name="description_motivational" type="html">
<div class="media align-items-center">
<div class="media-body">Reach the next rank to improve your status.</div>
- <img class="ml-3 img img-fluid" style="max-height: 72px;" src="gamification/static/img/rank_bachelor_badge.svg"/>
+ <img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_bachelor_badge.svg"/>
</div>
</field>
<field name="karma_min">100</field>
@@ -41,7 +41,7 @@
<field name="description_motivational" type="html">
<div class="media align-items-center">
<div class="media-body">Reach the next rank and become a Master!</div>
- <img class="ml-3 img img-fluid" style="max-height: 72px;" src="gamification/static/img/rank_master_badge.svg"/>
+ <img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_master_badge.svg"/>
</div>
</field>
<field name="karma_min">500</field>
@@ -54,7 +54,7 @@
<field name="description_motivational" type="html">
<div class="media align-items-center">
<div class="media-body">Reach the next rank and become a powerful user!</div>
- <img class="ml-3 img img-fluid" style="max-height: 72px;" src="gamification/static/img/rank_doctor_badge.svg"/>
+ <img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_doctor_badge.svg"/>
</div>
</field>
<field name="karma_min">2000</field>
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index 43f272afffc..d429e9845f3 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.company)
currency_id = fields.Many2one('res.currency', string='Currency', readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.company.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 @@
<record id="holiday_status_hw" model="hr.leave.type">
<field name="name">Home Workings</field>
<field name="color_name">lightgreen</field>
+ <field name="validity_start" eval="False"/>
<field name="leave_notif_subtype_id" eval="ref('mt_leave_home_working')"/>
</record>
diff --git a/addons/hr_holidays/data/hr_holidays_demo.xml b/addons/hr_holidays/data/hr_holidays_demo.xml
index cbdd561200d..557d3153fbd 100644
--- a/addons/hr_holidays/data/hr_holidays_demo.xml
+++ b/addons/hr_holidays/data/hr_holidays_demo.xml
@@ -8,6 +8,7 @@
<field name="allocation_type">fixed_allocation</field>
<field name="color_name">blue</field>
<field name="request_unit">hour</field>
+ <field name="validity_start" eval="False"/>
</record>
<record id="hr_holiday_status_dv" model="hr.leave.type">
@@ -15,6 +16,7 @@
<field name="allocation_type">fixed</field>
<field name="color_name">brown</field>
<field name="validation_type">both</field>
+ <field name="validity_start" eval="False"/>
</record>
<record id="holiday_status_cla" model="hr.leave.type">
diff --git a/addons/hr_holidays/models/hr_employee.py b/addons/hr_holidays/models/hr_employee.py
index 3ff7b0ebcf6..c0a47809e04 100644
--- a/addons/hr_holidays/models/hr_employee.py
+++ b/addons/hr_holidays/models/hr_employee.py
@@ -115,7 +115,7 @@ class HrEmployeeBase(models.AbstractModel):
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 4a3d2be8666..4403e000a8e 100644
--- a/addons/hr_holidays/models/hr_leave.py
+++ b/addons/hr_holidays/models/hr_leave.py
@@ -521,16 +521,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 577c159d671..f12be3ebee5 100644
--- a/addons/hr_holidays/models/hr_leave_type.py
+++ b/addons/hr_holidays/models/hr_leave_type.py
@@ -181,7 +181,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 eacd2a396e0..fd517d9ffe6 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_be/data/account_tax_template_data.xml b/addons/l10n_be/data/account_tax_template_data.xml
index 98e17c58ae2..70c71a446b2 100644
--- a/addons/l10n_be/data/account_tax_template_data.xml
+++ b/addons/l10n_be/data/account_tax_template_data.xml
@@ -2102,7 +2102,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
- 'minus_report_line_ids': [ref('tax_report_line_82')],
+ 'minus_report_line_ids': [ref('tax_report_line_82'), ref('tax_report_line_88')],
'plus_report_line_ids': [ref('tax_report_line_84')],
}),
@@ -2208,7 +2208,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
- 'minus_report_line_ids': [ref('tax_report_line_82')],
+ 'minus_report_line_ids': [ref('tax_report_line_82'), ref('tax_report_line_88')],
'plus_report_line_ids': [ref('tax_report_line_84')],
}),
@@ -2314,7 +2314,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
- 'minus_report_line_ids': [ref('tax_report_line_82')],
+ 'minus_report_line_ids': [ref('tax_report_line_82'), ref('tax_report_line_88')],
'plus_report_line_ids': [ref('tax_report_line_84')],
}),
@@ -2413,7 +2413,7 @@
(0,0, {
'factor_percent': 100,
'repartition_type': 'base',
- 'minus_report_line_ids': [ref('tax_report_line_82')],
+ 'minus_report_line_ids': [ref('tax_report_line_82'), ref('tax_report_line_88')],
'plus_report_line_ids': [ref('tax_report_line_84')],
}),
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_de/data/account_account_tags_data.xml b/addons/l10n_de/data/account_account_tags_data.xml
index f75bb709e8f..fe3212c9e32 100644
--- a/addons/l10n_de/data/account_account_tags_data.xml
+++ b/addons/l10n_de/data/account_account_tags_data.xml
@@ -74,7 +74,7 @@
<record id="tax_report_de_tag_41" model="account.tax.report.line">
<field name="name">41. an Abnehmer mit USt-IdNr (zeile 20)</field>
- <field name="tag_name">11</field>
+ <field name="tag_name">41</field>
<field name="country_id" ref="base.de"/>
<field name="sequence">1</field>
<field name="parent_id" ref="tax_report_de_tag_19"/>
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 @@
<field name="transfer_account_code_prefix">1109</field>
<field name="code_digits">8</field>
<field name="currency_id" ref="base.MNT"/>
+ <field name="use_anglo_saxon" eval="True"/>
</record>
</odoo>
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 70e2ba63ea4..c68f6cc3f7f 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 6bb11d0a090..7cabe960cdb 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 d34334fb772..dd7078ec6f0 100644
--- a/addons/mail/static/tests/chatter_tests.js
+++ b/addons/mail/static/tests/chatter_tests.js
@@ -241,6 +241,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: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="message_attachment_count" string="I\'m here"/>' +
+ '</group>' +
+ '</sheet>' +
+ '</form>',
+ 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: '<form string="Partners">' +
+ '<sheet>' +
+ '<group>' +
+ '<field name="message_attachment_count" string="I\'m here"/>' +
+ '</group>' +
+ '</sheet>' +
+ '<div class="oe_chatter">' +
+ '<field name="message_follower_ids" widget="mail_followers"/>' +
+ '<field name="message_ids" widget="mail_thread"/>' +
+ '<field name="activity_ids" widget="mail_activity"/>' +
+ '</div>' +
+ '</form>',
+ 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 @@
<field name="padding">5</field>
<field name="number_next">1</field>
<field name="number_increment">1</field>
+ <field name="company_id" eval="False"/>
</record>
<record id="sequence_mrp_unbuild" model="ir.sequence">
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index dc27c437219..cf65644076b 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -511,6 +511,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
@@ -518,10 +521,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 2736da29ee1..8e4292a1d06 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 f4bb7746c37..41f8d429517 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 f79252c622f..3481429bec8 100644
--- a/addons/payment/models/payment_acquirer.py
+++ b/addons/payment/models/payment_acquirer.py
@@ -899,7 +899,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 124042b3345..be621264cfb 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -1563,6 +1563,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,
@@ -1626,9 +1627,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;
@@ -1659,6 +1660,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;
},
@@ -1904,6 +1908,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;
@@ -1913,6 +1918,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 75eeb66b355..63efb729576 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -803,8 +803,8 @@
<left><t t-esc='line.product_name_wrapped[0]' /></left>
<right><value t-att-value-decimals='pos.currency.decimals'><t t-esc='line.price_display' /></value></right>
</line>
+ <t t-call="XmlReceiptWrappedProductNameLines"/>
</t>
- <t t-call="XmlReceiptWrappedProductNameLines"/>
<t t-if='!simple'>
<line><left><t t-esc='line.product_name_wrapped[0]' /></left></line>
<t t-call="XmlReceiptWrappedProductNameLines"/>
@@ -824,7 +824,12 @@
<line indent='1'>
<left>
<value t-att-value-decimals='pos.dp["Product Price"]'>
- <t t-esc="line.price_lst" />
+ <t t-if="pos.config.iface_tax_included === 'total'">
+ <t t-esc="line.price_with_tax_before_discount" />
+ </t>
+ <t t-else="">
+ <t t-esc="line.price_lst" />
+ </t>
</value>
</left>
</line>
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 a44d2fcde17..d81ef621411 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 7d463e9b01f..be124faa658 100644
--- a/addons/sale/models/sale.py
+++ b/addons/sale/models/sale.py
@@ -205,6 +205,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 ddb931103dc..4f4c2a35061 100644
--- a/addons/sale_mrp/tests/test_sale_mrp_flow.py
+++ b/addons/sale_mrp/tests/test_sale_mrp_flow.py
@@ -1457,3 +1457,49 @@ 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()
+
+ with self.assertRaises(UserError):
+ 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 fe4ded3bb71..860cf454dd4 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -1071,7 +1071,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, image_base64)
if not image_base64:
image_base64 = 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',
}),
/**
@@ -108,6 +109,33 @@ var AddNewFavoriteMenu = Widget.extend({
//--------------------------------------------------------------------------
/**
+ * 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/list/list_renderer.js b/addons/web/static/src/js/views/list/list_renderer.js
index 4267214d256..23a26bb6d73 100644
--- a/addons/web/static/src/js/views/list/list_renderer.js
+++ b/addons/web/static/src/js/views/list/list_renderer.js
@@ -29,6 +29,7 @@ var FIELD_CLASSES = {
integer: 'o_list_number',
monetary: 'o_list_number',
text: 'o_list_text',
+ many2one: 'o_list_many2one',
};
var ListRenderer = BasicRenderer.extend({
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 = $('<td>')
.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($('<div>', {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/list_view.scss b/addons/web/static/src/scss/list_view.scss
index f8c0afdd499..d7a0384ed22 100644
--- a/addons/web/static/src/scss/list_view.scss
+++ b/addons/web/static/src/scss/list_view.scss
@@ -33,6 +33,10 @@
&.o_list_text {
white-space: pre-wrap;
}
+ &.o_list_many2one {
+ white-space: pre-wrap;
+
+ }
}
tfoot {
@@ -176,7 +180,9 @@
.o_list_record_remove_header, .o_handle_cell {
width: 32px;
}
-
+ &>tbody>tr>td.o_list_many2one {
+ white-space: nowrap;
+ }
// checkboxes in editable lists should be clickable and activate the row
.o_data_row:not(.o_selected_row) .o_data_cell {
.custom-checkbox:not(.o_readonly_modifier) {
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 e34d017b1a4..bb744cbdb03 100644
--- a/addons/web/static/src/xml/base.xml
+++ b/addons/web/static/src/xml/base.xml
@@ -415,7 +415,7 @@
</div>
</div>
-<div t-name="PivotView" class="o_pivot">
+<div t-name="PivotView" t-attf-class="o_pivot #{widget.enableLinking ? 'o_enable_linking' : ''}">
<div class="o_field_selection"/>
</div>
<t t-name="PivotView.buttons">
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: "<search></search>",
+ 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 975f68551fa..ba69596c916 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 1e4b0c3c5b5..544276082a1 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/plugin/bullet.js b/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js
index 6a516dd2ad5..1d5c2d47a33 100644
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js
+++ b/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js
@@ -47,7 +47,7 @@ var BulletPlugin = AbstractPlugin.extend({
res = this._convertList(false, [], start, end, type);
} else {
var ul = this._createList(type);
- res = [].slice.call(ul.children);
+ res = ul ? [].slice.call(ul.children) : [];
}
var startLeaf = this.context.invoke('HelperPlugin.firstLeaf', start.node);
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js b/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js
index 521afe6137a..a025d6a0082 100644
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js
+++ b/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js
@@ -1047,6 +1047,7 @@ var HelperPlugin = AbstractPlugin.extend({
var reAllNBSP = /\u00A0/g;
var reMultipleSpace = /(\s){2,}/;
+ var textContent = range.sc.textContent;
var startBlanks = range.sc.textContent.match(reStartBlanks)[0] || '';
var endBlanks = range.sc.textContent.match(reEndBlanks)[0] || '';
var trimmed = range.sc.textContent.replace(reStartBlanks, '').replace(reEndBlanks, '');
@@ -1060,7 +1061,25 @@ var HelperPlugin = AbstractPlugin.extend({
this._removeInvisibleChar(range);
- range.select();
+ try {
+ range.select();
+ } catch (e) {
+ /* Under certain unknown conditions, the range cannot be
+ applied and triggered an error. In order to allow the user to
+ keep editing, we need to prevent the dialog from showing, hence
+ the use of `console.error`.
+ If this error appears, the carret is moved to the beginning
+ of the focused node.*/
+ console.error(e);
+ console.error('Informations for debugging', [
+ ['textContent', textContent],
+ ['trimmed', trimmed],
+ ['cleanContents', cleanContents],
+ ['start', range.sc.outerHTML || range.sc.textContent, range.so],
+ ['end', range.ec.outerHTML || range.ec.textContent, range.eo],
+ ['parent', range.sc.parentNode && range.sc.parentNode.outerHTML],
+ ]);
+ }
this.context.invoke('editor.saveRange');
},
/**
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/js/editor/wysiwyg_multizone.js b/addons/website/static/src/js/editor/wysiwyg_multizone.js
index 201cbd64d21..64c3e84daed 100644
--- a/addons/website/static/src/js/editor/wysiwyg_multizone.js
+++ b/addons/website/static/src/js/editor/wysiwyg_multizone.js
@@ -239,8 +239,8 @@ var WysiwygMultizone = Wysiwyg.extend({
}
var _super = this._super.bind(this);
return this.savingMutex.lock.then(function () {
- return _super().then(function (_isDirty, html) {
- this._summernote.layoutInfo.editable.html(html);
+ return _super().then(function (res) {
+ this._summernote.layoutInfo.editable.html(res.html);
var $editable = this._getEditableArea();
var $areaDirty = $editable.filter('.o_dirty');
@@ -492,7 +492,7 @@ var WysiwygMultizone = Wysiwyg.extend({
if (!$.contains(ev.data[0], this._focusedNode)) {
this._focusedNode = ev.data[0];
}
- ev.data.closest('.oe_structure > *:not(.o_fake_editable)').addClass('o_fake_editable').attr('contenteditable', true);
+ ev.data.closest('.oe_structure > *:o_editable').addClass('o_fake_editable').attr('contenteditable', true);
},
/**
* @override
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,641 +1,649 @@
+//------------------------------------------------------------------------------
+// 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;
+ #wrapwrap {
+ height: calc(100vh - #{$o-navbar-height});
+ padding-top: 0px;
- .btn-group {
- height: 100%;
- }
+ .btn {
+ -webkit-user-select: none;
+ }
+ }
+}
- .btn {
- height: 100%;
- margin: 0;
- padding: 10px;
- line-height: 1.2;
- font-size: 13px;
- font-family: $o-we-font-family;
+//------------------------------------------------------------------------------
+// Editor components style.
+//------------------------------------------------------------------------------
+
+// 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%;
+ }
- transition: all 0.3s ease 0s;
- border: none;
- border-radius: 0;
+ .btn {
+ height: 100%;
+ margin: 0;
+ padding: 10px;
+ line-height: 1.2;
+ font-size: 13px;
+ font-family: $o-we-font-family;
- .fa {
- margin-right: $grid-gutter-width/4;
+ transition: all 0.3s ease 0s;
+ border: none;
+ border-radius: 0;
- &.fa-times {
- color: $o-we-color-danger;
- }
- }
+ .fa {
+ margin-right: $grid-gutter-width/4;
- &.btn-primary {
- @include button-variant($o-brand-odoo, $o-brand-odoo);
+ &.fa-times {
+ color: $o-we-color-danger;
}
+ }
- &.btn-secondary {
- @include button-variant($o-we-color-dark, $o-we-color-dark);
+ &.btn-primary {
+ @include button-variant($o-brand-odoo, $o-brand-odoo);
+ }
- &:hover {
- background: rgba(black, 0.5);
- }
- }
+ &.btn-secondary {
+ @include button-variant($o-we-color-dark, $o-we-color-dark);
- &:focus,
- &:active,
- &:focus:active {
- outline: none;
+ &:hover {
+ background: rgba(black, 0.5);
}
}
- .dropdown-menu {
- left: auto;
- right: 0;
+ &:focus,
+ &:active,
+ &:focus:active {
+ outline: none;
}
}
- }
- #wrapwrap {
- height: calc(100vh - #{$o-navbar-height});
- padding-top: 0px;
-
- .btn {
- -webkit-user-select: none;
+ .dropdown-menu {
+ left: auto;
+ right: 0;
}
}
+}
- // Translations
- .oe_translate_examples li {
- margin: 10px;
- padding: 4px;
- }
+// Translations
+.oe_translate_examples li {
+ margin: 10px;
+ padding: 4px;
+}
- html[lang]>body.editor_enable [data-oe-translation-state] {
- background: rgba($o-we-content-to-translate-color, 0.5) !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;
- }
+ &[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;
- }
+ &.o_dirty {
+ background: rgba($o-we-translated-content-color, 0.25) !important;
}
+}
- // NOTE EDITOR
+// 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;
+// 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%));
+ 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;
- }
+ #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;
- }
+ #o_scroll {
+ .o_panel_header {
+ color: #999999;
}
}
+}
- .oe_snippet {
- background-color: $o-we-color-normal;
+.oe_snippet {
+ background-color: $o-we-color-normal;
- .oe_snippet_thumbnail {
- .oe_snippet_thumbnail_img {
- border: 1px solid $o-we-color-normal;
- }
+ .oe_snippet_thumbnail {
+ .oe_snippet_thumbnail_img {
+ border: 1px solid $o-we-color-normal;
+ }
- img.oe_snippet_thumbnail_img {}
+ img.oe_snippet_thumbnail_img {}
- .oe_snippet_thumbnail_title {
- border: 1px solid $o-we-color-dark;
- color: $o-we-color-text-light;
- }
+ .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;
- }
+ &: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;
- }
+// 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;
- }
+ .o_colorpicker_section {
+ &:after {
+ content: "";
+ display: table;
+ clear: both;
+ }
- >button {
+ >button {
- &:hover,
- &.selected {
- box-shadow: 0px 0px 2px 2px $o-we-color-light;
- }
+ &:hover,
+ &.selected {
+ box-shadow: 0px 0px 2px 2px $o-we-color-light;
+ }
- &[data-event="foreColor"] {
- background-color: $o-we-color-normal;
+ &[data-event="foreColor"] {
+ background-color: $o-we-color-normal;
- &:before {
- background-color: rgba(white, 0.3);
- }
+ &:before {
+ background-color: rgba(white, 0.3);
}
}
}
+ }
- .note-palette-title {
- color: $o-we-color-text-light;
- }
+ .note-palette-title {
+ color: $o-we-color-text-light;
+ }
- .palette-reset {
- .note-color-reset {
- color: desaturate(rgba($o-we-color-danger, 0.6), 40%);
+ .palette-reset {
+ .note-color-reset {
+ color: desaturate(rgba($o-we-color-danger, 0.6), 40%);
- &:hover {
- color: $o-we-color-danger;
- }
+ &: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;
+// 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;
- }
+ &:after {
+ border-bottom: $o-we-dropzone-border;
+ }
- &.oe_vertical {
- width: $o-we-dropzone-size;
- margin: 0 (-$o-we-dropzone-size/2);
+ &.oe_vertical {
+ width: $o-we-dropzone-size;
+ margin: 0 (-$o-we-dropzone-size/2);
- &:after {
- border-right: $o-we-dropzone-border;
- }
+ &:after {
+ border-right: $o-we-dropzone-border;
}
}
}
+}
- // MANIPULATORS
- #oe_manipulators {
+// MANIPULATORS
+#oe_manipulators {
- // SNIPPET MANIPULATORS
- .oe_overlay {
+ // SNIPPET MANIPULATORS
+ .oe_overlay {
- // OVERLAY OPTIONS
- >.oe_overlay_options {
- font-family: $o-we-font-family;
+ // 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};
+ >.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;
+ 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: lighten($o-we-color-dark, 10%);
+ background-color: darken($o-we-color-danger, 20%);
}
+ }
- &.oe_snippet_remove {
- background-color: $o-we-color-danger;
- border-color: darken($o-we-color-danger, 20%);
+ >.fa {
+ color: $o-we-color-text-light;
+ }
+ }
- &:hover {
- background-color: darken($o-we-color-danger, 20%);
- }
+ // 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%);
}
- >.fa {
- color: $o-we-color-text-light;
+ &:before,
+ &:after {
+ background-color: $o-we-color-text-light;
}
}
- // CUSTOMIZE MENU BUTTON
- >.oe_options {
- .btn {
- background-color: $o-we-color-dark;
+ // 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,
- &:active,
- &:focus {
- background-color: lighten($o-we-color-dark, 5%);
- border-color: lighten($o-we-color-dark, 10%);
+ &:hover {
+ background-color: fade-out($o-we-color-dark, 0.5);
}
- &:before,
- &:after {
- background-color: $o-we-color-text-light;
+ &.active {
+ background-color: fade-out($o-we-color-dark, 0.5);
}
}
- // 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-header {
+ color: wheat;
- .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 {
+ &.o_parent_editor_header {
color: wheat;
-
- &.o_parent_editor_header {
- color: wheat;
- }
}
+ }
- .dropdown-submenu {
- &::before {
- border-left-color: $o-we-color-text-light;
- }
+ .dropdown-submenu {
+ &::before {
+ border-left-color: $o-we-color-text-light;
}
}
}
}
}
+ }
- &.o_top_cover>.oe_overlay_options {
- top: $o-we-handle-border-width;
- right: $o-we-handle-border-width;
- }
-
+ &.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);
- }
+// Animations
+@keyframes fadeInDownSmall {
+ 0% {
+ opacity: 0;
+ transform: translate(0, -5px);
+ }
- 100% {
- opacity: 1;
- transform: translate(0, 0);
- }
+ 100% {
+ opacity: 1;
+ transform: translate(0, 0);
}
+}
- @keyframes fadeInOut {
- 0% {
- opacity: 0;
- }
+@keyframes fadeInOut {
+ 0% {
+ opacity: 0;
+ }
- 25% {
- opacity: 1;
- }
+ 25% {
+ opacity: 1;
+ }
- 75% {
- opacity: 1;
- }
+ 75% {
+ opacity: 1;
+ }
- 100% {
- opacity: 0;
- }
+ 100% {
+ opacity: 0;
}
+}
- @keyframes inputHighlighter {
- from {
- background: $o-brand-primary;
- }
+@keyframes inputHighlighter {
+ from {
+ background: $o-brand-primary;
+ }
- to {
- width: 0;
- background: transparent;
- }
+ to {
+ width: 0;
+ background: transparent;
}
+}
- // MODALS
- body .modal {
+// MODALS
+body .modal {
- // BACKGROUND IMAGE OPTIONS
- .o_bg_img_opt_modal .o_bg_img_opt {
- margin: 15px 0;
+ // BACKGROUND IMAGE OPTIONS
+ .o_bg_img_opt_modal .o_bg_img_opt {
+ margin: 15px 0;
- .help-control {
- @include o-position-absolute(15px, 15px);
- }
+ .help-control {
+ @include o-position-absolute(15px, 15px);
+ }
- .help {
- margin: 15px 0;
+ .help {
+ margin: 15px 0;
- .simulator {
- position: relative;
- float: left;
- margin-right: 15px;
- width: 120px;
- height: 100px;
+ .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, 0, 0, 0);
- border: 1px dotted #888787;
- background-image: url(/web/image);
- background-position: center center;
- background-size: cover;
+ @include o-position-absolute(0, 12px, 20px, 12px);
}
.el {
- @include o-position-absolute(10px, 0, 10px, 0);
- border: 1px solid #222;
+ @include o-position-absolute(0, 0, 20px, 0);
}
+ }
+ }
- &.contain_bg {
- .bg {
- @include o-position-absolute(0, 12px, 20px, 12px);
- }
+ +* {
+ clear: left;
+ }
+ }
- .el {
- @include o-position-absolute(0, 0, 20px, 0);
- }
- }
- }
+ .o_bg_img_opt_cover_edition {
+ @include o-we-preview-box;
+ padding: 0 25px 25px 25px;
- +* {
- clear: left;
- }
+ h6 {
+ color: white;
+ font-weight: bold;
}
- .o_bg_img_opt_cover_edition {
- @include o-we-preview-box;
- padding: 0 25px 25px 25px;
+ .o_bg_img_opt_object {
+ position: relative;
+ background: white;
+ min-height: 10px;
+ @include o-we-preview-content;
- h6 {
- color: white;
- font-weight: bold;
+ >img {
+ cursor: crosshair;
+ border-top: 1px solid #5A5A5A;
+ border-bottom: 1px solid #111;
}
- .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;
+ }
+ }
- &: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_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;
+ .o_x {
+ margin-right: 10px
+ }
+ }
- span {
- font-weight: normal;
- }
+ .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;
- .o_x {
- margin-right: 10px
- }
+ &.grid-2 {
+ top: 66.66%
}
- .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-3 {
- top: 0;
- left: 33.33%;
- width: 1px;
- height: 100%
- }
+ &.grid-4 {
+ top: 0;
+ left: 66.66%;
+ 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;
}
- .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;
+ &:before {
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;
- }
+ 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;
+.wysiwyg_multizone.note-editor {
+ &>.note-toolbar-wrapper .note-toolbar {
+ height: $o-navbar-height;
- .btn {
- height: $o-navbar-height !important; // force this because of themes
- }
+ .btn {
+ height: $o-navbar-height !important; // force this because of themes
}
+ }
- &>.note-statusbar {
- display: none;
- }
+ &>.note-statusbar {
+ display: none;
+ }
- &>.note-editing-area {
- overflow: visible;
- }
+ &>.note-editing-area {
+ overflow: visible;
}
+}
- .wysiwyg_multizone.note-popover.popover {
- border-width: 0 1px;
+.wysiwyg_multizone.note-popover.popover {
+ border-width: 0 1px;
- .popover-content .btn {
- height: $o-navbar-height;
- }
+ .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
+.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;
- .btn {
- background-color: $o-we-color-dark !important;
+ >.fa {
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;
- &:hover {
+ >.fa {
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;
+ &.active {
+ background-color: $o-we-color-dark !important;
+ color: $o-we-color-text-light !important;
- >.fa {
- color: $o-we-color-text-light !important;
- }
+ >.fa {
+ color: $o-we-color-text-light !important;
}
}
+ }
- >.btn-group {
- margin-top: 0 !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;
+ &.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;
- }
+ >.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-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;
+ .dropdown-item {
+ width: 100% !important;
+ color: $o-we-color-text-light !important;
+
+ >.fa {
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;
- &:hover {
- background-color: $o-we-tab-active-bg !important;
+ >.fa {
color: white !important;
-
- >.fa {
- color: white !important;
- }
}
}
}
+ }
- .note-para .dropdown-menu {
- min-width: 147px !important;
- }
+ .note-para .dropdown-menu {
+ min-width: 147px !important;
+ }
- .note-style .dropdown-item * {
- color: $o-we-color-text-light !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 @@
<template id="iframe_css_assets_edit" name="CSS assets for wysiwyg iframe content for popup">
<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="web.assets_frontend" t-js="false"/>
+ <t t-call-assets="web_editor.assets_wysiwyg" t-js="false"/>
+ <t t-call-assets="website.assets_editor" t-js="false"/>
<link rel="stylesheet" href="/website_mass_mailing/static/src/css/website_mass_mailing_popup.css" type="text/css"/>
</template>
diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py
index b5083e3f7c6..61fad2a5393 100644
--- a/addons/website_sale/controllers/main.py
+++ b/addons/website_sale/controllers/main.py
@@ -627,9 +627,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..f701374b350 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
@@ -63,8 +65,10 @@ var ProductComparison = publicWidget.Widget.extend(VariantMixin, {
});
$(document.body).on('click.product_comparaison_widget', '.o_comparelist_remove', function (ev) {
self._removeFromComparelist(ev);
- var new_link = '/shop/compare/?products=' + self.comparelist_product_ids.toString();
- window.location.href = _.isEmpty(self.comparelist_product_ids) ? '/shop' : new_link;
+ self.guard.exec(function() {
+ var new_link = '/shop/compare/?products=' + self.comparelist_product_ids.toString();
+ window.location.href = _.isEmpty(self.comparelist_product_ids) ? '/shop' : new_link;
+ });
});
return this._super.apply(this, arguments);
@@ -154,6 +158,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 +168,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,8 +197,12 @@ var ProductComparison = publicWidget.Widget.extend(VariantMixin, {
* @private
*/
_removeFromComparelist: 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.guard.exec(this._removeFromComparelistImpl.bind(this, e));
+ },
+ _removeFromComparelistImpl: function (e) {
+ var target = $(e.target.closest('.o_comparelist_remove, .o_remove'));
+ this.comparelist_product_ids = _.without(this.comparelist_product_ids, target.data('product_product_id'));
+ target.parents('.o_product_row').remove();
this._updateCookie();
$('.o_comparelist_limit_warning').hide();
this._updateContent('show');
diff --git a/addons/website_slides/controllers/main.py b/addons/website_slides/controllers/main.py
index 2eada6aee13..a2e56835b2f 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/<model("slide.slide", "[('website_id', 'in', (False, current_website_id))]"):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 22029420ec5..1a4e7ac6e1c 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,49 +49,13 @@ 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 @@
</page>
<page string="Document">
<group>
- <field name="slide_type" readonly="1"/>
+ <field name="slide_type"/>
<field name="url"
- attrs="{'required': [('slide_type', 'in', ('video'))], 'readonly': [('slide_type', 'in', ('document', 'video'))], 'invisible': [('slide_type', 'not in', ('document', 'presentation', 'video'))]}" />
+ attrs="{'required': [('slide_type', 'in', ('video'))], 'invisible': [('slide_type', 'not in', ('document', 'presentation', 'video'))]}" />
<field name="document_id" readonly="1" attrs="{'invisible': [('document_id', '=', False)]}"/>
<field name="mime_type" readonly="1"
attrs="{'invisible': [('slide_type', 'not in', ('document', 'presentation', 'infographic', 'webpage'))]}"/>
- <field name="datas" readonly="1"
+ <field name="datas"
attrs="{'invisible': [('slide_type', 'not in', ('document', 'presentation', 'infographic', 'webpage'))]}"/>
</group>
</page>
@@ -93,6 +93,7 @@
<field name="slide_views"/>
<field name="public_views"/>
<field name="total_views"/>
+ <field name="completion_time" widget='float_time'/>
</group>
</group>
<label for="index_content"/>
diff --git a/addons/website_slides/views/website_slides_templates_course.xml b/addons/website_slides/views/website_slides_templates_course.xml
index 8aeb4df9749..0ea39c59f84 100644
--- a/addons/website_slides/views/website_slides_templates_course.xml
+++ b/addons/website_slides/views/website_slides_templates_course.xml
@@ -7,30 +7,34 @@
<div class="container">
<div class="row align-items-center justify-content-between">
<!-- Desktop Mode -->
- <nav aria-label="breadcrumb" class="col d-none d-md-flex">
+ <nav aria-label="breadcrumb" class="col-md-8 d-none d-md-flex">
<ol class="breadcrumb bg-transparent mb-0 pl-0 py-0">
<li class="breadcrumb-item">
<a href="/slides">Home</a>
</li>
- <li t-att-class="'breadcrumb-item %s' % ('active' if not search_category and not search_tag and not search_slide_type else '')">
+ <t t-set="breadcrumb_class" t-value="'breadcrumb-item %s' % ('active' if not slide else '')" />
+ <li t-att-class="'breadcrumb-item %s' % ('active' if not search_category and not search_tag and not search_slide_type and not slide else '')">
<a t-att-href="'/slides/%s' % slug(channel)"><span t-esc="channel.name"/></a>
</li>
- <li class="breadcrumb-item active" t-att-aria-current="'page' and search_category" t-if="search_category">
+ <li t-att-class="breadcrumb_class" t-att-aria-current="'page' and search_category" t-if="search_category">
<a t-att-href="'/slides/%s/category/%s' % (slug(channel), slug(search_category))"><span t-esc="search_category.name"/></a>
</li>
- <li class="breadcrumb-item active" t-att-aria-current="'page' and search_tag" t-if="search_tag">
+ <li t-att-class="breadcrumb_class" t-att-aria-current="'page' and search_tag" t-if="search_tag">
<a t-att-href="'/slides/%s/tag/%s' % (slug(channel), slug(search_tag))"><span t-esc="search_tag.name"/></a>
</li>
- <li class="breadcrumb-item active" t-att-aria-current="'page' and search_uncategorized" t-if="search_uncategorized">
+ <li t-att-class="breadcrumb_class" t-att-aria-current="'page' and search_uncategorized" t-if="search_uncategorized">
<a t-att-href="'/slides/%s?search_uncategorized=1' % (slug(channel))">Uncategorized</a>
</li>
- <li class="breadcrumb-item active" t-att-aria-current="'page' and search_slide_type" t-if="search_slide_type">
+ <li t-att-class="breadcrumb_class" t-att-aria-current="'page' and search_slide_type" t-if="search_slide_type">
<a t-att-href="'/slides/%s?slide_type=%s' % (slug(channel), search_slide_type)"><span t-esc="slide_types[search_slide_type]"/></a>
</li>
+ <li t-if="slide" class="breadcrumb-item active">
+ <a t-att-href="'/slides/slide/%s' % slug(slide)"><span t-esc="slide.name"/></a>
+ </li>
</ol>
</nav>
- <div class="col d-none d-md-flex flex-row align-items-center justify-content-end">
+ <div class="col-md-4 d-none d-md-flex flex-row align-items-center justify-content-end">
<!-- search -->
<form t-attf-action="/slides/all" role="search" method="get">
<div class="input-group o_wslides_course_nav_search ml-1 position-relative">
@@ -52,21 +56,25 @@
<div class="dropdown-menu">
<a class="dropdown-item" href="/slides">Home</a>
+ <t t-set="dropdown_class" t-value="'dropdown-item %s' % ('active' if not slide else '')"/>
<a t-att-class="'dropdown-item %s' % ('active' if not search_category and not search_tag and not search_slide_type else '')" t-att-href="'/slides/%s' % slug(channel)">
&#9492;<span class="ml-1" t-esc="channel.name"/>
</a>
- <a class="dropdown-item active" t-att-aria-current="'page' and search_category" t-if="search_category" t-att-href="'/slides/%s/category/%s' % (slug(channel), slug(search_category))">
+ <a t-att-class="dropdown_class" t-att-aria-current="'page' and search_category" t-if="search_category" t-att-href="'/slides/%s/category/%s' % (slug(channel), slug(search_category))">
&#9492;<span class="ml-1" t-esc="search_category.name"/>
</a>
- <a class="dropdown-item active" t-att-aria-current="'page' and search_tag" t-if="search_tag" t-att-href="'/slides/%s/tag/%s' % (slug(channel), slug(search_tag))">
+ <a t-att-class="dropdown_class" t-att-aria-current="'page' and search_tag" t-if="search_tag" t-att-href="'/slides/%s/tag/%s' % (slug(channel), slug(search_tag))">
&#9492;<span class="ml-1" t-esc="search_tag.name"/>
</a>
- <a class="dropdown-item active" t-att-aria-current="'page' and search_uncategorized" t-if="search_uncategorized" t-att-href="'/slides/%s?search_uncategorized=1' % (slug(channel))">
+ <a t-att-class="dropdown_class" t-att-aria-current="'page' and search_uncategorized" t-if="search_uncategorized" t-att-href="'/slides/%s?search_uncategorized=1' % (slug(channel))">
&#9492;<span class="ml-1">Uncategorized</span>
</a>
- <a class="dropdown-item active" t-att-aria-current="'page' and search_slide_type" t-if="search_slide_type" t-att-href="'/slides/%s?slide_type=%s' % (slug(channel), search_slide_type)">
+ <a t-att-class="dropdown_class" t-att-aria-current="'page' and search_slide_type" t-if="search_slide_type" t-att-href="'/slides/%s?slide_type=%s' % (slug(channel), search_slide_type)">
&#9492;<span class="ml-1" t-esc="slide_types[search_slide_type]"/>
</a>
+ <a t-if="slide" class="dropdown-item active" t-att-href="'/slides/slide/%s' % (slug(slide))">
+ &#9492;<span class="ml-1" t-esc="slide.name"/>
+ </a>
</div>
</div>
@@ -519,7 +527,7 @@
<t t-set="image_url" t-value="'/web/image/slide.slide/%s/image' % slide_promoted.id"/>
<a t-if="slide_promoted.is_preview or channel.is_member or is_slides_publisher"
- t-attf-href="/slides/slide/#{slug(slide_promoted)}" class="w-100 w-lg-50 flex-shrink-0 rounded">
+ t-attf-href="/slides/slide/#{slug(slide_promoted)}#{query_string}" class="w-100 w-lg-50 flex-shrink-0 rounded">
<div t-attf-style="background-image:url(#{image_url}); background-size: cover; background-position:center; padding-bottom: 35%;" class="h-lg-100"/>
</a>
<div t-else="" class="w-100 w-lg-50 flex-shrink-0 rounded">
@@ -528,7 +536,7 @@
<div class="card-body">
<a t-if="slide_promoted.is_preview or channel.is_member or is_slides_publisher"
- t-attf-href="/slides/slide/#{slug(slide_promoted)}"
+ t-attf-href="/slides/slide/#{slug(slide_promoted)}#{query_string}"
class="h4 d-block" t-att-title="slide_promoted.name" t-field="slide_promoted.name"/>
<h4 t-else="" class="text-muted" t-field="slide_promoted.name"/>
@@ -597,20 +605,20 @@
<t t-if="slide.image">
<t t-set="lesson_image" t-value="'/web/image/slide.slide/%s/image' % slide.id"/>
- <a t-if="can_access" t-attf-href="/slides/slide/#{slug(slide)}" t-title="slide.name">
+ <a t-if="can_access" t-attf-href="/slides/slide/#{slug(slide)}#{query_string}" t-title="slide.name">
<div class="card-img-top border-bottom" t-attf-style="padding-top: 50%; background-image: url(#{lesson_image}); background-size: cover; background-position:center"/>
</a>
<div t-else="" class="card-img-top border-bottom" t-attf-style="padding-top: 50%; background-image: url(#{lesson_image}); background-size: cover; background-position:center"/>
</t>
<t t-else="">
- <a t-if="can_access" t-attf-href="/slides/slide/#{slug(slide)}" t-title="slide.name">
+ <a t-if="can_access" t-attf-href="/slides/slide/#{slug(slide)}#{query_string}" t-title="slide.name">
<div class="card-img-top border-bottom o_wslides_gradient" t-attf-style="padding-top: 50%;"/>
</a>
<div t-else="" class="card-img-top border-bottom o_wslides_gradient" t-attf-style="padding-top: 50%;"/>
</t>
<div class="card-body p-3">
- <a t-if="can_access" class="card-title h5 mb-2o_wslides_desc_truncate_2" t-attf-href="/slides/slide/#{slug(slide)}" t-esc="slide.name"/>
+ <a t-if="can_access" class="card-title h5 mb-2o_wslides_desc_truncate_2" t-attf-href="/slides/slide/#{slug(slide)}#{query_string}" t-esc="slide.name"/>
<span t-else="" class="card-title h5 mb-2 o_wslides_desc_truncate_2 text-muted" t-esc="slide.name"/>
<div class="card-subtitle mb-2 text-muted" t-if="slide.is_preview or (not slide.is_published and user.has_group('website.group_website_publisher'))">
<t t-if="slide.is_preview">
@@ -624,7 +632,7 @@
<div class="o_wslides_desc_truncate_3 font-weight-light oe_no_empty" t-field="slide.description"/>
<div t-if="slide.tag_ids" class="mt-2 pt-1 o_wslides_desc_truncate_2">
<t t-foreach="slide.tag_ids" t-as="tag">
- <a t-att-href="'/slides/all?channel_tag_id=%s' % tag.id" class="badge badge-light" t-esc="tag.name"/>
+ <a t-att-href="'/slides/%s/tag/%s' % (slug(slide.channel_id), slug(tag))" class="badge badge-light" t-esc="tag.name"/>
</t>
</div>
</div>
diff --git a/addons/website_slides/views/website_slides_templates_homepage.xml b/addons/website_slides/views/website_slides_templates_homepage.xml
index 1cf83c02b13..7141ea25299 100644
--- a/addons/website_slides/views/website_slides_templates_homepage.xml
+++ b/addons/website_slides/views/website_slides_templates_homepage.xml
@@ -178,7 +178,7 @@
<template id='courses_all' name="Odoo All Courses">
<t t-set="body_classname" t-value="'o_wslides_body'"/>
<t t-call="website.layout">
- <div class="wrap">
+ <div id="wrap" class="wrap o_wslides_wrap">
<section class="s_banner bg-900" style="background-image: url(&quot;/website_slides/static/src/img/banner_default_all.svg&quot;); background-size: cover; background-position: 80% 20%">
<div class="container py-5">
<h1 t-if="search_my" class="display-3 mb-0">My Courses</h1>
diff --git a/addons/website_slides/views/website_slides_templates_lesson.xml b/addons/website_slides/views/website_slides_templates_lesson.xml
index 0853c8dccb9..77e154b1ccc 100644
--- a/addons/website_slides/views/website_slides_templates_lesson.xml
+++ b/addons/website_slides/views/website_slides_templates_lesson.xml
@@ -185,7 +185,7 @@
<div class="row align-items-center my-3">
<div class="col-12 col-md order-2 order-md-1 d-flex">
<div class="d-flex align-items-center">
- <h1 class="h5 my-0">
+ <h1 class="h4 my-0">
<t t-call="website_slides.slide_icon">
<t t-set="icon_class">mr-1</t>
</t>
@@ -245,36 +245,7 @@
<div t-field="slide.html_content"/>
</div>
</div>
- <div class="row" t-if="slide.question_ids">
- <t t-call="website_slides.lesson_content_quiz"/>
- </div>
- <div class="row mt-3 mb-3">
- <div class="col-12 col-md d-flex align-items-start mb-4 mb-md-0" t-if="len(slide.link_ids)">
- <span t-if="slide.link_ids" class="text-muted font-weight-bold mr-3">External sources</span>
- <div class="text-muted mr-auto border-left pl-3">
- <t t-foreach="slide.link_ids" t-as="link">
- <a t-att-href="link.link" t-esc="link.name"/><br />
- </t>
- </div>
- </div>
- <div t-if="slide.channel_id.allow_comment and slide.channel_id.channel_type == 'documentation'"
- class="col-12 col-md d-flex align-items-start justify-content-md-end mb-2 mb-md-0">
- <span class="text-muted font-weight-bold mr-3">Rating</span>
- <div class="text-muted border-left pl-3">
- <div class="o_wslides_js_slide_like mr-2">
- <span t-att-class="('o_wslides_js_slide_like_up %s') % ('disabled' if not slide.channel_id.can_vote else '')" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
- <i class="fa fa-thumbs-up fa-1x" role="img" aria-label="Likes" title="Likes"></i>
- <span t-esc="slide.likes"/>
- </span>
- <span t-att-class="('o_wslides_js_slide_like_down ml-3 %s') % ('disabled' if not slide.channel_id.can_vote else '')" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
- <i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"></i>
- <span t-esc="slide.dislikes"/>
- </span>
- </div>
- </div>
- </div>
- </div>
- <div class="mb-5">
+ <div class="my-3">
<ul class="nav nav-tabs o_wslides_lesson_nav" role="tablist">
<li class="nav-item">
<a href="#about" aria-controls="about" class="nav-link active" role="tab" data-toggle="tab">
@@ -362,27 +333,6 @@
</tbody>
</table>
</div>
- <div class="col-12 col-md">
- <table class="table table-sm">
- <tbody>
- <tr>
- <th colspan="2" class="border-top-0">Share count</th>
- </tr>
- <tr class="border-top-0">
- <th class="border-top-0"><span id="facebook-badge">0</span></th>
- <td class="border-top-0 w-100">Facebook</td>
- </tr>
- <tr>
- <th><span id="twitter-badge">0</span></th>
- <td>Twitter</td>
- </tr>
- <tr>
- <th><span id="linkedin-badge"/></th>
- <td>LinkedIn</td>
- </tr>
- </tbody>
- </table>
- </div>
</div>
</div>
<div role="tabpanel" class="tab-pane fade" t-if="slide.website_published" id="share">
@@ -409,6 +359,35 @@
</div>
</div>
</div>
+ <div class="row" t-if="slide.question_ids">
+ <t t-call="website_slides.lesson_content_quiz"/>
+ </div>
+ <div class="row mt-3 mb-3">
+ <div class="col-12 col-md d-flex align-items-start mb-4 mb-md-0" t-if="len(slide.link_ids)">
+ <span t-if="slide.link_ids" class="text-muted font-weight-bold mr-3">External sources</span>
+ <div class="text-muted mr-auto border-left pl-3">
+ <t t-foreach="slide.link_ids" t-as="link">
+ <a t-att-href="link.link" t-esc="link.name"/><br />
+ </t>
+ </div>
+ </div>
+ <div t-if="slide.channel_id.allow_comment and slide.channel_id.channel_type == 'documentation'"
+ class="col-12 col-md d-flex align-items-start justify-content-md-end mb-2 mb-md-0">
+ <span class="text-muted font-weight-bold mr-3">Rating</span>
+ <div class="text-muted border-left pl-3">
+ <div class="o_wslides_js_slide_like mr-2">
+ <span t-att-class="('o_wslides_js_slide_like_up %s') % ('disabled' if not slide.channel_id.can_vote else '')" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
+ <i class="fa fa-thumbs-up fa-1x" role="img" aria-label="Likes" title="Likes"></i>
+ <span t-esc="slide.likes"/>
+ </span>
+ <span t-att-class="('o_wslides_js_slide_like_down ml-3 %s') % ('disabled' if not slide.channel_id.can_vote else '')" tabindex="0" data-toggle="popover" t-att-data-slide-id="slide.id">
+ <i class="fa fa-thumbs-down fa-1x" role="img" aria-label="Dislikes" title="Dislikes"></i>
+ <span t-esc="slide.dislikes"/>
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
</template>
<!-- Slide sub-tempalte: render a quiz serverside. Should be sync with JS qweb template "slide.slide.quiz" -->
@@ -427,7 +406,7 @@
t-att-data-has-next="1 if next_slide else 0"
t-att-data-next-slide-url="'/slides/slide/%s' % (slug(next_slide)) if next_slide else None">
<div t-foreach="slide_questions" t-as="question"
- t-att-class="'o_wslides_js_lesson_quiz_question mt-3 mb-4 %s' % ('completed-disabled' if slide_completed else ('disabled' if not slide.channel_id.is_member else ''))" t-att-data-question-id="question['id']" t-att-data-title="question['question']">
+ t-att-class="'o_wslides_js_lesson_quiz_question my-5 %s' % ('completed-disabled' if slide_completed else ('disabled' if not slide.channel_id.is_member else ''))" t-att-data-question-id="question['id']" t-att-data-title="question['question']">
<div class="h4">
<small class="text-muted"><span t-esc="question_index+1"/>. </small> <span t-esc="question['question']"/>
</div>
diff --git a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml
index 5180385f117..7022de0df99 100644
--- a/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml
+++ b/addons/website_slides/views/website_slides_templates_lesson_fullscreen.xml
@@ -3,15 +3,18 @@
<!-- Slide template for the fullscreen mode -->
<template id="slide_fullscreen" name="Fullscreen">
+ <t t-set="head">
+ <link rel="canonical" t-att-href="slide.website_url" />
+ </t>
<t t-call="website.layout">
<div class="o_wslides_fs_main d-flex flex-column font-weight-light">
<div class="o_wslides_slide_fs_header d-flex flex-shrink-0 text-white">
<div class="d-flex">
- <a class="o_wslides_fs_toggle_sidebar d-flex align-items-center px-3 active" href="#" title="Lessons">
+ <a class="o_wslides_fs_toggle_sidebar d-flex align-items-center px-3" href="#" title="Lessons">
<i class="fa fa-bars"/><span class="d-none d-md-inline-block ml-1">Lessons</span>
</a>
- <a class="o_wslides_fs_review d-flex align-items-center px-3" t-att-href="slide.channel_id.website_url" title="Reviews" t-if="slide.channel_id.allow_comment">
+ <a class="o_wslides_fs_review d-flex align-items-center px-3" t-att-href="slide.channel_id.website_url + '?active_tab=review'" title="Reviews" t-if="slide.channel_id.allow_comment">
<i class="fa fa-pencil"/><span class="d-none d-md-inline-block ml-1">Write a review</span>
</a>
</div>
@@ -20,7 +23,7 @@
<i class="fa fa-sign-out d-md-none"/><span class="d-none d-md-inline-block ml-1">Exit Fullscreen</span>
</a>
<a class="d-flex align-items-center px-3" t-attf-href="/slides/#{slug(slide.channel_id)}">
- <i class="fa fa-sign-out d-md-none"/><span class="d-none d-md-inline-block ml-1">Back to course</span>
+ <i class="fa fa-home d-md-none"/><span class="d-none d-md-inline-block ml-1">Back to course</span>
</a>
</div>
</div>
@@ -28,7 +31,7 @@
<div class="o_wslides_fs_container d-flex position-relative overflow-hidden flex-grow-1">
<div class="o_wslides_fs_content align-items-stretch justify-content-center d-flex flex-grow-1 order-2"></div>
- <div class="o_wslides_fs_sidebar text-white flex-shrink-0 order-1">
+ <div class="o_wslides_fs_sidebar o_wslides_fs_sidebar_hidden text-white flex-shrink-0 order-1">
<div class="o_wslides_fs_sidebar_content d-flex flex-column px-3 pt-3 h-100">
<div class="o_wslides_fs_sidebar_header mb-3">
<a class="h5 d-block mb-1" t-attf-href="/slides/#{slug(slide.channel_id)}">
diff --git a/addons/website_slides/views/website_slides_templates_profile.xml b/addons/website_slides/views/website_slides_templates_profile.xml
index 262c95a7717..212b0ad9993 100644
--- a/addons/website_slides/views/website_slides_templates_profile.xml
+++ b/addons/website_slides/views/website_slides_templates_profile.xml
@@ -51,12 +51,6 @@
<t t-foreach="course.channel_id.tag_ids" t-as="tag">
<a t-att-href="'/slides/all?channel_tag_id=%s' % tag.id" class="badge badge-light" t-esc="tag.name"/>
</t>
- <t t-foreach="course.channel_id.tag_ids" t-as="tag">
- <a t-att-href="'/slides/all?channel_tag_id=%s' % tag.id" class="badge badge-light" t-esc="tag.name"/>
- </t>
- <t t-foreach="course.channel_id.tag_ids" t-as="tag">
- <a t-att-href="'/slides/all?channel_tag_id=%s' % tag.id" class="badge badge-light" t-esc="tag.name"/>
- </t>
</div>
<div class="d-flex align-items-center">
diff --git a/doc/cla/corporate/bumanit.md b/doc/cla/corporate/bumanit.md
new file mode 100644
index 00000000000..b05d68242ad
--- /dev/null
+++ b/doc/cla/corporate/bumanit.md
@@ -0,0 +1,15 @@
+Mongolia, 2019-06-14
+
+BumanIT agrees to the terms of the Odoo Corporate Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Baskhuu Lodoikhuu baskhuujacara@gmail.com https://github.com/jacara
+
+List of contributors:
+
+Baskhuu Lodoikhuu baskhuujacara@gmail.com https://github.com/jacara
diff --git a/doc/cla/individual/jakubhruby.md b/doc/cla/individual/jakubhruby.md
new file mode 100644
index 00000000000..46d2ff703d4
--- /dev/null
+++ b/doc/cla/individual/jakubhruby.md
@@ -0,0 +1,11 @@
+Czech Republic, 2019-06-03
+
+I hereby agree to the terms of the Odoo Individual Contributor License
+Agreement v1.0.
+
+I declare that I am authorized and able to make this agreement and sign this
+declaration.
+
+Signed,
+
+Jakub Hrubý jakub@jhruby.cz https://github.com/jakubhruby
diff --git a/odoo/addons/base/data/res.country.state.csv b/odoo/addons/base/data/res.country.state.csv
index 9a5535bf462..515e846ffdb 100644
--- a/odoo/addons/base/data/res.country.state.csv
+++ b/odoo/addons/base/data/res.country.state.csv
@@ -678,3 +678,33 @@ state_co_30,co,"Guainía","GUA"
state_co_31,co,"Guaviare","GUV"
state_co_32,co,"Vaupés","VAU"
state_co_33,co,"Vichada","VID"
+state_mn_01,mn,"Архангай","01"
+state_mn_02,mn,"Баян-Өлгий","02"
+state_mn_03,mn,"Баянхонгор","03"
+state_mn_04,mn,"Булган","04"
+state_mn_05,mn,"Говь-Алтай","05"
+state_mn_06,mn,"Дорноговь","06"
+state_mn_07,mn,"Дорнод","07"
+state_mn_08,mn,"Дундговь","08"
+state_mn_09,mn,"Завхан","09"
+state_mn_10,mn,"Өвөрхангай","10"
+state_mn_11,mn,"Өмнөговь","11"
+state_mn_12,mn,"Сүхбаатар","12"
+state_mn_13,mn,"Сэлэнгэ","13"
+state_mn_14,mn,"Төв","14"
+state_mn_15,mn,"Увс","15"
+state_mn_16,mn,"Ховд","16"
+state_mn_17,mn,"Хөвсгөл","17"
+state_mn_18,mn,"Хэнтий","18"
+state_mn_19,mn,"Дархан-Уул","19"
+state_mn_20,mn,"Орхон","20"
+state_mn_23,mn,"УБ - Хан Уул","23"
+state_mn_24,mn,"УБ - Баянзүрх","24"
+state_mn_25,mn,"УБ - Сүхбаатар","25"
+state_mn_26,mn,"УБ - Баянгол","26"
+state_mn_27,mn,"УБ - Багануур","27"
+state_mn_28,mn,"УБ - Багахангай","28"
+state_mn_29,mn,"УБ - Налайх","29"
+state_mn_32,mn,"Говьсүмбэр","32"
+state_mn_34,mn,"УБ - Сонгино Хайрхан","34"
+state_mn_35,mn,"УБ - Чингэлтэй","35"
diff --git a/odoo/addons/base/data/res_currency_data.xml b/odoo/addons/base/data/res_currency_data.xml
index d705c4cbe18..39e77487d3a 100644
--- a/odoo/addons/base/data/res_currency_data.xml
+++ b/odoo/addons/base/data/res_currency_data.xml
@@ -222,6 +222,7 @@
<field name="symbol">¥</field>
<field name="rounding">1.00</field>
<field name="active" eval="False"/>
+ <field name="position">before</field>
<field name="currency_unit_label">Yen</field>
<field name="currency_subunit_label">Cen</field>
</record>
diff --git a/odoo/addons/base/models/ir_actions.py b/odoo/addons/base/models/ir_actions.py
index f25d4080466..c2d7858cc15 100644
--- a/odoo/addons/base/models/ir_actions.py
+++ b/odoo/addons/base/models/ir_actions.py
@@ -247,7 +247,10 @@ class IrActionsActWindow(models.Model):
existing = self.filtered(lambda rec: rec.id in ids)
if len(existing) < len(self):
# mark missing records in cache with a failed value
- exc = MissingError(_("Record does not exist or has been deleted."))
+ exc = MissingError(
+ _("Record does not exist or has been deleted.")
+ + '\n\n({} {}, {} {})'.format(_('Records:'), (self - existing).ids[:6], _('User:'), self._uid)
+ )
for record in (self - existing):
record._cache.set_failed(self._fields, exc)
return existing
diff --git a/odoo/addons/base/models/ir_http.py b/odoo/addons/base/models/ir_http.py
index c69c3c6a12d..25ea6fd0835 100644
--- a/odoo/addons/base/models/ir_http.py
+++ b/odoo/addons/base/models/ir_http.py
@@ -376,9 +376,11 @@ class IrHttp(models.AbstractModel):
headers = [('Content-Type', mimetype), ('X-Content-Type-Options', 'nosniff')]
# cache
etag = bool(request) and request.httprequest.headers.get('If-None-Match')
- status = status or (304 if filehash and etag == filehash else 200)
+ status = status or 200
if filehash:
headers.append(('ETag', filehash))
+ if etag == filehash and status == 200:
+ status = 304
headers.append(('Cache-Control', 'max-age=%s' % (STATIC_CACHE if unique else 0)))
# content-disposition default name
if download:
@@ -421,11 +423,11 @@ class IrHttp(models.AbstractModel):
status, content, filename, mimetype, filehash = self._binary_ir_attachment_redirect_content(record, default_mimetype=default_mimetype)
if not content:
status, content, filename, mimetype, filehash = self._binary_record_content(
- record, field=field, filename=None, filename_field=filename_field,
+ record, field=field, filename=filename, filename_field=filename_field,
default_mimetype='application/octet-stream')
status, headers, content = self._binary_set_headers(
- status, content, filename, mimetype, unique, filehash=False, download=download)
+ status, content, filename, mimetype, unique, filehash=filehash, download=download)
return status, headers, content
diff --git a/odoo/addons/base/models/ir_logging.py b/odoo/addons/base/models/ir_logging.py
index 04acbe33873..30fcaf3bd1d 100644
--- a/odoo/addons/base/models/ir_logging.py
+++ b/odoo/addons/base/models/ir_logging.py
@@ -35,4 +35,10 @@ class IrLogging(models.Model):
@api.model_cr
def init(self):
super(IrLogging, self).init()
- self._cr.execute("ALTER TABLE ir_logging DROP CONSTRAINT IF EXISTS ir_logging_write_uid_fkey")
+ self._cr.execute("select 1 from information_schema.constraint_column_usage where table_name = 'ir_logging' and constraint_name = 'ir_logging_write_uid_fkey'")
+ if self._cr.rowcount:
+ # DROP CONSTRAINT unconditionally takes an ACCESS EXCLUSIVE lock
+ # on the table, even "IF EXISTS" is set and not matching; disabling
+ # the relevant trigger instead acquires SHARE ROW EXCLUSIVE, which
+ # still conflicts with the ROW EXCLUSIVE needed for an insert
+ self._cr.execute("ALTER TABLE ir_logging DROP CONSTRAINT ir_logging_write_uid_fkey")
diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py
index 4c33eb38b79..e25cb176b60 100644
--- a/odoo/addons/base/models/ir_model.py
+++ b/odoo/addons/base/models/ir_model.py
@@ -1275,6 +1275,7 @@ class IrModelAccess(models.Model):
msg_params['groups_list'] = groups
else:
msg_tail = _("No group currently allows this operation.")
+ msg_tail += ' - ({} {}, {} {})'.format(_('Operation:'), mode, _('User:'), self._uid)
_logger.info('Access Denied by ACLs for operation: %s, uid: %s, model: %s', mode, self._uid, model)
msg = '%s %s' % (msg_heads[mode], msg_tail)
raise AccessError(msg % msg_params)
diff --git a/odoo/addons/test_access_rights/tests/test_feedback.py b/odoo/addons/test_access_rights/tests/test_feedback.py
index 7fe27af11a9..1c349b75f6f 100644
--- a/odoo/addons/test_access_rights/tests/test_feedback.py
+++ b/odoo/addons/test_access_rights/tests/test_feedback.py
@@ -47,7 +47,7 @@ class TestACLFeedback(Feedback):
self.record.sudo(self.user).write({'val': 10})
self.assertEqual(
ctx.exception.args[0],
- """Sorry, you are not allowed to modify documents of type 'Object For Test Access Right' (test_access_right.some_obj). No group currently allows this operation."""
+ """Sorry, you are not allowed to modify documents of type 'Object For Test Access Right' (test_access_right.some_obj). No group currently allows this operation. - (Operation: write, User: %d)""" % self.user.id
)
def test_one_group(self):
@@ -57,12 +57,12 @@ class TestACLFeedback(Feedback):
})
self.assertEqual(
ctx.exception.args[0],
- """Sorry, you are not allowed to create documents of type 'Object For Test Access Right' (test_access_right.some_obj). This operation is allowed for the groups:\n\t- Group 0"""
+ """Sorry, you are not allowed to create documents of type 'Object For Test Access Right' (test_access_right.some_obj). This operation is allowed for the groups:\n\t- Group 0 - (Operation: create, User: %d)""" % self.user.id
)
def test_two_groups(self):
r = self.record.sudo(self.user)
- expected = """Sorry, you are not allowed to access documents of type 'Object For Test Access Right' (test_access_right.some_obj). This operation is allowed for the groups:\n\t- Group 0\n\t- Group 1"""
+ expected = """Sorry, you are not allowed to access documents of type 'Object For Test Access Right' (test_access_right.some_obj). This operation is allowed for the groups:\n\t- Group 0\n\t- Group 1 - (Operation: read, User: %d)""" % self.user.id
with self.assertRaises(AccessError) as ctx:
# noinspection PyStatementEffect
r.val
diff --git a/odoo/netsvc.py b/odoo/netsvc.py
index 76bef839eec..2cf24a36a61 100644
--- a/odoo/netsvc.py
+++ b/odoo/netsvc.py
@@ -29,7 +29,7 @@ def log(logger, level, prefix, msg, depth=None):
path_prefix = os.path.realpath(os.path.dirname(os.path.dirname(__file__)))
class PostgreSQLHandler(logging.Handler):
- """ PostgreSQL Loggin Handler will store logs in the database, by default
+ """ PostgreSQL Logging Handler will store logs in the database, by default
the current database, can be set using --log-db=DBNAME
"""
def emit(self, record):
@@ -39,7 +39,8 @@ class PostgreSQLHandler(logging.Handler):
if not dbname:
return
with tools.ignore(Exception), tools.mute_logger('odoo.sql_db'), sql_db.db_connect(dbname, allow_uri=True).cursor() as cr:
- cr.autocommit(True)
+ # preclude risks of deadlocks
+ cr.execute("SET LOCAL statement_timeout = 1000")
msg = tools.ustr(record.msg)
if record.args:
msg = msg % record.args
diff --git a/odoo/tests/common.py b/odoo/tests/common.py
index 90655bd24b6..f4c36abe2a8 100644
--- a/odoo/tests/common.py
+++ b/odoo/tests/common.py
@@ -486,7 +486,7 @@ class ChromeBrowser():
self.chrome_process.wait()
if self.user_data_dir and os.path.isdir(self.user_data_dir) and self.user_data_dir != '/':
self._logger.info('Removing chrome user profile "%s"', self.user_data_dir)
- shutil.rmtree(self.user_data_dir)
+ shutil.rmtree(self.user_data_dir, ignore_errors=True)
# Restore previous signal handler
if self.sigxcpu_handler and os.name == 'posix':
signal.signal(signal.SIGXCPU, self.sigxcpu_handler)
@@ -828,12 +828,12 @@ class HttpCase(TransactionCase):
self.opener = requests.Session()
self.opener.cookies['session_id'] = self.session_id
- def url_open(self, url, data=None, timeout=10):
+ def url_open(self, url, data=None, timeout=10, headers=None):
if url.startswith('/'):
url = "http://%s:%s%s" % (HOST, PORT, url)
if data:
- return self.opener.post(url, data=data, timeout=timeout)
- return self.opener.get(url, timeout=timeout)
+ return self.opener.post(url, data=data, timeout=timeout, headers=headers)
+ return self.opener.get(url, timeout=timeout, headers=headers)
def _wait_remaining_requests(self):
t0 = int(time.time())