[IMP] base,** : make error message user friendly

Before this commit error messages in odoo are boring and
not much attractive to user. Those were like odoo is preventing
them from doing something user want to do.

In this commit, we have modified the message to be a more friendly
and humorous tone, making it less tedious and more enjoyable. It
aims to enhance the user experience and ensure that interactions
with any application are both pleasant and informative. As a result,
the messages have been clear, short, easy to understand and informative.

task-3356114

Part-of: odoo/odoo#124820
Co-authored-by: Kamlesh Pathekar <kpt@odoo.com>
This commit is contained in:
Kartik Chavda
2023-10-10 12:34:56 +00:00
committed by Xavier Bol (xbo)
co-authored by Kamlesh Pathekar
parent 1bc0cea716
commit e3bc974ea4
31 changed files with 165 additions and 146 deletions
+3 -1
View File
@@ -756,7 +756,9 @@ class AccountAccount(models.Model):
if partner_prop_acc:
account_name = partner_prop_acc.get_by_record().display_name
raise UserError(
_('You cannot remove/deactivate the account %s which is set on a customer or vendor.', account_name)
_("You can't delete the account %s, as it is used on a contact.\n\n"
"Imagine that a customer’s receivables evaporate; your CFO might not like it :)"
, account_name)
)
@api.ondelete(at_uninstall=False)
+1 -1
View File
@@ -1563,7 +1563,7 @@ class AccountMoveLine(models.Model):
def _unlink_except_posted(self):
# Prevent deleting lines on posted entries
if not self._context.get('force_delete') and any(m.state == 'posted' for m in self.move_id):
raise UserError(_('You cannot delete an item linked to a posted entry.'))
raise UserError(_("You can't delete a posted journal item. Don’t play Jenga with your accounting records; reset the journal entry to draft before deleting it."))
@api.ondelete(at_uninstall=False)
def _prevent_automatic_line_deletion(self):
+1 -1
View File
@@ -14,7 +14,7 @@ class Lead(models.Model):
def action_generate_leads(self):
return {
"name": _("Generate Leads"),
"name": _("Need help reaching your target?"),
"type": "ir.actions.act_window",
"res_model": "crm.iap.lead.mining.request",
"target": "new",
@@ -47,7 +47,7 @@
</div>
</span>
<h2>What do you need?</h2>
<h2>How many leads would you like?</h2>
<group name="requests">
<group>
+5 -8
View File
@@ -681,8 +681,9 @@ class HolidaysRequest(models.Model):
date_to=conflicting_holiday_data['date_to'],
state=conflicting_holiday_data['state'])
conflicting_holidays_strings.append(conflicting_holidays_string)
raise ValidationError(_('You can not set two time off that overlap on the same day.\nExisting time off:\n%s') %
('\n'.join(conflicting_holidays_strings)))
raise ValidationError(_("You’ve already booked time off which overlaps with this period:" +
"\n".join(conflicting_holidays_strings) +
"Attempting to double-book your time off won't magically make your vacation 2x better!"))
for conflicting_holiday_data in conflicting_holidays_list:
conflicting_holidays_string = _('%(employee_name)s - From %(date_from)s To %(date_to)s - %(state)s',
employee_name=conflicting_holiday_data['employee_name'],
@@ -691,12 +692,8 @@ class HolidaysRequest(models.Model):
state=conflicting_holiday_data['state'])
conflicting_holidays_strings.append(conflicting_holidays_string)
conflicting_employees = set(employee_ids) - set(conflicting_holidays.employee_id.ids)
# Only one employee has a conflicting holiday
if len(conflicting_employees) == len(employee_ids) - 1:
raise ValidationError(_('You can not set two time off that overlap on the same day for the same employee.\nExisting time off:\n%s') %
('\n'.join(conflicting_holidays_strings)))
raise ValidationError(_('You can not set two time off that overlap on the same day for the same employees.\nExisting time off:\n%s') %
('\n'.join(conflicting_holidays_strings)))
raise ValidationError(_("An employee already booked time off which overlaps with this period:" +
"\n".join(conflicting_holidays_strings)))
@api.constrains('date_from', 'date_to', 'employee_id')
def _check_date_state(self):
@@ -61,8 +61,9 @@
<header>
<button name="action_launch" type="object" class="oe_highlight" string="Send"
invisible="state in ('in_queue', 'sending', 'done')" data-hotkey="q"
confirm="This will send the email to all recipients. Are you sure you want to proceed?"
confirm-title="Send Confirmation" confirm-label="Send to all"/>
confirm="Once you send these emails, they'll be barking in all the inboxes like there's no tomorrow!"
confirm-title="Ready to unleash emails?" confirm-label="Send to all"
/>
<button name="action_schedule" type="object" class="btn-secondary" string="Schedule"
invisible="state in ('in_queue', 'sending', 'done')" data-hotkey="v"/>
<button name="action_duplicate" type="object" class="btn-secondary" string="Duplicate"
@@ -492,7 +493,7 @@
<field name="subject"/>
</h3>
<div class="progress border col-3 px-0 mt-2" style="background-color: inherit; height:12px;">
<div class="progress-bar" role="progressbar"
<div class="progress-bar" role="progressbar"
aria-valuemin="0"
t-att-aria-valuenow="record.delivered.raw_value"
t-att-aria-valuemax="record.expected.raw_value"
@@ -519,13 +520,13 @@
</div>
<div invisible="not sent_date" class="d-flex justify-content-between">
<div name="stat_opened">
<b><field name="opened_ratio" />%</b> Opened
<b><field name="opened_ratio" />%</b> Opened
</div>
<div name="stat_replied">
<b><field name="replied_ratio" />%</b> Replied
<b><field name="replied_ratio" />%</b> Replied
</div>
<div name="stat_clicks">
<b><field name="clicks_ratio" />%</b> Clicks
<b><field name="clicks_ratio" />%</b> Clicks
</div>
</div>
</div>
@@ -7,7 +7,7 @@
<form string="Take Future Schedule Date">
<group>
<group>
<field name="schedule_date" string="Send on" required="1"/>
<field name="schedule_date" string="Send on" placeholder="Choose a date" required="1"/>
</group>
</group>
<footer>
+5 -3
View File
@@ -1478,9 +1478,11 @@ class MrpProduction(models.Model):
def button_unplan(self):
if any(wo.state == 'done' for wo in self.workorder_ids):
raise UserError(_("Some work orders are already done, you cannot unplan this manufacturing order."))
raise UserError(_("Some work orders have already been done, so you cannot unplan this manufacturing order.\n\n"
"It’d be a shame to waste all that progress, right?"))
elif any(wo.state == 'progress' for wo in self.workorder_ids):
raise UserError(_("Some work orders have already started, you cannot unplan this manufacturing order."))
raise UserError(_("Some work orders have already been started, so you cannot unplan this manufacturing order.\n\n"
"It’d be a shame to waste all that progress, right?"))
self.workorder_ids.leave_id.unlink()
self.workorder_ids.write({
@@ -2038,7 +2040,7 @@ class MrpProduction(models.Model):
def button_scrap(self):
self.ensure_one()
return {
'name': _('Scrap'),
'name': _('Scrap Products'),
'view_mode': 'form',
'res_model': 'stock.scrap',
'views': [[self.env.ref('stock.stock_scrap_form_view2').id, 'form']],
+1 -1
View File
@@ -697,7 +697,7 @@ class MrpWorkorder(models.Model):
def button_scrap(self):
self.ensure_one()
return {
'name': _('Scrap'),
'name': _('Scrap Products'),
'view_mode': 'form',
'res_model': 'stock.scrap',
'views': [(self.env.ref('stock.stock_scrap_form_view2').id, 'form')],
@@ -27,17 +27,17 @@
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Onboarding Tips</h5>
<h5 class="modal-title">Hide Onboarding Tips</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"/>
</div>
<div class="modal-body">
<p>Are you sure you want to hide these onboarding tips?</p>
<p>Are you sure you want to hide these configuration steps?</p>
</div>
<div class="modal-footer justify-content-start">
<a type="action" class="btn btn-primary" data-bs-dismiss="modal"
<a type="action" class="btn btn-primary me-1" data-bs-dismiss="modal"
data-o-hide-banner="true" t-att-data-model="close_model" t-att-data-method="close_method"
>
Remove
Get that out of my sight!
</a>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
+6 -2
View File
@@ -28,7 +28,9 @@ class ProductTemplate(models.Model):
product_ctx = dict(self.env.context or {}, active_test=False)
if self.with_context(product_ctx).search_count([('id', 'in', self.ids), ('available_in_pos', '=', True)]):
if self.env['pos.session'].sudo().search_count([('state', '!=', 'closed')]):
raise UserError(_('You cannot delete a product saleable in point of sale while a session is still opened.'))
raise UserError(_("To delete a product, make sure all point of sale sessions are closed.\n\n"
"Deleting a product available in a session would be like attempting to snatch a"
"hamburger from a customer’s hand mid-bite; chaos will ensue as ketchup and mayo go flying everywhere!"))
@api.onchange('sale_ok')
def _onchange_sale_ok(self):
@@ -51,7 +53,9 @@ class ProductProduct(models.Model):
product_ctx = dict(self.env.context or {}, active_test=False)
if self.env['pos.session'].sudo().search_count([('state', '!=', 'closed')]):
if self.with_context(product_ctx).search_count([('id', 'in', self.ids), ('product_tmpl_id.available_in_pos', '=', True)]):
raise UserError(_('You cannot delete a product saleable in point of sale while a session is still opened.'))
raise UserError(_("To delete a product, make sure all point of sale sessions are closed.\n\n"
"Deleting a product available in a session would be like attempting to snatch a"
"hamburger from a customer’s hand mid-bite; chaos will ensue as ketchup and mayo go flying everywhere!"))
def get_product_info_pos(self, price, quantity, pos_config_id):
self.ensure_one()
@@ -113,7 +113,7 @@ class PurchaseOrder(models.Model):
if alternative_po_ids:
view = self.env.ref('purchase_requisition.purchase_requisition_alternative_warning_form')
return {
'name': _('Remaining Open Alternatives'),
'name': _("What about the alternative Requests for Quotations?"),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'purchase.requisition.alternative.warning',
@@ -143,7 +143,8 @@ class PurchaseRequisition(models.Model):
Generate all purchase order based on selected lines, should only be called on one agreement at a time
"""
if any(purchase_order.state in ['draft', 'sent', 'to approve'] for purchase_order in self.mapped('purchase_ids')):
raise UserError(_('You have to cancel or validate every RfQ before closing the purchase requisition.'))
raise UserError(_("To close this purchase requisition, cancel related Requests for Quotation.\n\n"
"Imagine the mess if someone confirms these duplicates: double the order, double the trouble :)"))
for requisition in self:
for requisition_line in requisition.line_ids:
requisition_line.supplier_info_ids.sudo().unlink()
+2 -1
View File
@@ -1062,7 +1062,8 @@ class SaleOrderLine(models.Model):
@api.ondelete(at_uninstall=False)
def _unlink_except_confirmed(self):
if self._check_line_unlink():
raise UserError(_("You can not remove an order line once the sales order is confirmed.\nYou should rather set the quantity to 0."))
raise UserError(_("Once a sales order is confirmed, you can't remove one of its lines (we need to track if something gets invoiced or delivered).\n\
Set the quantity to 0 instead."))
#=== ACTION METHODS ===#
+2 -2
View File
@@ -1091,7 +1091,7 @@ class Picking(models.Model):
if not self._should_show_transfers():
if pickings_without_moves:
raise UserError(_('Please add some items to move.'))
raise UserError(_("You can’t validate an empty transfer. Please add some products to move before proceeding."))
if pickings_without_quantities:
raise UserError(self._get_without_quantities_error_message())
if pickings_without_lots:
@@ -1588,7 +1588,7 @@ class Picking(models.Model):
if move.state not in ('draft', 'cancel') and move.product_id.type in ('product', 'consu'):
products |= move.product_id
return {
'name': _('Scrap'),
'name': _('Scrap Products'),
'view_mode': 'form',
'res_model': 'stock.scrap',
'view_id': view.id,
+1 -1
View File
@@ -52,7 +52,7 @@ class StockScrap(models.Model):
('done', 'Done')],
string='Status', default="draft", readonly=True, tracking=True)
date_done = fields.Datetime('Date', readonly=True)
should_replenish = fields.Boolean(string='Replenish Scrapped Quantities')
should_replenish = fields.Boolean(string='Replenish Quantities')
@api.depends('product_id')
def _compute_product_uom_id(self):
+2 -1
View File
@@ -171,6 +171,7 @@
<field name="product_uom_category_id" invisible="1"/>
<field name="product_uom_id" readonly="tracking == 'serial'" groups="uom.group_uom"/>
</div>
<field name="should_replenish"/>
<field name="lot_id" groups="stock.group_production_lot"
context="{'default_company_id': company_id, 'default_product_id': product_id}"
invisible="not product_id or tracking == 'none'"
@@ -188,7 +189,7 @@
</group>
</group>
<footer>
<button name="action_validate" string="Validate" type="object" class="btn-primary" data-hotkey="q"/>
<button name="action_validate" string="Scrap Products" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Discard" class="btn-secondary" special="cancel" data-hotkey="x" />
</footer>
</form>
@@ -6,9 +6,14 @@
<field name="arch" type="xml">
<form string="Replenish wizard">
<p>
Use this assistant to replenish your stock.
Depending on your product configuration, launching a replenishment may trigger a request for quotation,
a manufacturing order or a transfer.
The forecasted quantity of
<field name="product_id"
domain="[('product_tmpl_id', '=', product_tmpl_id)]"
readonly="not product_has_variants"
options="{'no_open': 1, 'no_create': 1}" class="fw-bold"/> is
<field name="forecasted_quantity"/>
<field class="ps-1" name="forecast_uom_id" domain="[('category_id', '=', product_uom_category_id)]" groups="uom.group_uom" readonly="1"/>
<field name="forecast_uom_id" groups="!uom.group_uom" invisible="1"/>
</p>
<group>
<group>
@@ -17,10 +22,6 @@
<field name="product_uom_category_id" invisible="1"/>
<field name="allowed_route_ids" invisible="1"/>
<field name="company_id" invisible="1"/>
<field name="product_id"
domain="[('product_tmpl_id', '=', product_tmpl_id)]"
readonly="not product_has_variants"
options="{'no_open': 1, 'no_create': 1}"/>
<label for="quantity"/>
<div class="o_row">
<field name="quantity" />
@@ -30,12 +31,6 @@
groups="uom.group_uom"
options="{'no_open': 1, 'no_create': 1}"/>
</div>
<label for="forecasted_quantity"/>
<div class="o_row">
<field name="forecasted_quantity" readonly="1"/>
<field name="forecast_uom_id" groups="!uom.group_uom" invisible="1"/>
<field name="forecast_uom_id" domain="[('category_id', '=', product_uom_category_id)]" groups="uom.group_uom" readonly="1"/>
</div>
<field name="date_planned" widget="date"/>
<field name="warehouse_id"
groups="stock.group_stock_multi_warehouses"/>
@@ -57,7 +52,7 @@
</record>
<record id="action_product_replenish" model="ir.actions.act_window">
<field name="name">Replenish</field>
<field name="name">Low on stock? Let's replenish.</field>
<field name="res_model">product.replenish</field>
<!-- binding_model_id evaluated to False
to remove it in existing db's as it was bug-prone -->
@@ -16,7 +16,7 @@ class StockInventoryAdjustmentName(models.TransientModel):
return res
quant_ids = fields.Many2many('stock.quant')
inventory_adjustment_name = fields.Char(default="Quantity Updated")
inventory_adjustment_name = fields.Char(default="Quantity Updated", string="Inventory Reason")
show_info = fields.Boolean('Show warning')
def action_apply(self):
@@ -11,19 +11,19 @@
Some selected lines don't have any quantities set, they will be ignored.
</div>
<group>
<field name="inventory_adjustment_name" string="Inventory Reference / Reason"/>
<field name="inventory_adjustment_name"/>
<field name="show_info" invisible="1"/>
</group>
</div>
<footer>
<button name="action_apply" string="Apply" type="object" class="btn-primary" data-hotkey="q"/>
<button name="action_apply" string="Update Quantities" type="object" class="btn-primary" data-hotkey="q"/>
<button name="cancel_button" string="Discard" class="btn-secondary" special="cancel" data-hotkey="x"/>
</footer>
</form>
</field>
</record>
<record id="action_stock_inventory_adjustement_name" model="ir.actions.act_window">
<field name="name">Inventory Adjustment Reference / Reason</field>
<field name="name">Inventory Adjustment</field>
<field name="res_model">stock.inventory.adjustment.name</field>
<field name="view_mode">form</field>
<field name="view_id" ref="stock_inventory_adjustment_name_form_view"/>
@@ -1,11 +1,15 @@
/** @odoo-module */
import { Dialog } from "../dialog/dialog";
import { _t } from "@web/core/l10n/translation";
import { _t, _lt } from "@web/core/l10n/translation";
import { useChildRef } from "@web/core/utils/hooks";
import { Component } from "@odoo/owl";
export const deleteConfirmationMessage = _lt("Ready to make your record disappear into thin air? Are you sure?\n\
Poof! It will be gone forever!\n\
\nThink twice before you click that 'Delete' button!");
export class ConfirmationDialog extends Component {
setup() {
this.env.dialogData.dismiss = () => this._cancel();
@@ -27,7 +27,7 @@ export const odooExceptionTitleMap = new Map(
"odoo.addons.base.models.ir_mail_server.MailDeliveryException": _t("MailDeliveryException"),
"odoo.exceptions.AccessDenied": _t("Access Denied"),
"odoo.exceptions.MissingError": _t("Missing Record"),
"odoo.exceptions.UserError": _t("User Error"),
"odoo.exceptions.UserError": _t("Invalid Operation"),
"odoo.exceptions.ValidationError": _t("Validation Error"),
"odoo.exceptions.AccessError": _t("Access Error"),
"odoo.exceptions.Warning": _t("Warning"),
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { deleteConfirmationMessage, ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { _t } from "@web/core/l10n/translation";
import { useOwnedDialogs, useService } from "@web/core/utils/hooks";
import { Layout } from "@web/search/layout";
@@ -271,8 +271,8 @@ export class CalendarController extends Component {
}
deleteRecord(record) {
this.displayDialog(ConfirmationDialog, {
title: _t("Confirmation"),
body: _t("Are you sure you want to delete this record?"),
title: _t("Bye-bye, record!"),
body: deleteConfirmationMessage,
confirm: () => {
this.model.unlinkRecord(record.id);
},
@@ -281,6 +281,7 @@ export class CalendarController extends Component {
// `ConfirmationDialog` needs this prop to display the cancel
// button but we do nothing on cancel.
},
cancelLabel: _t("No, keep it"),
});
}
@@ -2,7 +2,7 @@
import { _t } from "@web/core/l10n/translation";
import { hasTouch } from "@web/core/browser/feature_detection";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { deleteConfirmationMessage, ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { makeContext } from "@web/core/context";
import { useDebugCategory } from "@web/core/debug/debug_context";
import { registry } from "@web/core/registry";
@@ -463,7 +463,8 @@ export class FormController extends Component {
get deleteConfirmationDialogProps() {
return {
body: _t("Are you sure you want to delete this record?"),
title: _t("Bye-bye, record!"),
body: deleteConfirmationMessage,
confirm: async () => {
await this.model.root.delete();
if (!this.model.root.resId) {
@@ -472,6 +473,7 @@ export class FormController extends Component {
},
confirmLabel: _t("Delete"),
cancel: () => {},
cancelLabel: _t("No, keep it"),
};
}
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { deleteConfirmationMessage, ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { _t } from "@web/core/l10n/translation";
import { useService } from "@web/core/utils/hooks";
import { CogMenu } from "@web/search/cog_menu/cog_menu";
@@ -201,10 +201,12 @@ export class KanbanController extends Component {
async deleteRecord(record) {
this.dialog.add(ConfirmationDialog, {
body: _t("Are you sure you want to delete this record?"),
title: _t("Bye-bye, record!"),
body: deleteConfirmationMessage,
confirm: () => this.model.root.deleteRecords([record]),
confirmLabel: _t("Delete"),
cancel: () => {},
cancelLabel: _t("No, keep it"),
});
}
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { deleteConfirmationMessage, ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
import { download } from "@web/core/network/download";
import { evaluateExpr, evaluateBooleanExpr } from "@web/core/py_js/py";
import { unique } from "@web/core/utils/arrays";
@@ -529,15 +529,17 @@ export class ListController extends Component {
get deleteConfirmationDialogProps() {
const root = this.model.root;
const body =
root.isDomainSelected || root.selection.length > 1
? _t("Are you sure you want to delete these records?")
: _t("Are you sure you want to delete this record?");
let body = deleteConfirmationMessage;
if (root.isDomainSelected || root.selection.length > 1) {
body = _t("Are you sure you want to delete these records?");
}
return {
title: _t("Bye-bye, record!"),
body,
confirmLabel: _t("Delete"),
confirm: () => this.model.root.deleteRecords(),
cancel: () => {},
cancelLabel: _t("No, keep it"),
};
}
@@ -182,7 +182,7 @@ QUnit.test("WarningDialog", async (assert) => {
},
});
assert.containsOnce(target, ".o_dialog");
assert.strictEqual(target.querySelector("header .modal-title").textContent, "User Error");
assert.strictEqual(target.querySelector("header .modal-title").textContent, "Invalid Operation");
assert.containsOnce(target, ".o_error_dialog");
assert.strictEqual(target.querySelector("main").textContent, "Some strange unreadable message");
assert.strictEqual(target.querySelector(".o_dialog footer button").textContent, "Close");
@@ -938,7 +938,7 @@ QUnit.module("Views", ({ beforeEach }) => {
await click(target, ".o_cw_popover_delete");
assert.strictEqual(
target.querySelector(".modal-title").textContent,
"Confirmation",
"Bye-bye, record!",
"should display the confirm message"
);
@@ -30,10 +30,10 @@
<t t-if="!hasContent and !isFetching">
<div t-if="state.needle" class="o_nocontent_help">
<p class="o_empty_folder_image">No images found.</p>
<p class="o_empty_folder_subtitle">You can upload images with the button located in the top left of the screen.</p>
<p class="o_empty_folder_subtitle">Psst! Want to impress your screen? Upload from the button in the top right corner!</p>
</div>
<div t-else="" class="o_we_search_prompt">
<h2>Get the perfect image by searching in our library of copyright free photos and illustrations.</h2>
<h2>Discover a world of awesomeness in our copyright-free image haven. No legal drama, just nice images!</h2>
</div>
</t>
<t t-else="">
+18 -18
View File
@@ -193,19 +193,22 @@ class IrRule(models.Model):
model = records._name
description = self.env['ir.model']._get(model).name or model
msg_heads = {
# Messages are declared in extenso so they are properly exported in translation terms
'read': _("Due to security restrictions, you are not allowed to access '%(document_kind)s' (%(document_model)s) records.", document_kind=description, document_model=model),
'write': _("Due to security restrictions, you are not allowed to modify '%(document_kind)s' (%(document_model)s) records.", document_kind=description, document_model=model),
'create': _("Due to security restrictions, you are not allowed to create '%(document_kind)s' (%(document_model)s) records.", document_kind=description, document_model=model),
'unlink': _("Due to security restrictions, you are not allowed to delete '%(document_kind)s' (%(document_model)s) records.", document_kind=description, document_model=model)
operations = {
'read': _("read"),
'write': _("write"),
'create': _("create"),
'unlink': _("unlink"),
}
operation_error = msg_heads[operation]
resolution_info = _("Contact your administrator to request access if necessary.")
user_description = f"{self.env.user.name} (id={self.env.user.id})"
operation_error = _("Uh-oh! Looks like you have stumbled upon some top-secret records.\n\n" \
"Sorry, %s doesn't have '%s' access to:", user_description, operations[operation])
failing_model = _("- %s (%s)", description, model)
resolution_info = _("If you really, really need access, go bribe your friendly administrator.")
if not self.user_has_groups('base.group_no_one') or not self.env.user.has_group('base.group_user'):
records.invalidate_recordset()
return AccessError(f"{operation_error}\n\n{resolution_info}")
return AccessError(f"{operation_error}\n{failing_model}\n\n{resolution_info}")
# This extended AccessError is only displayed in debug mode.
# Note that by default, public and portal users do not have
@@ -220,24 +223,21 @@ class IrRule(models.Model):
# If the user has access to the company of the record, add this
# information in the description to help them to change company
if company_related and 'company_id' in rec and rec.company_id in self.env.user.company_ids:
return f'{rec.display_name} (id={rec.id}, company={rec.company_id.display_name})'
return f'{rec.display_name} (id={rec.id})'
return f'{description}, {rec.display_name} ({model}: {rec.id}, company={rec.company_id.display_name})'
return f'{description}, {rec.display_name} ({model}: {rec.id})'
records_description = ', '.join(get_record_description(rec) for rec in records_sudo)
failing_records = _("Records: %s", records_description)
user_description = f'{self.env.user.name} (id={self.env.user.id})'
failing_user = _("User: %s", user_description)
failing_records = '\n '.join(f'- {get_record_description(rec)}' for rec in records_sudo)
rules_description = '\n'.join(f'- {rule.name}' for rule in rules)
failing_rules = _("This restriction is due to the following rules:\n%s", rules_description)
failing_rules = _("Blame the following rules:\n%s", rules_description)
if company_related:
failing_rules += "\n\n" + _('Note: this might be a multi-company issue.')
# clean up the cache of records prefetched with display_name above
records_sudo.invalidate_recordset()
msg = f"{operation_error}\n\n{failing_records}\n{failing_user}\n\n{failing_rules}\n\n{resolution_info}"
msg = f"{operation_error}\n{failing_records}\n\n{failing_rules}\n\n{resolution_info}"
return AccessError(msg)
@@ -198,41 +198,45 @@ class TestIRRuleFeedback(Feedback):
self.record.write({'val': 1})
self.assertEqual(
ctx.exception.args[0],
"""Due to security restrictions, you are not allowed to modify 'Object For Test Access Right' (test_access_right.some_obj) records.
"""Uh-oh! Looks like you have stumbled upon some top-secret records.
Contact your administrator to request access if necessary.""")
Sorry, %s (id=%s) doesn't have 'write' access to:
- %s (%s)
If you really, really need access, go bribe your friendly administrator."""
% (self.user.name, self.user.id, self.record._description, self.record._name))
# debug mode
self.debug_mode()
with self.assertRaises(AccessError) as ctx:
self.record.write({'val': 1})
self.assertEqual(
ctx.exception.args[0],
"""Due to security restrictions, you are not allowed to modify 'Object For Test Access Right' (test_access_right.some_obj) records.
"""Uh-oh! Looks like you have stumbled upon some top-secret records.
Records: %s (id=%s)
User: %s (id=%s)
Sorry, %s (id=%s) doesn't have 'write' access to:
- %s, %s (%s: %s)
This restriction is due to the following rules:
Blame the following rules:
- rule 0
Contact your administrator to request access if necessary.""" % (self.record.display_name, self.record.id, self.user.name, self.user.id)
)
If you really, really need access, go bribe your friendly administrator."""
% (self.user.name, self.user.id, self.record._description, self.record.display_name, self.record._name, self.record.id))
ChildModel = self.env['test_access_right.inherits']
with self.assertRaises(AccessError) as ctx:
ChildModel.with_user(self.user).create({'some_id': self.record.id, 'val': 2})
self.assertEqual(
ctx.exception.args[0],
"""Due to security restrictions, you are not allowed to modify 'Object For Test Access Right' (test_access_right.some_obj) records.
"""Uh-oh! Looks like you have stumbled upon some top-secret records.
Records: %s (id=%s)
User: %s (id=%s)
Sorry, %s (id=%s) doesn't have 'write' access to:
- %s, %s (%s: %s)
This restriction is due to the following rules:
Blame the following rules:
- rule 0
Contact your administrator to request access if necessary.""" % (self.record.display_name, self.record.id, self.user.name, self.user.id)
)
If you really, really need access, go bribe your friendly administrator."""
% (self.user.name, self.user.id, self.record._description, self.record.display_name, self.record._name, self.record.id))
def test_locals(self):
self._make_rule('rule 0', '[("val", "=", 42)]')
@@ -242,17 +246,17 @@ Contact your administrator to request access if necessary.""" % (self.record.dis
self.record.write({'val': 1})
self.assertEqual(
ctx.exception.args[0],
"""Due to security restrictions, you are not allowed to modify 'Object For Test Access Right' (test_access_right.some_obj) records.
"""Uh-oh! Looks like you have stumbled upon some top-secret records.
Records: %s (id=%s)
User: %s (id=%s)
Sorry, %s (id=%s) doesn't have 'write' access to:
- %s, %s (%s: %s)
This restriction is due to the following rules:
Blame the following rules:
- rule 0
- rule 1
Contact your administrator to request access if necessary.""" % (self.record.display_name, self.record.id, self.user.name, self.user.id)
)
If you really, really need access, go bribe your friendly administrator."""
% (self.user.name, self.user.id, self.record._description, self.record.display_name, self.record._name, self.record.id))
def test_globals_all(self):
self._make_rule('rule 0', '[("val", "=", 42)]', global_=True)
@@ -262,17 +266,17 @@ Contact your administrator to request access if necessary.""" % (self.record.dis
self.record.write({'val': 1})
self.assertEqual(
ctx.exception.args[0],
"""Due to security restrictions, you are not allowed to modify 'Object For Test Access Right' (test_access_right.some_obj) records.
"""Uh-oh! Looks like you have stumbled upon some top-secret records.
Records: %s (id=%s)
User: %s (id=%s)
Sorry, %s (id=%s) doesn't have 'write' access to:
- %s, %s (%s: %s)
This restriction is due to the following rules:
Blame the following rules:
- rule 0
- rule 1
Contact your administrator to request access if necessary.""" % (self.record.display_name, self.record.id, self.user.name, self.user.id)
)
If you really, really need access, go bribe your friendly administrator."""
% (self.user.name, self.user.id, self.record._description, self.record.display_name, self.record._name, self.record.id))
def test_globals_any(self):
""" Global rules are AND-eded together, so when an access fails it
@@ -285,16 +289,16 @@ Contact your administrator to request access if necessary.""" % (self.record.dis
self.record.write({'val': 1})
self.assertEqual(
ctx.exception.args[0],
"""Due to security restrictions, you are not allowed to modify 'Object For Test Access Right' (test_access_right.some_obj) records.
"""Uh-oh! Looks like you have stumbled upon some top-secret records.
Records: %s (id=%s)
User: %s (id=%s)
Sorry, %s (id=%s) doesn't have 'write' access to:
- %s, %s (%s: %s)
This restriction is due to the following rules:
Blame the following rules:
- rule 0
Contact your administrator to request access if necessary.""" % (self.record.display_name, self.record.id, self.user.name, self.user.id)
)
If you really, really need access, go bribe your friendly administrator."""
% (self.user.name, self.user.id, self.record._description, self.record.display_name, self.record._name, self.record.id))
def test_combination(self):
self._make_rule('rule 0', '[("val", "=", 42)]', global_=True)
@@ -306,18 +310,18 @@ Contact your administrator to request access if necessary.""" % (self.record.dis
self.record.write({'val': 1})
self.assertEqual(
ctx.exception.args[0],
"""Due to security restrictions, you are not allowed to modify 'Object For Test Access Right' (test_access_right.some_obj) records.
"""Uh-oh! Looks like you have stumbled upon some top-secret records.
Records: %s (id=%s)
User: %s (id=%s)
Sorry, %s (id=%s) doesn't have 'write' access to:
- %s, %s (%s: %s)
This restriction is due to the following rules:
Blame the following rules:
- rule 0
- rule 2
- rule 3
Contact your administrator to request access if necessary.""" % (self.record.display_name, self.record.id, self.user.name, self.user.id)
)
If you really, really need access, go bribe your friendly administrator."""
% (self.user.name, self.user.id, self.record._description, self.record.display_name, self.record._name, self.record.id))
def test_warn_company_no_access(self):
""" If one of the failing rules mentions company_id, add a note that
@@ -331,18 +335,18 @@ Contact your administrator to request access if necessary.""" % (self.record.dis
self.record.write({'val': 1})
self.assertEqual(
ctx.exception.args[0],
"""Due to security restrictions, you are not allowed to modify 'Object For Test Access Right' (test_access_right.some_obj) records.
"""Uh-oh! Looks like you have stumbled upon some top-secret records.
Records: %s (id=%s)
User: %s (id=%s)
Sorry, %s (id=%s) doesn't have 'write' access to:
- %s, %s (%s: %s)
This restriction is due to the following rules:
Blame the following rules:
- rule 0
Note: this might be a multi-company issue.
Contact your administrator to request access if necessary.""" % (self.record.display_name, self.record.id, self.user.name, self.user.id)
)
If you really, really need access, go bribe your friendly administrator."""
% (self.user.name, self.user.id, self.record._description, self.record.display_name, self.record._name, self.record.id))
def test_warn_company_no_company_field(self):
""" If one of the failing rules mentions company_id, add a note that
@@ -365,18 +369,18 @@ Contact your administrator to request access if necessary.""" % (self.record.dis
_ = child_record.parent_id
self.assertEqual(
ctx.exception.args[0],
"""Due to security restrictions, you are not allowed to access 'Object for testing company ir rule' (test_access_right.child) records.
"""Uh-oh! Looks like you have stumbled upon some top-secret records.
Records: %s (id=%s)
User: %s (id=%s)
Sorry, %s (id=%s) doesn't have 'read' access to:
- %s, %s (%s: %s)
This restriction is due to the following rules:
Blame the following rules:
- rule 0
Note: this might be a multi-company issue.
Contact your administrator to request access if necessary.""" % (child_record.display_name, child_record.id, self.user.name, self.user.id)
)
If you really, really need access, go bribe your friendly administrator."""
% (self.user.name, self.user.id, child_record._description, child_record.display_name, child_record._name, child_record.id))
def test_warn_company_access(self):
""" because of prefetching, read() goes through a different codepath
@@ -390,18 +394,18 @@ Contact your administrator to request access if necessary.""" % (child_record.di
_ = self.record.val
self.assertEqual(
ctx.exception.args[0],
"""Due to security restrictions, you are not allowed to access 'Object For Test Access Right' (test_access_right.some_obj) records.
"""Uh-oh! Looks like you have stumbled upon some top-secret records.
Records: %s (id=%s, company=%s)
User: %s (id=%s)
Sorry, %s (id=%s) doesn't have 'read' access to:
- %s, %s (%s: %s, company=%s)
This restriction is due to the following rules:
Blame the following rules:
- rule 0
Note: this might be a multi-company issue.
Contact your administrator to request access if necessary.""" % (self.record.display_name, self.record.id, self.record.sudo().company_id.display_name, self.user.name, self.user.id)
)
If you really, really need access, go bribe your friendly administrator."""
% (self.user.name, self.user.id, self.record._description, self.record.display_name, self.record._name, self.record.id, self.record.sudo().company_id.display_name))
p = self.env['test_access_right.inherits'].create({'some_id': self.record.id})
self.env.flush_all()
self.env.invalidate_all()