[PATCH] Upstream patch - 01022023

This commit is contained in:
Parthiv Patel
2023-02-01 08:35:17 +00:00
parent e31afdaf95
commit 7d53ba8b53
22 changed files with 388 additions and 67 deletions
+7 -8
View File
@@ -36,14 +36,13 @@ class AuthSignupHome(Home):
try:
self.do_signup(qcontext)
# Send an account creation confirmation email
if qcontext.get('token'):
User = request.env['res.users']
user_sudo = User.sudo().search(
User._get_login_domain(qcontext.get('login')), order=User._get_login_order(), limit=1
)
template = request.env.ref('auth_signup.mail_template_user_signup_account_created', raise_if_not_found=False)
if user_sudo and template:
template.sudo().send_mail(user_sudo.id, force_send=True)
User = request.env['res.users']
user_sudo = User.sudo().search(
User._get_login_domain(qcontext.get('login')), order=User._get_login_order(), limit=1
)
template = request.env.ref('auth_signup.mail_template_user_signup_account_created', raise_if_not_found=False)
if user_sudo and template:
template.sudo().send_mail(user_sudo.id, force_send=True)
return self.web_login(*args, **kw)
except UserError as e:
qcontext['error'] = e.args[0]
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import test_auth_signup
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra.tests import HttpCase
from flectra import http
class TestAuthSignupFlow(HttpCase):
def setUp(self):
super(TestAuthSignupFlow, self).setUp()
res_config = self.env['res.config.settings']
self.default_values = res_config.default_get(list(res_config.fields_get()))
def _activate_free_signup(self):
self.default_values.update({'auth_signup_uninvited': 'b2c'})
def _get_free_signup_url(self):
return '/web/signup'
def test_confirmation_mail_free_signup(self):
"""
Check if a new user is informed by email when he is registered
"""
# Activate free signup
self._activate_free_signup()
# Get csrf_token
self.authenticate(None, None)
csrf_token = http.WebRequest.csrf_token(self)
# Values from login form
name = 'toto'
payload = {
'login': 'toto@example.com',
'name': name,
'password': 'mypassword',
'confirm_password': 'mypassword',
'csrf_token': csrf_token,
}
# Call the controller
url_free_signup = self._get_free_signup_url()
self.url_open(url_free_signup, data=payload)
# Check if an email is sent to the new user
new_user = self.env['res.users'].search([('name', '=', name)])
self.assertTrue(new_user)
mail = self.env['mail.message'].search([('message_type', '=', 'email'), ('model', '=', 'res.users'), ('res_id', '=', new_user.id)], limit=1)
self.assertTrue(mail, "The new user must be informed of his registration")
@@ -39,7 +39,7 @@ class HrOrgChartController(http.Controller):
job_id=job.id,
job_name=job.name or '',
job_title=employee.job_title or '',
direct_sub_count=len(employee.child_ids),
direct_sub_count=len(employee.child_ids - employee),
indirect_sub_count=employee.child_all_count,
)
@@ -91,7 +91,7 @@ class HrOrgChartController(http.Controller):
return {}
if subordinates_type == 'direct':
res = employee.child_ids.ids
res = (employee.child_ids - employee).ids
elif subordinates_type == 'indirect':
res = (employee.subordinate_ids - employee.child_ids).ids
else:
@@ -35,4 +35,4 @@
"cuenta701_01","Pérdida cambiaria","701.01.01","account.data_account_type_expenses","l10n_mx.mx_coa","l10n_mx.account_tag_701_01","False"
"cuenta702_01","Utilidad cambiaria","702.01.01","account.data_account_type_revenue","l10n_mx.mx_coa","l10n_mx.account_tag_702_01","False"
"cuenta801_01","Utilidad o pérdida fiscal en venta y/o baja de activo fijo","811.01.01","account.data_account_type_expenses","l10n_mx.mx_coa","l10n_mx.account_tag_811_01","False"
"cuenta801_01_99","Base Imponible de Impuestos en Base a Flujo de Efectivo","899.01.99","account.data_account_type_expenses","l10n_mx.mx_coa","l10n_mx.account_tag_801_01","False"
"cuenta801_01_99","Base Imponible de Impuestos en Base a Flujo de Efectivo","899.01.99","account.data_account_type_expenses","l10n_mx.mx_coa","l10n_mx.account_tag_899_01","False"
1 id name code user_type_id/id chart_template_id/id tag_ids/id reconcile
35 cuenta701_01 Pérdida cambiaria 701.01.01 account.data_account_type_expenses l10n_mx.mx_coa l10n_mx.account_tag_701_01 False
36 cuenta702_01 Utilidad cambiaria 702.01.01 account.data_account_type_revenue l10n_mx.mx_coa l10n_mx.account_tag_702_01 False
37 cuenta801_01 Utilidad o pérdida fiscal en venta y/o baja de activo fijo 811.01.01 account.data_account_type_expenses l10n_mx.mx_coa l10n_mx.account_tag_811_01 False
38 cuenta801_01_99 Base Imponible de Impuestos en Base a Flujo de Efectivo 899.01.99 account.data_account_type_expenses l10n_mx.mx_coa l10n_mx.account_tag_801_01 l10n_mx.account_tag_899_01 False
@@ -21,7 +21,6 @@ class MicrosoftOutlookMixin(models.AbstractModel):
_description = 'Microsoft Outlook Mixin'
_OUTLOOK_SCOPE = None
_OUTLOOK_ENDPOINT = 'https://login.microsoftonline.com/common/oauth2/v2.0/'
use_microsoft_outlook_service = fields.Boolean('Outlook Authentication')
is_microsoft_outlook_configured = fields.Boolean('Is Outlook Credential Configured',
@@ -53,7 +52,7 @@ class MicrosoftOutlookMixin(models.AbstractModel):
record.microsoft_outlook_uri = False
continue
record.microsoft_outlook_uri = url_join(self._OUTLOOK_ENDPOINT, 'authorize?%s' % url_encode({
record.microsoft_outlook_uri = url_join(self._get_microsoft_endpoint(), 'authorize?%s' % url_encode({
'client_id': microsoft_outlook_client_id,
'response_type': 'code',
'redirect_uri': url_join(base_url, '/microsoft_outlook/confirm'),
@@ -127,7 +126,7 @@ class MicrosoftOutlookMixin(models.AbstractModel):
microsoft_outlook_client_secret = Config.get_param('microsoft_outlook_client_secret')
response = requests.post(
url_join(self._OUTLOOK_ENDPOINT, 'token'),
url_join(self._get_microsoft_endpoint(), 'token'),
data={
'client_id': microsoft_outlook_client_id,
'client_secret': microsoft_outlook_client_secret,
@@ -188,3 +187,10 @@ class MicrosoftOutlookMixin(models.AbstractModel):
scope='microsoft_outlook_oauth',
message=(self._name, self.id),
)
@api.model
def _get_microsoft_endpoint(self):
return self.env["ir.config_parameter"].sudo().get_param(
'microsoft_outlook.endpoint',
'https://login.microsoftonline.com/common/oauth2/v2.0/',
)
+2 -1
View File
@@ -189,12 +189,13 @@ class PosOrder(models.Model):
order.add_payment(return_payment_vals)
def _prepare_invoice_line(self, order_line):
name = order_line.product_id.default_code + " " + order_line.product_id.display_name if order_line.product_id.default_code else order_line.product_id.display_name
return {
'product_id': order_line.product_id.id,
'quantity': order_line.qty if self.amount_total >= 0 else -order_line.qty,
'discount': order_line.discount,
'price_unit': order_line.price_unit,
'name': order_line.product_id.display_name,
'name': name,
'tax_ids': [(6, 0, order_line.tax_ids_after_fiscal_position.ids)],
'product_uom_id': order_line.product_uom_id.id,
}
@@ -37,12 +37,19 @@ class PosPaymentMethod(models.Model):
for payment_method in self:
if not payment_method.adyen_terminal_identifier:
continue
existing_payment_method = self.search([('id', '!=', payment_method.id),
# sudo() to search all companies
existing_payment_method = self.sudo().search([('id', '!=', payment_method.id),
('adyen_terminal_identifier', '=', payment_method.adyen_terminal_identifier)],
limit=1)
if existing_payment_method:
raise ValidationError(_('Terminal %s is already used on payment method %s.')
if existing_payment_method.company_id == payment_method.company_id:
raise ValidationError(_('Terminal %s is already used on payment method %s.')
% (payment_method.adyen_terminal_identifier, existing_payment_method.display_name))
else:
raise ValidationError(_('Terminal %s is already used in company %s on payment method %s.')
% (payment_method.adyen_terminal_identifier,
existing_payment_method.company_id.name,
existing_payment_method.display_name))
def _get_adyen_endpoints(self):
return {
+1 -1
View File
@@ -13,7 +13,7 @@ class AccountMoveLine(models.Model):
if so_line:
bom = self.env['mrp.bom']._bom_find(product=so_line.product_id, company_id=so_line.company_id.id, bom_type='phantom')[:1]
if bom:
is_line_reversing = bool(self.move_id.reversed_entry_id)
is_line_reversing = self.move_id.move_type == 'out_refund'
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
posted_invoice_lines = so_line.invoice_lines.filtered(lambda l: l.move_id.state == 'posted' and bool(l.move_id.reversed_entry_id) == is_line_reversing)
qty_invoiced = sum([x.product_uom_id._compute_quantity(x.quantity, x.product_id.uom_id) for x in posted_invoice_lines])
@@ -2188,6 +2188,103 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon):
self.assertEqual(cogs_aml.debit, 0)
self.assertEqual(cogs_aml.credit, 20, 'Should be to the value of the returned component')
def test_anglo_saxo_return_and_create_invoice(self):
"""
When creating an invoice for a returned kit, the value of the anglo-saxo lines
should be based on the returned component's value
"""
stock_input_account, stock_output_account, stock_valuation_account, expense_account, stock_journal = _create_accounting_data(self.env)
fifo = self.env['product.category'].create({
'name': 'FIFO',
'property_valuation': 'real_time',
'property_cost_method': 'fifo',
'property_stock_account_input_categ_id': stock_input_account.id,
'property_stock_account_output_categ_id': stock_output_account.id,
'property_stock_valuation_account_id': stock_valuation_account.id,
'property_stock_journal': stock_journal.id,
})
kit = self._create_product('Simple Kit', self.uom_unit)
(kit + self.component_a).categ_id = fifo
kit.property_account_expense_id = expense_account
self.env['mrp.bom'].create({
'product_tmpl_id': kit.product_tmpl_id.id,
'product_qty': 1.0,
'type': 'phantom',
'bom_line_ids': [(0, 0, {'product_id': self.component_a.id, 'product_qty': 1.0})]
})
# Receive 3 components: one @10, one @20 and one @60
in_moves = self.env['stock.move'].create([{
'name': 'IN move @%s' % p,
'product_id': self.component_a.id,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
'product_uom': self.component_a.uom_id.id,
'product_uom_qty': 1,
'price_unit': p,
} for p in [10, 20, 60]])
in_moves._action_confirm()
in_moves.quantity_done = 1
in_moves._action_done()
# Sell 3 kits
so = self.env['sale.order'].create({
'partner_id': self.env.ref('base.res_partner_1').id,
'order_line': [
(0, 0, {
'name': kit.name,
'product_id': kit.id,
'product_uom_qty': 3.0,
'product_uom': kit.uom_id.id,
'price_unit': 100,
'tax_id': False,
})],
})
so.action_confirm()
# Deliver the components: 1@10, then 1@20 and then 1@60
pickings = []
picking = so.picking_ids
while picking:
pickings.append(picking)
picking.move_lines.quantity_done = 1
action = picking.button_validate()
if isinstance(action, dict):
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
picking = picking.backorder_ids
invoice = so._create_invoices()
invoice.action_post()
# Return the second picking (i.e. one component @20)
ctx = {'active_id': pickings[1].id, 'active_model': 'stock.picking'}
return_wizard = Form(self.env['stock.return.picking'].with_context(ctx)).save()
return_picking_id, dummy = return_wizard._create_returns()
return_picking = self.env['stock.picking'].browse(return_picking_id)
return_picking.move_lines.quantity_done = 1
return_picking.button_validate()
# Create a new invoice for the returned kit
ctx = {'active_model': 'sale.order', 'active_ids': so.ids}
create_invoice_wizard = self.env['sale.advance.payment.inv'].with_context(ctx).create({'advance_payment_method': 'delivered'})
create_invoice_wizard.create_invoices()
reverse_invoice = so.invoice_ids[-1]
with Form(reverse_invoice) as reverse_invoice_form:
with reverse_invoice_form.invoice_line_ids.edit(0) as line:
line.quantity = 1
reverse_invoice.action_post()
amls = reverse_invoice.line_ids
stock_out_aml = amls.filtered(lambda aml: aml.account_id == stock_output_account)
self.assertEqual(stock_out_aml.debit, 20, 'Should be to the value of the returned component')
self.assertEqual(stock_out_aml.credit, 0)
cogs_aml = amls.filtered(lambda aml: aml.account_id == expense_account)
self.assertEqual(cogs_aml.debit, 0)
self.assertEqual(cogs_aml.credit, 20, 'Should be to the value of the returned component')
def test_kit_margin_and_return_picking(self):
""" This test ensure that, when returning the components of a sold kit, the
sale order line cost does not change"""
+1 -1
View File
@@ -117,7 +117,7 @@ class AccountMoveLine(models.Model):
so_line = self.sale_line_ids and self.sale_line_ids[-1] or False
if so_line:
is_line_reversing = bool(self.move_id.reversed_entry_id)
is_line_reversing = self.move_id.move_type == 'out_refund'
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
account_moves = so_line.invoice_lines.move_id.filtered(lambda m: m.state == 'posted' and bool(m.reversed_entry_id) == is_line_reversing)
posted_cogs = account_moves.line_ids.filtered(lambda l: l.is_anglo_saxon_line and l.product_id == self.product_id and l.balance > 0)
@@ -1397,6 +1397,97 @@ class TestAngloSaxonValuation(ValuationReconciliationTestCommon):
self.assertEqual(cogs_aml.debit, 0)
self.assertEqual(cogs_aml.credit, 20, 'Should be to the value of the returned product')
def test_fifo_return_and_create_invoice(self):
"""
When creating an invoice for a returned product, the value of the anglo-saxo lines
should be based on the returned product's value
"""
self.product.categ_id.property_cost_method = 'fifo'
# Receive one @10, one @20 and one @60
in_moves = self.env['stock.move'].create([{
'name': 'IN move @%s' % p,
'product_id': self.product.id,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
'product_uom': self.product.uom_id.id,
'product_uom_qty': 1,
'price_unit': p,
} for p in [10, 20, 60]])
in_moves._action_confirm()
in_moves.quantity_done = 1
in_moves._action_done()
# Sell 3 units
so = self.env['sale.order'].create({
'partner_id': self.partner_a.id,
'order_line': [
(0, 0, {
'name': self.product.name,
'product_id': self.product.id,
'product_uom_qty': 3.0,
'product_uom': self.product.uom_id.id,
'price_unit': 100,
'tax_id': False,
})],
})
so.action_confirm()
# Deliver 1@10, then 1@20 and then 1@60
pickings = []
picking = so.picking_ids
while picking:
pickings.append(picking)
picking.move_lines.quantity_done = 1
action = picking.button_validate()
if isinstance(action, dict):
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
picking = picking.backorder_ids
invoice = so._create_invoices()
invoice.action_post()
# Receive one @100
in_moves = self.env['stock.move'].create({
'name': 'IN move @100',
'product_id': self.product.id,
'location_id': self.env.ref('stock.stock_location_suppliers').id,
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
'product_uom': self.product.uom_id.id,
'product_uom_qty': 1,
'price_unit': 100,
})
in_moves._action_confirm()
in_moves.quantity_done = 1
in_moves._action_done()
# Return the second picking (i.e. 1@20)
ctx = {'active_id': pickings[1].id, 'active_model': 'stock.picking'}
return_wizard = Form(self.env['stock.return.picking'].with_context(ctx)).save()
return_picking_id, dummy = return_wizard._create_returns()
return_picking = self.env['stock.picking'].browse(return_picking_id)
return_picking.move_lines.quantity_done = 1
return_picking.button_validate()
# Create a new invoice for the returned product
ctx = {'active_model': 'sale.order', 'active_ids': so.ids}
create_invoice_wizard = self.env['sale.advance.payment.inv'].with_context(ctx).create({'advance_payment_method': 'delivered'})
create_invoice_wizard.create_invoices()
reverse_invoice = so.invoice_ids[-1]
with Form(reverse_invoice) as reverse_invoice_form:
with reverse_invoice_form.invoice_line_ids.edit(0) as line:
line.quantity = 1
reverse_invoice.action_post()
amls = reverse_invoice.line_ids
stock_out_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out'])
self.assertEqual(stock_out_aml.debit, 20, 'Should be to the value of the returned product')
self.assertEqual(stock_out_aml.credit, 0)
cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
self.assertEqual(cogs_aml.debit, 0)
self.assertEqual(cogs_aml.credit, 20, 'Should be to the value of the returned product')
def test_fifo_several_invoices_reset_repost(self):
self.product.categ_id.property_cost_method = 'fifo'
+2 -5
View File
@@ -5,6 +5,7 @@ from werkzeug.exceptions import InternalServerError
from flectra import http
from flectra.http import request
from flectra.addons.web.controllers.main import _serialize_exception
from flectra.tools.misc import html_escape
import json
@@ -35,9 +36,5 @@ class StockReportController(http.Controller):
'message': 'Flectra Server Error',
'data': se
}
res = werkzeug.wrappers.Response(
json.dumps(error),
status=500,
headers=[("Content-Type", "application/json")]
)
res = request.make_response(html_escape(json.dumps(error)))
raise InternalServerError(response=res) from e
+8 -2
View File
@@ -551,7 +551,10 @@ class ProductProduct(models.Model):
if float_is_zero(product.quantity_svl, precision_rounding=product.uom_id.rounding):
# FIXME: create an empty layer to track the change?
continue
svsl_vals = product._prepare_out_svl_vals(product.quantity_svl, self.env.company)
if float_compare(product.quantity_svl, 0, precision_rounding=product.uom_id.rounding) > 0:
svsl_vals = product._prepare_out_svl_vals(product.quantity_svl, self.env.company)
else:
svsl_vals = product._prepare_in_svl_vals(abs(product.quantity_svl), product.value_svl / product.quantity_svl)
svsl_vals['description'] = description + svsl_vals.pop('rounding_adjustment', '')
svsl_vals['company_id'] = self.env.company.id
empty_stock_svl_list.append(svsl_vals)
@@ -562,7 +565,10 @@ class ProductProduct(models.Model):
for product in self:
quantity_svl = products_orig_quantity_svl[product.id]
if quantity_svl:
svl_vals = product._prepare_in_svl_vals(quantity_svl, product.standard_price)
if float_compare(quantity_svl, 0, precision_rounding=product.uom_id.rounding) > 0:
svl_vals = product._prepare_in_svl_vals(quantity_svl, product.standard_price)
else:
svl_vals = product._prepare_out_svl_vals(abs(quantity_svl), self.env.company)
svl_vals['description'] = description
svl_vals['company_id'] = self.env.company.id
refill_stock_svl_list.append(svl_vals)
@@ -1,4 +1,11 @@
@media print {
.chartjs-size-monitor {
display: none;
}
.chartjs-render-monitor {
width: 100% !important;
height: 100% !important;
}
.js_surveyform {
font-size: 13px;
}
+10
View File
@@ -72,6 +72,16 @@ class TestSurveyInvite(common.TestSurveyCommon):
self.assertEqual(answers.mapped('partner_id'), self.customer)
self.assertEqual(set(answers.mapped('deadline')), set([deadline]))
with self.subTest('Warning when inviting an already invited partner'):
action = self.survey.action_send_survey()
invite_form = Form(self.env[action['res_model']].with_context(action['context']))
invite_form.partner_ids.add(self.customer)
self.assertIn(self.customer, invite_form.existing_partner_ids)
self.assertEqual(invite_form.existing_text,
'The following customers have already received an invite: Caroline Customer.')
@users('survey_manager')
def test_survey_invite_authentication_nosignup(self):
Answer = self.env['survey.user_input']
+1 -2
View File
@@ -74,8 +74,7 @@ class SurveyInvite(models.TransientModel):
@api.depends('partner_ids', 'survey_id')
def _compute_existing_partner_ids(self):
existing_answers = self.survey_id.user_input_ids
self.existing_partner_ids = existing_answers.mapped('partner_id') & self.partner_ids
self.existing_partner_ids = list(set(self.survey_id.user_input_ids.partner_id.ids) & set(self.partner_ids.ids))
@api.depends('emails', 'survey_id')
def _compute_existing_emails(self):
+1 -5
View File
@@ -2152,11 +2152,7 @@ class ReportController(http.Controller):
'message': "Flectra Server Error",
'data': se
}
res = werkzeug.wrappers.Response(
json.dumps(error),
status=500,
headers=[("Content-Type", "application/json")]
)
res = request.make_response(html_escape(json.dumps(error)))
raise werkzeug.exceptions.InternalServerError(response=res) from e
@http.route(['/report/check_wkhtmltopdf'], type='json', auth="user")
+63 -30
View File
@@ -3884,16 +3884,17 @@ See https://github.com/flectra/owl/blob/master/doc/reference/config.md#mode for
continue;
}
}
let isValid;
let whyInvalid;
try {
isValid = isValidProp(props[propName], propsDef[propName]);
whyInvalid = whyInvalidProp(props[propName], propsDef[propName]);
}
catch (e) {
e.message = `Invalid prop '${propName}' in component ${Widget.name} (${e.message})`;
throw e;
}
if (!isValid) {
throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}'`);
if (whyInvalid !== null) {
whyInvalid = whyInvalid.replace(/\${propName}/g, propName);
throw new Error(`Invalid Prop '${propName}' in component '${Widget.name}': ${whyInvalid}`);
}
}
for (let propName in props) {
@@ -3904,11 +3905,11 @@ See https://github.com/flectra/owl/blob/master/doc/reference/config.md#mode for
}
};
/**
* Check if an invidual prop value matches its (static) prop definition
* Check why an invidual prop value doesn't match its (static) prop definition
*/
function isValidProp(prop, propDef) {
function whyInvalidProp(prop, propDef) {
if (propDef === true) {
return true;
return null;
}
if (typeof propDef === "function") {
// Check if a value is constructed by some Constructor. Note that there is a
@@ -3917,46 +3918,70 @@ See https://github.com/flectra/owl/blob/master/doc/reference/config.md#mode for
// So, even though 1 is not an instance of Number, we want to consider that
// it is valid.
if (typeof prop === "object") {
return prop instanceof propDef;
if (prop instanceof propDef) {
return null;
}
return `\${propName} is not an instance of ${propDef.name}`;
}
return typeof prop === propDef.name.toLowerCase();
if (typeof prop === propDef.name.toLowerCase()) {
return null;
}
return `type of \${propName} is not ${propDef.name}`;
}
else if (propDef instanceof Array) {
// If this code is executed, this means that we want to check if a prop
// matches at least one of its descriptor.
let result = false;
let reasons = [];
for (let i = 0, iLen = propDef.length; i < iLen; i++) {
result = result || isValidProp(prop, propDef[i]);
const why = whyInvalidProp(prop, propDef[i]);
if (why === null) {
return null;
}
reasons.push(why);
}
if (reasons.length > 1) {
return reasons.slice(0, -1).join(", ") + " and " + reasons[reasons.length - 1];
}
else {
return reasons[0];
}
return result;
}
// propsDef is an object
if (propDef.optional && prop === undefined) {
return true;
return null;
}
let result = propDef.type ? isValidProp(prop, propDef.type) : true;
if (propDef.validate) {
result = result && propDef.validate(prop);
if (propDef.type) {
const why = whyInvalidProp(prop, propDef.type);
if (why !== null) {
return why;
}
}
if (propDef.validate && !propDef.validate(prop)) {
return "${propName} could not be validated by `validate` function";
}
if (propDef.type === Array && propDef.element) {
for (let i = 0, iLen = prop.length; i < iLen; i++) {
result = result && isValidProp(prop[i], propDef.element);
const why = whyInvalidProp(prop[i], propDef.element);
if (why !== null) {
return why.replace(/\${propName}/g, `\${propName}[${i}]`);
}
}
}
if (propDef.type === Object && propDef.shape) {
const shape = propDef.shape;
for (let key in shape) {
result = result && isValidProp(prop[key], shape[key]);
const why = whyInvalidProp(prop[key], shape[key]);
if (why !== null) {
return why.replace(/\${propName}/g, `\${propName}['${key}']`);
}
}
if (result) {
for (let propName in prop) {
if (!(propName in shape)) {
throw new Error(`unknown prop '${propName}'`);
}
for (let propName in prop) {
if (!(propName in shape)) {
return `unknown prop \${propName}['${propName}']`;
}
}
}
return result;
return null;
}
/**
@@ -4865,12 +4890,20 @@ See https://github.com/flectra/owl/blob/master/doc/reference/config.md#mode for
const __owl__ = Component.current.__owl__;
return {
get el() {
var _a, _b;
const val = __owl__.refs && __owl__.refs[name];
if (val instanceof Component) {
return val.el;
}
if (val instanceof HTMLElement) {
return val;
}
else if (val instanceof Component) {
return val.el;
// Extra check in case the app was created outside an iframe but mounted into one
// on Firefox 109+, the prototype of the element changes to use the iframe window's HTMLElement
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1813499
const ownerWindow = (_b = (_a = val) === null || _a === void 0 ? void 0 : _a.ownerDocument) === null || _b === void 0 ? void 0 : _b.defaultView;
if (ownerWindow && val instanceof ownerWindow.HTMLElement) {
return val;
}
return null;
},
@@ -5568,10 +5601,10 @@ See https://github.com/flectra/owl/blob/master/doc/reference/config.md#mode for
Object.defineProperty(exports, '__esModule', { value: true });
__info__.version = '1.4.10';
__info__.date = '2022-04-27T09:54:49.146Z';
__info__.hash = 'c060490';
__info__.version = '1.4.11';
__info__.date = '2023-01-30T13:09:39.141Z';
__info__.hash = 'a38c534';
__info__.url = 'https://github.com/flectra/owl';
})(this.owl = this.owl || {});
}(this.owl = this.owl || {}));
+13 -4
View File
@@ -247,13 +247,22 @@ class Http(models.AbstractModel):
@classmethod
def _serve_page(cls):
req_page = request.httprequest.path
page_domain = [('url', '=', req_page)] + request.website.website_domain()
published_domain = page_domain
def _search_page(comparator='='):
page_domain = [('url', comparator, req_page)] + request.website.website_domain()
return request.env['website.page'].sudo().search(page_domain, order='website_id asc', limit=1)
# specific page first
page = request.env['website.page'].sudo().search(published_domain, order='website_id asc', limit=1)
page = _search_page()
# redirect withtout trailing /
# case insensitive search
if not page:
page = _search_page('=ilike')
if page:
logger.info("Page %r not found, redirecting to existing page %r", req_page, page.url)
return request.redirect(page.url)
# redirect without trailing /
if not page and req_page != "/" and req_page.endswith("/"):
return request.redirect(req_page[:-1])
+7
View File
@@ -271,3 +271,10 @@ class WithContext(HttpCase):
root_html = html.fromstring(r.content)
canonical_url = root_html.xpath('//link[@rel="canonical"]')[0].attrib['href']
self.assertEqual(canonical_url, website.domain + "/")
def test_page_url_case_insensitive_match(self):
r = self.url_open('/page_1')
self.assertEqual(r.status_code, 200, "Reaching page URL, common case")
r2 = self.url_open('/Page_1', allow_redirects=False)
self.assertEqual(r2.status_code, 302, "URL exists only in different casing, should redirect to it")
self.assertTrue(r2.headers.get('Location').endswith('/page_1'), "Should redirect /Page_1 to /page_1")
@@ -1618,6 +1618,8 @@
<div class="tab-content" id="o_tab_content_spam">
<div class="tab-pane fade show active" data-key="create_uid" id="spam_user" role="tabpanel" aria-labelledby="user-tab">
<form class="row" >
<!-- Prevent the foreach loop to overide the `user` variable from the controller -->
<t t-set="env_user" t-value="user"/>
<div t-foreach="posts_ids.mapped('create_uid')" t-as="user" class="col-6">
<div class="card mb-2">
<div class="card-body py-2">
@@ -1631,6 +1633,7 @@
</div>
</div>
</div>
<t t-set="user" t-value="env_user"/>
</form>
</div>
<div class="tab-pane fade" data-key="country_id" id="spam_country" role="tabpanel" aria-labelledby="country-tab">