Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions addons/base_automation/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

{
'name': 'Automated Action Rules',
'name': 'Automation Rules',
'version': '1.0',
'category': 'Sales/Sales',
'description': """
This module allows to implement action rules for any object.
============================================================
This module allows to implement automation rules for any object.
================================================================

Use automated actions to automatically trigger actions for various screens.
Use automation rules to automatically trigger actions for various screens.

**Example:** A lead created by a specific user may be automatically set to a specific
Sales Team, or an opportunity which still has status pending after 14 days might
Expand All @@ -23,8 +23,7 @@
],
'assets': {
'web.assets_backend': [
'base_automation/static/src/js/**/*',
'base_automation/static/src/xml/*.xml',
'base_automation/static/src/**/*',
],
'web.qunit_suite_tests': [
'base_automation/static/tests/**/*',
Expand Down
2 changes: 1 addition & 1 deletion addons/base_automation/data/base_automation_data.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<odoo>
<data noupdate="1">
<record id="ir_cron_data_base_automation_check" model="ir.cron">
<field name="name">Base Action Rule: check and execute</field>
<field name="name">Automation Rules: check and execute</field>
<field name="model_id" ref="model_base_automation"/>
<field name="state">code</field>
<field name="code">model._check(True)</field>
Expand Down
603 changes: 386 additions & 217 deletions addons/base_automation/models/base_automation.py

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion addons/base_automation/models/ir_actions_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ class ServerAction(models.Model):
_inherit = "ir.actions.server"

usage = fields.Selection(selection_add=[
('base_automation', 'Automated Action')
('base_automation', 'Automation Rule')
], ondelete={'base_automation': 'cascade'})
base_automation_id = fields.Many2one('base.automation', string='Automation Rule', ondelete='cascade')
56 changes: 56 additions & 0 deletions addons/base_automation/static/src/base_automation.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
.o_base_automation_actions_field,
.o_base_automation_kanban_view {
.o_kanban_ungrouped {
padding: 0;

.o_kanban_record {
width: 100%;
margin: 0;

&.o_kanban_ghost {
display: none;
}

}
@include media-breakpoint-up(md) {
.o_automation_base_info {
width: 25%;
min-width: 200px;
min-height: 90px
};
.o_automation_actions {
display: flex !important;
}
}

}
}

.o_base_automation_actions_field .o_kanban_ghost {
display: none;
}

.o_base_automation_kanban_view {
.o_kanban_grouped .row {
flex-direction: column !important;
gap: 0.5rem !important;

> * {
width: 100% !important;

> * {
margin: 0 0.5rem !important;
}
}
}

.o_kanban_ungrouped .o_kanban_record .oe_kanban_global_click {
border-top: 0;
display: flex;
align-items: center;

.o_widget_web_ribbon {
align-self: flex-start;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/** @odoo-module **/

import { Component, useExternalListener, useEffect, useRef } from "@odoo/owl";
import { _lt, _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { useThrottleForAnimation } from "@web/core/utils/timing";

class ActionsOne2ManyField extends Component {
static props = ["*"];
static template = "base_automation.ActionsOne2ManyField";
static actionStates = {
code: _lt("Execute Python Code"),
object_create: _lt("Create a new Record"),
object_write: _lt("Update the Record"),
multi: _lt("Execute several actions"),
mail_post: _lt("Send email"),
followers: _lt("Add followers"),
remove_followers: _lt("Remove followers"),
next_activity: _lt("Create next activity"),
sms: _lt("Send SMS Text Message"),
};
setup() {
this.root = useRef("root");

let adaptCounter = 0;
useEffect(
() => {
this.adapt();
},
() => [adaptCounter]
);
const throttledRenderAndAdapt = useThrottleForAnimation(() => {
adaptCounter++;
this.render();
});
useExternalListener(window, "resize", throttledRenderAndAdapt);
this.currentActions = this.props.record.data[this.props.name].records;
this.hiddenActionsCount = 0;
}
async adapt() {
// --- Initialize ---
// use getBoundingClientRect to get unrounded width
// of the elements in order to avoid rounding issues
const rootWidth = this.root.el.getBoundingClientRect().width;

// remove all d-none classes (needed to get the real width of the elements)
const actionsEls = Array.from(this.root.el.children).filter((el) => el.dataset.actionId);
actionsEls.forEach((el) => el.classList.remove("d-none"));
const actionsTotalWidth = actionsEls.reduce(
(sum, el) => sum + el.getBoundingClientRect().width,
0
);

// --- Check first overflowing action ---
let overflowingActionId;
if (actionsTotalWidth > rootWidth) {
let width = 56; // for the ellipsis
for (const el of actionsEls) {
const elWidth = el.getBoundingClientRect().width;
if (width + elWidth > rootWidth) {
// All the remaining elements are overflowing
overflowingActionId = el.dataset.actionId;
const firstOverflowingEl = actionsEls.find(
(el) => el.dataset.actionId === overflowingActionId
);
const firstOverflowingIndex = actionsEls.indexOf(firstOverflowingEl);
const overflowingEls = actionsEls.slice(firstOverflowingIndex);
// hide overflowing elements
overflowingEls.forEach((el) => el.classList.add("d-none"));
break;
}
width += elWidth;
}
}

// --- Final rendering ---
const initialHiddenActionsCount = this.hiddenActionsCount;
this.hiddenActionsCount = overflowingActionId
? this.currentActions.length -
this.currentActions.findIndex((action) => action.id === overflowingActionId)
: 0;
if (initialHiddenActionsCount !== this.hiddenActionsCount) {
// Render only if hidden actions count has changed.
return this.render();
}
}
getActionType(action) {
return this.constructor.actionStates[action.data.state] || action.data.state;
}
get moreText() {
const isPlural = this.hiddenActionsCount > 1;
return isPlural ? _t("%s actions", this.hiddenActionsCount) : _t("1 action");
}
}

const actionsOne2ManyField = {
component: ActionsOne2ManyField,
relatedFields: [
{ name: "name", type: "char" },
{
name: "state",
type: "selection",
selection: [
["code", _lt("Execute Python Code")],
["object_create", _lt("Create a new Record")],
["object_write", _lt("Update the Record")],
["multi", _lt("Execute several actions")],
["mail_post", _lt("Send email")],
["followers", _lt("Add followers")],
["remove_followers", _lt("Remove followers")],
["next_activity", _lt("Create next activity")],
["sms", _lt("Send SMS Text Message")],
],
},
// Execute Python Code
{ name: "code", type: "text" },
// Create
{ name: "crud_model_id", type: "many2one" },
{ name: "crud_model_name", type: "char" },
// Add Followers
{ name: "partner_ids", type: "many2many" },
// Message Post / Email
{ name: "template_id", type: "many2one" },
{ name: "mail_post_autofollow", type: "boolean" },
{
name: "mail_post_method",
type: "selection",
selection: [
["email", _lt("Email")],
["comment", _lt("Post as Message")],
["note", _lt("Post as Note")],
],
},
// Schedule Next Activity
{ name: "activity_type_id", type: "many2one" },
{ name: "activity_summary", type: "char" },
{ name: "activity_note", type: "html" },
{ name: "activity_date_deadline_range", type: "integer" },
{
name: "activity_date_deadline_range_type",
type: "selection",
selection: [
["days", _lt("Days")],
["weeks", _lt("Weeks")],
["months", _lt("Months")],
],
},
{
name: "activity_user_type",
type: "selection",
selection: [
["specific", _lt("Specific User")],
["generic", _lt("Generic User")],
],
},
{ name: "activity_user_id", type: "many2one" },
{ name: "activity_user_field_name", type: "char" },
],
};

registry.category("fields").add("base_automation_actions_one2many", actionsOne2ManyField);
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>

<templates>
<t t-name="base_automation.ActionsOne2ManyField" owl="1">
<div class="d-flex align-items-center" t-ref="root">
<t t-if="currentActions.length === 0">
<span class="text-muted">no action defined...</span>
</t>
<t t-foreach="currentActions" t-as="action" t-key="action.id">
<div style="min-width: fit-content;" t-att-data-action-id="action.id">
<div class="fs-5 d-flex align-items-center">
<i
data-name="server_action_icon"
t-att-title="getActionType(action)"
class="fa"
t-att-class="{
'code': 'fa-file-code-o',
'object_create': 'fa-edit',
'object_write': 'fa-refresh',
'multi': 'fa-list-ul',
'mail_post': 'fa-envelope',
'followers': 'fa-user-o',
'remove_followers': 'fa-user-times',
'next_activity': 'fa-clock-o',
'sms': 'fa-comments-o',
}[action.data.state]"
/>
<div class="ps-2" t-esc="action.data.name" />
</div>
</div>
<div t-if="!action_last" class="px-3 align-self-center" t-att-data-action-id="action.id">
<i class="fa fa-lg fa-plus text-primary"></i>
</div>
</t>
<t t-if="hiddenActionsCount">
<div class="fs-3 align-self-center text-muted">
<t t-out="moreText"/>
</div>
</t>
</div>
</t>
</templates>
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ export class BaseAutomationErrorDialog extends RPCErrorDialog {
setup() {
super.setup(...arguments);
const { id, name } = this.props.data.context.base_automation;
this.actionId = id;
this.actionName = name;
this.automationId = id;
this.automationName = name;
this.isUserAdmin = useService("user").isAdmin;
this.actionService = useService("action");
this.orm = useService("orm");
Expand All @@ -20,32 +20,30 @@ export class BaseAutomationErrorDialog extends RPCErrorDialog {
//--------------------------------------------------------------------------

/**
* This method is called when the user clicks on the 'Disable action' button
* displayed when a crash occurs in the evaluation of an automated action.
* Then, we write `active` to `False` on the automated action to disable it.
* This method is called when the user clicks on the 'Disable Automation Rule' button
* displayed when a crash occurs in the evaluation of an automation rule.
* Then, we write `active` to `False` on the automation rule to disable it.
*
* @private
* @param {MouseEvent} ev
*/
async disableAction(ev) {
await this.orm.write("base.automation", [this.actionId], {
active: false,
});
async disableAutomation(ev) {
await this.orm.write("base.automation", [this.automationId], { active: false });
this.props.close();
}
/**
* This method is called when the user clicks on the 'Edit action' button
* displayed when a crash occurs in the evaluation of an automated action.
* Then, we redirect the user to the automated action form.
* displayed when a crash occurs in the evaluation of an automation rule.
* Then, we redirect the user to the automation rule form.
*
* @private
* @param {MouseEvent} ev
*/
editAction(ev) {
editAutomation(ev) {
this.actionService.doAction({
name: "Automated Actions",
name: "Automation Rules",
res_model: "base.automation",
res_id: this.actionId,
res_id: this.automationId,
views: [[false, "form"]],
type: "ir.actions.act_window",
view_mode: "form",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,29 @@
<t t-name="base_automation.ErrorDialog" t-inherit="web.ErrorDialog" t-inherit-mode="primary">
<xpath expr="//div[@role='alert']" position="inside">
<p>
The error occurred during the execution of the automated action
The error occurred during the execution of the automation rule
"<t t-esc="actionName"/>"
(ID: <t t-esc="actionId"/>).
<br/>
</p>
<p t-if="isUserAdmin">
You can disable this automated action or edit it to solve the issue.<br/>
Disabling this automated action will enable you to continue your workflow
You can disable this automation rule or edit it to solve the issue.<br/>
Disabling this automation rule will enable you to continue your workflow
but any data created after this could potentially be corrupted,
as you are effectively disabling a customization that may set
important and/or required fields.
</p>
<p t-else="">
You can ask an administrator to disable or correct this automated action.
You can ask an administrator to disable or correct this automation rule.
</p>
</xpath>
<xpath expr="//div[@role='alert']//button" position="after">
<t t-if="isUserAdmin">
<button class="btn btn-secondary mt4 o_disable_action_button" t-on-click.prevent="disableAction">
<i class="fa fa-ban mr8"/>Disable Action
<button class="btn btn-secondary mt4 o_disable_action_button me-3" t-on-click.prevent="disableAction">
<i class="fa fa-ban mr8"/>Disable Automation Rule
</button>
<button class="btn btn-secondary mt4 o_edit_action_button" t-on-click.prevent="editAction">
<i class="fa fa-edit mr8"/>Edit action
<i class="fa fa-edit mr8"/>Edit Automation Rule
</button>
</t>
</xpath>
Expand Down
Loading