[PATCH] Upstream patch - 29092023

This commit is contained in:
Parthiv Patel
2023-09-29 08:35:59 +00:00
parent 0d0d318ea4
commit 294048d6ee
25 changed files with 299 additions and 56 deletions
+9 -6
View File
@@ -1102,6 +1102,7 @@ class AccountMove(models.Model):
expected_tax_rep_lines = set()
current_tax_rep_lines = set()
inv_recompute_all_taxes = recompute_all_taxes
has_taxes = False
for line in invoice.line_ids:
if line.recompute_tax_line:
inv_recompute_all_taxes = True
@@ -1109,6 +1110,7 @@ class AccountMove(models.Model):
if line.tax_repartition_line_id:
current_tax_rep_lines.add(line.tax_repartition_line_id._origin)
elif line.tax_ids:
has_taxes = True
if invoice.is_invoice(include_receipts=True):
is_refund = invoice.move_type in ('out_refund', 'in_refund')
else:
@@ -1129,12 +1131,13 @@ class AccountMove(models.Model):
delta_tax_rep_lines = expected_tax_rep_lines - current_tax_rep_lines
# Compute taxes.
if inv_recompute_all_taxes:
invoice._recompute_tax_lines()
elif recompute_tax_base_amount:
invoice._recompute_tax_lines(recompute_tax_base_amount=True)
elif delta_tax_rep_lines and not self._context.get('move_reverse_cancel'):
invoice._recompute_tax_lines(tax_rep_lines_to_recompute=delta_tax_rep_lines)
if has_taxes or current_tax_rep_lines:
if inv_recompute_all_taxes:
invoice._recompute_tax_lines()
elif recompute_tax_base_amount:
invoice._recompute_tax_lines(recompute_tax_base_amount=True)
elif delta_tax_rep_lines and not self._context.get('move_reverse_cancel'):
invoice._recompute_tax_lines(tax_rep_lines_to_recompute=delta_tax_rep_lines)
if invoice.is_invoice(include_receipts=True):
@@ -480,6 +480,29 @@ class TestAccountMove(AccountTestInvoicingCommon):
{'name': 'credit_line_1', 'debit': 0.0, 'credit': 1200.0, 'tax_ids': [], 'tax_line_id': False},
])
def test_misc_custom_tags(self):
tag = self.env['account.account.tag'].create({
'name': "test_misc_custom_tags",
'applicability': 'taxes',
'country_id': self.env.ref('base.us').id,
})
move_form = Form(self.env['account.move'].with_context(default_move_type='entry'))
with move_form.line_ids.new() as debit_line:
debit_line.name = 'debit_line'
debit_line.account_id = self.company_data['default_account_revenue']
debit_line.debit = 1000
debit_line.tax_tag_ids.add(tag)
with move_form.line_ids.new() as credit_line:
credit_line.name = 'credit_line'
credit_line.account_id = self.company_data['default_account_revenue']
credit_line.credit = 1000
move = move_form.save()
self.assertRecordValues(move.line_ids, [
# pylint: disable=bad-whitespace
{'debit': 1000.0, 'credit': 0.0, 'tax_tag_ids': tag.ids},
{'debit': 0.0, 'credit': 1000.0, 'tax_tag_ids': []},
])
def test_misc_prevent_unlink_posted_items(self):
# You cannot remove journal items if the related journal entry is posted.
self.test_move.action_post()
+1 -1
View File
@@ -874,7 +874,7 @@ class Import(models.TransientModel):
return base64.b64encode(content)
except Exception as e:
_logger.exception(e)
_logger.warning(e, exc_info=True)
raise ValueError(_("Could not retrieve URL: %(url)s [%(field_name)s: L%(line_number)d]: %(error)s") % {
'url': url,
'field_name': field,
+5 -1
View File
@@ -168,7 +168,11 @@ You receive this email because you are:
@api.model
def run(self, autocommit=False):
schedulers = self.search([('done', '=', False), ('scheduled_date', '<=', datetime.strftime(fields.datetime.now(), tools.DEFAULT_SERVER_DATETIME_FORMAT))])
schedulers = self.search([
('event_id.active', '=', True),
('done', '=', False),
('scheduled_date', '<=', datetime.strftime(fields.datetime.now(), tools.DEFAULT_SERVER_DATETIME_FORMAT))
])
for scheduler in schedulers:
try:
with self.env.cr.savepoint():
@@ -256,3 +256,68 @@ class TestMailSchedule(TestEventCommon, MockEmail):
fields_values={'subject': '%s: today' % test_event.name,
'email_from': self.user_eventmanager.company_id.email_formatted,
})
@mute_logger('flectra.addons.base.models.ir_model', 'flectra.models')
def test_archived_event_mail_schedule(self):
""" Test mail scheduling for archived events """
event_cron_id = self.env.ref('event.event_mail_scheduler')
# deactivate other schedulers to avoid messing with crons
self.env['event.mail'].search([]).unlink()
# freeze some datetimes, and ensure more than 1D+1H before event starts
# to ease time-based scheduler check
now = datetime(2023, 7, 24, 14, 30, 15)
event_date_begin = datetime(2023, 7, 26, 8, 0, 0)
event_date_end = datetime(2023, 7, 28, 18, 0, 0)
with freeze_time(now):
test_event = self.env['event.event'].with_user(self.user_eventmanager).create({
'name': 'TestEventMail',
'auto_confirm': True,
'date_begin': event_date_begin,
'date_end': event_date_end,
'event_mail_ids': [
(0, 0, { # right at subscription
'interval_unit': 'now',
'interval_type': 'after_sub',
'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_subscription')}),
(0, 0, { # 3 hours before event
'interval_nbr': 3,
'interval_unit': 'hours',
'interval_type': 'before_event',
'template_id': self.env['ir.model.data'].xmlid_to_res_id('event.event_reminder')})
]
})
# check event scheduler
scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id)])
self.assertEqual(len(scheduler), 2, 'event: wrong scheduler creation')
event_prev_scheduler = self.env['event.mail'].search([('event_id', '=', test_event.id), ('interval_type', '=', 'before_event')])
with freeze_time(now), self.mock_mail_gateway():
self.env['event.registration'].with_user(self.user_eventuser).create({
'event_id': test_event.id,
'name': 'Reg1',
'email': 'reg1@example.com',
})
self.env['event.registration'].with_user(self.user_eventuser).create({
'event_id': test_event.id,
'name': 'Reg2',
'email': 'reg2@example.com',
})
# check emails effectively sent
self.assertEqual(len(self._new_mails), 2, 'event: should have 2 scheduled emails (1 / registration)')
# Archive the Event
test_event.action_archive()
# execute cron to run schedulers
now_start = event_date_begin + relativedelta(hours=-3)
with freeze_time(now_start), self.mock_mail_gateway():
event_cron_id.method_direct_trigger()
# check that scheduler is not executed
self.assertFalse(event_prev_scheduler.mail_sent, 'event: reminder scheduler should not run')
self.assertFalse(event_prev_scheduler.done, 'event: reminder scheduler should not run')
+2
View File
@@ -328,6 +328,8 @@ Or send your receipts at <a href="mailto:%(email)s?subject=Lunch%%20with%%20cust
raise UserError(_("You cannot report expenses for different employees in the same report."))
if any(not expense.product_id for expense in self):
raise UserError(_("You can not create report without product."))
if len(self.company_id) != 1:
raise UserError(_("You cannot report expenses for different companies in the same report."))
todo = self.filtered(lambda x: x.payment_mode=='own_account') or self.filtered(lambda x: x.payment_mode=='company_account')
sheet = self.env['hr.expense.sheet'].create({
+8
View File
@@ -22,6 +22,14 @@ class IapAccount(models.Model):
account_token = fields.Char(default=lambda s: uuid.uuid4().hex)
company_ids = fields.Many2many('res.company')
@api.model
def create(self, vals):
account = super().create(vals)
if self.env['ir.config_parameter'].sudo().get_param('database.is_neutralized') and account.account_token:
# Disable new accounts on a neutralized database
account.account_token = f"{account.account_token.split('+')[0]}+disabled"
return account
@api.model
def get(self, service_name, force_create=True):
domain = [
+4 -4
View File
@@ -40,7 +40,7 @@
<!-- New report layout for din5008 format -->
<template id="external_layout_din5008">
<div>
<div t-attf-class="header din_page o_company_#{company.id}_layout">
<div t-attf-class="header din_page o_company_#{company.id}_layout #{'din_page_pdf' if report_type == 'pdf' else ''}">
<table class="company_header" t-att-style="'height: %dmm;' % (din_header_spacing or 27)">
<tr>
<td><h3 class="mt0" t-field="company.report_header"/></td>
@@ -49,7 +49,7 @@
</table>
</div>
<div t-attf-class="din_page invoice_note article o_company_#{company.id}_layout" t-att-data-oe-model="o and o._name" t-att-data-oe-id="o and o.id">
<div t-attf-class="din_page invoice_note article o_company_#{company.id}_layout #{'din_page_pdf' if report_type == 'pdf' else ''}" t-att-data-oe-model="o and o._name" t-att-data-oe-id="o and o.id">
<table>
<tr>
<td>
@@ -116,7 +116,7 @@
<t t-raw="0"/>
</div>
<div t-attf-class="din_page footer o_company_#{company.id}_layout">
<div t-attf-class="din_page footer o_company_#{company.id}_layout #{'din_page_pdf' if report_type == 'pdf' else ''}">
<div class="text-right page_number">
<div class="text-muted">
Page: <span class="page"/> of <span class="topage"/>
@@ -126,7 +126,7 @@
<table>
<tr>
<td>
<ul class="list-inline">
<ul class="list-inline text-nowrap">
<li t-if="company.name"><span t-field="company.name"/></li>
<li t-if="company.street"><span t-field="company.street"/></li>
<li t-if="company.street2"><span t-field="company.street2"/></li>
@@ -1,5 +1,4 @@
.din_page {
margin-left: -1rem;
font-size: 9pt;
&.header {
@@ -22,7 +21,6 @@
}
&.invoice_note {
padding-top: 20px;
tr {
td {
vertical-align: bottom;
@@ -110,6 +108,11 @@
}
}
.din_page_pdf {
width: 180mm;
margin-left: -1rem;
}
// TODO WAN remove in master
.din {
+1
View File
@@ -5,5 +5,6 @@ from . import res_partner
from . import res_company
from . import account_invoice
from . import account_edi_format
from . import account_chart_template
from . import ir_mail_server
from . import ddt
@@ -0,0 +1,20 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import models
class AccountChartTemplate(models.Model):
_inherit = 'account.chart.template'
def _load(self, sale_tax_rate, purchase_tax_rate, company):
res = super()._load(sale_tax_rate, purchase_tax_rate, company)
if self == self.env.ref('l10n_it.l10n_it_chart_template_generic', raise_if_not_found=False):
tax = self.env.ref(f'l10n_it.{company.id}_00eu', raise_if_not_found=False)
if tax:
tax.write({
'l10n_it_has_exoneration': True,
'l10n_it_kind_exoneration': 'N3.2',
'l10n_it_law_reference': 'Art. 41, DL n. 331/93',
})
return res
+34 -24
View File
@@ -117,29 +117,46 @@ GROUP BY channel_moderator.res_users_id""", [tuple(self.ids)])
@api.model
def systray_get_activities(self):
activities = self.env["mail.activity"].search([("user_id", "=", self.env.uid)])
activities_by_record_by_model_name = defaultdict(lambda: defaultdict(lambda: self.env["mail.activity"]))
for activity in activities:
record = self.env[activity.res_model].browse(activity.res_id)
activities_by_record_by_model_name[activity.res_model][record] += activity
model_ids = list({self.env["ir.model"]._get(name).id for name in activities_by_record_by_model_name.keys()})
query = """SELECT array_agg(res_id) as res_ids, m.id, count(*),
CASE
WHEN %(today)s::date - act.date_deadline::date = 0 Then 'today'
WHEN %(today)s::date - act.date_deadline::date > 0 Then 'overdue'
WHEN %(today)s::date - act.date_deadline::date < 0 Then 'planned'
END AS states
FROM mail_activity AS act
JOIN ir_model AS m ON act.res_model_id = m.id
WHERE user_id = %(user_id)s
GROUP BY m.id, states;
"""
self.env.cr.execute(query, {
'today': fields.Date.context_today(self),
'user_id': self.env.uid,
})
activity_data = self.env.cr.dictfetchall()
records_by_state_by_model = defaultdict(lambda: {"today": set(), "overdue": set(), "planned": set(), "all": set()})
for data in activity_data:
records_by_state_by_model[data["id"]][data["states"]] = set(data["res_ids"])
records_by_state_by_model[data["id"]]["all"] = records_by_state_by_model[data["id"]]["all"] | set(data["res_ids"])
user_activities = {}
for model_name, activities_by_record in activities_by_record_by_model_name.items():
domain = [("id", "in", list({r.id for r in activities_by_record.keys()}))]
allowed_records = self.env[model_name].search(domain)
for model_id in records_by_state_by_model:
model_dic = records_by_state_by_model[model_id]
model = self.env["ir.model"].browse(model_id).with_prefetch(tuple(records_by_state_by_model.keys()))
allowed_records = self.env[model.model].search([("id", "in", tuple(model_dic["all"]))])
if not allowed_records:
continue
module = self.env[model_name]._original_module
module = self.env[model.model]._original_module
icon = module and modules.module.get_module_icon(module)
user_activities[model_name] = {
"name": self.env["ir.model"]._get(model_name).with_prefetch(model_ids).name,
"model": model_name,
today = len(model_dic["today"] & set(allowed_records.ids))
overdue = len(model_dic["overdue"] & set(allowed_records.ids))
user_activities[model.model] = {
"name": model.name,
"model": model.model,
"type": "activity",
"icon": icon,
"total_count": 0,
"today_count": 0,
"overdue_count": 0,
"planned_count": 0,
"total_count": today + overdue,
"today_count": today,
"overdue_count": overdue,
"planned_count": len(model_dic["planned"] & set(allowed_records.ids)),
"actions": [
{
"icon": "fa-clock-o",
@@ -147,13 +164,6 @@ GROUP BY channel_moderator.res_users_id""", [tuple(self.ids)])
}
],
}
for record, activities in activities_by_record.items():
if record not in allowed_records:
continue
for activity in activities:
user_activities[model_name]["%s_count" % activity.state] += 1
if activity.state in ("today", "overdue"):
user_activities[model_name]["total_count"] += 1
return list(user_activities.values())
@@ -143,7 +143,7 @@ class MicrosoftService(models.AbstractModel):
status = res.status_code
if int(status) in RESOURCE_NOT_FOUND_STATUSES:
response = False
response = {}
else:
# Some answers return empty content
response = res.content and res.json() or {}
@@ -155,7 +155,7 @@ class MicrosoftService(models.AbstractModel):
except requests.HTTPError as error:
if error.response.status_code in RESOURCE_NOT_FOUND_STATUSES:
status = error.response.status_code
response = ""
response = {}
else:
_logger.exception("Bad microsoft request : %s !", error.response.content)
raise error
@@ -95,7 +95,7 @@ class StockProductionLot(models.Model):
for lot in alert_lots:
lot.activity_schedule(
'product_expiry.mail_activity_type_alert_date_reached',
user_id=lot.product_id.responsible_id.id or SUPERUSER_ID,
user_id=lot.product_id.with_company(lot.company_id).responsible_id.id or lot.product_id.responsible_id.id or SUPERUSER_ID,
note=_("The alert date has been reached for this lot/serial number")
)
alert_lots.write({
+1 -1
View File
@@ -126,7 +126,7 @@ class SendSMS(models.TransientModel):
composer.recipient_single_number_itf = ''
continue
records.ensure_one()
res = records._sms_get_recipients_info(force_field=composer.number_field_name, partner_fallback=False)
res = records._sms_get_recipients_info(force_field=composer.number_field_name, partner_fallback=True)
composer.recipient_single_description = res[records.id]['partner'].name or records._sms_get_default_partners().display_name
composer.recipient_single_number = res[records.id]['number'] or ''
if not composer.recipient_single_number_itf:
+4 -2
View File
@@ -515,11 +515,13 @@ class StockMove(models.Model):
cost = -1 * cost
self.with_company(self.company_id)._create_account_move_line(acc_valuation, acc_dest, journal_id, qty, description, svl_id, cost)
elif self._is_dropshipped_returned():
if cost > 0:
if cost > 0 and self.location_dest_id._should_be_valued():
self.with_company(self.company_id)._create_account_move_line(acc_valuation, acc_src, journal_id, qty, description, svl_id, cost)
elif cost > 0:
self.with_company(self.company_id)._create_account_move_line(acc_dest, acc_valuation, journal_id, qty, description, svl_id, cost)
else:
cost = -1 * cost
self.with_company(self.company_id)._create_account_move_line(acc_dest, acc_valuation, journal_id, qty, description, svl_id, cost)
self.with_company(self.company_id)._create_account_move_line(acc_valuation, acc_src, journal_id, qty, description, svl_id, cost)
if self.company_id.anglo_saxon_accounting:
# Eventually reconcile together the invoice and valuation accounting entries on the stock interim accounts
@@ -1076,3 +1076,89 @@ class TestAngloSaxonAccounting(TestStockValuationCommon):
self.assertEqual(len(anglo_lines), 2)
self.assertEqual(abs(anglo_lines[0].balance), 10)
self.assertEqual(abs(anglo_lines[1].balance), 10)
def test_dropship_return_accounts_1(self):
"""
When returning a dropshipped move, make sure the correct accounts are used
"""
# pylint: disable=bad-whitespace
self.product1.categ_id.property_cost_method = 'fifo'
move1 = self._make_dropship_move(self.product1, 2, unit_cost=10)
move2 = self._make_return(move1, 2)
# First: Input -> Valuation
# Second: Valuation -> Output
origin_svls = move1.stock_valuation_layer_ids.sorted('quantity', reverse=True)
# First: Output -> Valuation
# Second: Valuation -> Input
return_svls = move2.stock_valuation_layer_ids.sorted('quantity', reverse=True)
self.assertEqual(len(origin_svls), 2)
self.assertEqual(len(return_svls), 2)
acc_in, acc_out, acc_valuation = self.stock_input_account, self.stock_output_account, self.stock_valuation_account
# Dropshipping should be: Input -> Output
self.assertRecordValues(origin_svls[0].account_move_id.line_ids, [
{'account_id': acc_in.id, 'debit': 0, 'credit': 20},
{'account_id': acc_valuation.id, 'debit': 20, 'credit': 0},
])
self.assertRecordValues(origin_svls[1].account_move_id.line_ids, [
{'account_id': acc_valuation.id, 'debit': 0, 'credit': 20},
{'account_id': acc_out.id, 'debit': 20, 'credit': 0},
])
# Return should be: Output -> Input
self.assertRecordValues(return_svls[0].account_move_id.line_ids, [
{'account_id': acc_out.id, 'debit': 0, 'credit': 20},
{'account_id': acc_valuation.id, 'debit': 20, 'credit': 0},
])
self.assertRecordValues(return_svls[1].account_move_id.line_ids, [
{'account_id': acc_valuation.id, 'debit': 0, 'credit': 20},
{'account_id': acc_in.id, 'debit': 20, 'credit': 0},
])
def test_dropship_return_accounts_2(self):
"""
When returning a dropshipped move, make sure the correct accounts are used
"""
# pylint: disable=bad-whitespace
self.product1.categ_id.property_cost_method = 'fifo'
move1 = self._make_dropship_move(self.product1, 2, unit_cost=10)
# return to WH/Stock
stock_return_picking = Form(self.env['stock.return.picking']\
.with_context(active_ids=[move1.picking_id.id], active_id=move1.picking_id.id, active_model='stock.picking'))
stock_return_picking = stock_return_picking.save()
stock_return_picking.product_return_moves.quantity = 2
stock_return_picking.location_id = self.stock_location
stock_return_picking_action = stock_return_picking.create_returns()
return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
return_pick.move_lines[0].move_line_ids[0].qty_done = 2
return_pick._action_done()
move2 = return_pick.move_lines
# First: Input -> Valuation
# Second: Valuation -> Output
origin_svls = move1.stock_valuation_layer_ids.sorted('quantity', reverse=True)
# Only one: Output -> Valuation
return_svl = move2.stock_valuation_layer_ids
self.assertEqual(len(origin_svls), 2)
self.assertEqual(len(return_svl), 1)
acc_in, acc_out, acc_valuation = self.stock_input_account, self.stock_output_account, self.stock_valuation_account
# Dropshipping should be: Input -> Output
self.assertRecordValues(origin_svls[0].account_move_id.line_ids, [
{'account_id': acc_in.id, 'debit': 0, 'credit': 20},
{'account_id': acc_valuation.id, 'debit': 20, 'credit': 0},
])
self.assertRecordValues(origin_svls[1].account_move_id.line_ids, [
{'account_id': acc_valuation.id, 'debit': 0, 'credit': 20},
{'account_id': acc_out.id, 'debit': 20, 'credit': 0},
])
# Return should be: Output -> Valuation
self.assertRecordValues(return_svl.account_move_id.line_ids, [
{'account_id': acc_out.id, 'debit': 0, 'credit': 20},
{'account_id': acc_valuation.id, 'debit': 20, 'credit': 0},
])
@@ -134,6 +134,7 @@ class TestSMSComposerComment(TestMailFullCommon, TestRecipients):
self.assertSMSNotification([{'number': self.random_numbers_san[0]}], self._test_body)
def test_composer_default_recipient(self):
""" Test default description of SMS composer must be partner name"""
self.test_record.write({
'phone_nbr': '0123456789',
})
@@ -145,9 +146,21 @@ class TestSMSComposerComment(TestMailFullCommon, TestRecipients):
'number_field_name': 'phone_nbr',
})
self.assertFalse(composer.recipient_single_valid)
self.assertEqual(composer.recipient_single_description, self.test_record.customer_id.display_name)
def test_composer_nofield_w_customer(self):
""" Test SMS composer without number field, the number on partner must be used instead"""
with self.with_user('employee'):
composer = self.env['sms.composer'].with_context(
default_res_model='mail.test.sms', default_res_id=self.test_record.id,
).create({
'body': self._test_body,
})
self.assertTrue(composer.recipient_single_valid)
self.assertEqual(composer.recipient_single_number, self.test_numbers[1])
self.assertEqual(composer.recipient_single_number_itf, self.test_numbers[1])
def test_composer_internals(self):
with self.with_user('employee'):
composer = self.env['sms.composer'].with_context(
+1 -1
View File
@@ -473,7 +473,7 @@ class Image(models.AbstractModel):
# force a complete load of the image data to validate it
image.load()
except Exception:
logger.exception("Failed to load remote image %r", url)
logger.warning("Failed to load remote image %r", url, exc_info=True)
return None
# don't use original data in case weird stuff was smuggled in, with
@@ -981,6 +981,9 @@ registry.anchorSlide = publicWidget.Widget.extend({
* @private
*/
_onAnimateClick: function (ev) {
if (this.$target[0].pathname !== window.location.pathname) {
return;
}
var hash = this.$target[0].hash;
if (hash === '#top' || hash === '#bottom') {
// If the anchor targets #top or #bottom, directly call the
+1 -1
View File
@@ -21,7 +21,7 @@ class WebsiteVisitor(models.Model):
self.flush()
left_visitors = self.filtered(lambda visitor: not visitor.email or not visitor.mobile)
leads = left_visitors.mapped('lead_ids').sorted('create_date', reverse=True)
leads = left_visitors.sudo().mapped('lead_ids').sorted('create_date', reverse=True)
visitor_to_lead_ids = dict((visitor.id, visitor.lead_ids.ids) for visitor in left_visitors)
for visitor in left_visitors:
@@ -21,7 +21,7 @@
<field name="arch" type="xml">
<xpath expr="//button[@id='w_visitor_visit_counter']" position="before">
<button name="%(website_crm.website_visitor_crm_lead_action)d" type="action" class="oe_stat_button" icon="fa-star"
attrs="{'invisible': [('lead_count', '=', 0)]}">
groups="sales_team.group_sale_salesman" attrs="{'invisible': [('lead_count', '=', 0)]}">
<field name="lead_count" widget="statinfo" string="Leads"/>
</button>
</xpath>
@@ -8,7 +8,7 @@
<div class="row align-items-center justify-content-between">
<!-- Desktop Mode -->
<nav aria-label="breadcrumb" class="col-md-8 d-none d-md-flex">
<ol class="breadcrumb bg-transparent mb-0 pl-0 py-0">
<ol class="breadcrumb bg-transparent mb-0 pl-0 py-0 overflow-hidden">
<li class="breadcrumb-item">
<a href="/slides">Courses</a>
</li>
@@ -28,7 +28,7 @@
<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">
<li t-if="slide" class="breadcrumb-item active text-truncate text-white">
<a t-att-href="'/slides/slide/%s' % slug(slide)"><span t-esc="slide.name"/></a>
</li>
</ol>
@@ -151,7 +151,7 @@
t-att-class="'mr-1 fa fa-fw %s' % ('text-success fa-check-circle' if channel_progress[aside_slide.id].get('completed') else 'text-600 fa-circle')">
</i>
</div>
<div class="o_wslides_lesson_link_name">
<div class="o_wslides_lesson_link_name text-truncate">
<t t-call="website_slides.slide_icon">
<t t-set="slide" t-value="aside_slide"/>
</t>
@@ -208,8 +208,8 @@
<t t-set="is_training_channel" t-value="slide.channel_id.channel_type == 'training'"/>
<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="h4 my-0">
<div class="d-flex align-items-center overflow-hidden">
<h1 class="h4 my-0 text-truncate">
<t t-call="website_slides.slide_icon">
<t t-set="icon_class" t-valuef="mr-1"/>
</t>
@@ -105,18 +105,18 @@
<i t-if="slide_completed and is_member" class="o_wslides_slide_completed fa fa-check fa-fw text-success" t-att-data-slide-id="slide.id"/>
<i t-if="not slide_completed and is_member" class="fa fa-circle-thin fa-fw" t-att-data-slide-id="slide.id"/>
</span>
<div class="ml-2">
<div class="ml-2 overflow-hidden">
<a t-if="can_access" class="d-block pt-1" href="#">
<div class="d-flex ">
<t t-call="website_slides.slide_icon"/>
<div class="o_wslides_fs_slide_name" t-esc="slide.name"/>
<div class="o_wslides_fs_slide_name text-truncate" t-esc="slide.name"/>
</div>
</a>
<span t-else="" class="d-block pt-1" href="#">
<div class="d-flex ">
<t t-set="icon_class" t-value="'mr-2 text-600'"/>
<t t-call="website_slides.slide_icon"/>
<div class="o_wslides_fs_slide_name text-600" t-esc="slide.name"/>
<div class="o_wslides_fs_slide_name text-600 text-truncate" t-esc="slide.name"/>
</div>
</span>
<ul class="list-unstyled w-100 pt-2 small" t-if="slide.link_ids or slide._has_additional_resources() or (slide.question_ids and not slide.slide_type =='quiz')" >