mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-08-17 16:54:42 -05:00
[PATCH] Upstream patch - 01022023
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
@@ -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/',
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 || {}));
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user