Public release from ruodoo-project: 19.0 - 2026-08-30 21:16:35 UTC

This commit is contained in:
CI Publish Bot
2026-08-30 21:16:45 +00:00
commit 87e77d6515
1335 changed files with 191620 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
import {Component, useState} from "@odoo/owl";
import {Dropdown} from "@web/core/dropdown/dropdown";
import {registry} from "@web/core/registry";
import {useDiscussSystray} from "@mail/utils/common/hooks";
import {useDropdownState} from "@web/core/dropdown/dropdown_hooks";
import {useService} from "@web/core/utils/hooks";
export class TierReviewMenu extends Component {
static components = {Dropdown};
static props = [];
static template = "base_tier_validation.TierReviewMenu";
setup() {
super.setup();
this.discussSystray = useDiscussSystray();
this.orm = useService("orm");
this.store = useState(useService("mail.store"));
this.action = useService("action");
this.dropdown = useDropdownState();
this.fetchSystrayReviewer();
}
async fetchSystrayReviewer() {
const groups = await this.orm.call("res.users", "review_user_count");
let total = 0;
for (const group of groups) {
total += group.pending_count || 0;
}
this.store.tierReviewCounter = total;
this.store.tierReviewGroups = groups;
}
availableViews() {
return [
[false, "kanban"],
[false, "list"],
[false, "form"],
[false, "activity"],
];
}
openReviewGroup(group) {
this.dropdown.close();
const context = {};
const domain = [["can_review", "=", true]];
if (group.active_field) {
domain.push(["active", "in", [true, false]]);
}
const views = this.availableViews();
this.action.doAction(
{
context,
domain,
name: group.name,
res_model: group.model,
search_view_id: [false],
type: "ir.actions.act_window",
views,
},
{
clearBreadcrumbs: true,
}
);
}
}
export const systrayItem = {
Component: TierReviewMenu,
};
registry
.category("systray")
.add("base_tier_validation.ReviewerMenu", systrayItem, {sequence: 99});

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="base_tier_validation.TierReviewMenu">
<Dropdown
position="'bottom-end'"
state="dropdown"
beforeOpen.bind="fetchSystrayReviewer"
menuClass="discussSystray.menuClass"
>
<button>
<i
class="fa fa-lg fa-pencil-square-o"
role="img"
aria-label="Reviews"
/>
<span
t-if="store.tierReviewCounter"
class="o-mail-ActivityMenu-counter badge rounded-pill"
>
<t t-out="store.tierReviewCounter" />
</span>
</button>
<t t-set-slot="content">
<div t-att-class="`${discussSystray.contentClass} o-mail-ActivityMenu`">
<div
t-if="store.tierReviewCounter === 0"
class="o-mail-ActivityMenu-empty align-items-center text-muted p-2 opacity-50 d-flex justify-content-center"
>
<span>No reviews to do.</span>
</div>
<div
class="d-flex flex-column list-group-flush"
name="reviewGroups"
>
<t
t-foreach="store.tierReviewGroups"
t-as="review"
t-key="review_index"
name="reviewGroupLoop"
>
<div
class="o-mail-ActivityGroup list-group-item list-group-item-action d-flex p-2 cursor-pointer"
t-att-data-model_name="review.model"
t-on-click="() => this.openReviewGroup(review)"
>
<img alt="Review" t-att-src="review.icon" />
<div class="flex-grow-1 overflow-hidden">
<div
class="d-flex px-2"
name="reviewTitle"
t-out="review.name"
/>
<div class="d-flex">
<span
t-attf-class="#{review.pending_count ? 'btn btn-link' : 'text-muted'} py-0 px-2 text-truncate"
t-on-click.stop="() => this.openReviewGroup(review)"
>
<t t-out="review.pending_count" /> Pending
</span>
</div>
</div>
</div>
</t>
</div>
</div>
</t>
</Dropdown>
</t>
</templates>

View File

@@ -0,0 +1,53 @@
import {Component, useState} from "@odoo/owl";
import {registry} from "@web/core/registry";
import {useService} from "@web/core/utils/hooks";
export class ReviewsTable extends Component {
setup() {
super.setup();
this.orm = useService("orm");
this.state = useState({
collapse: false,
});
}
_getReviewData() {
const records = this.props.record.data.review_ids.records;
return records.map((record) => record.data);
}
onToggleCollapse(ev) {
const panelHeading = ev.currentTarget.closest(".panel-heading");
const collapseDiv = panelHeading.nextElementSibling.matches("div#collapse1")
? panelHeading.nextElementSibling
: null;
if (!collapseDiv) return;
if (this.state.collapse) {
collapseDiv.style.display = "none";
} else {
collapseDiv.style.display = "block";
}
this.state.collapse = !this.state.collapse;
}
}
ReviewsTable.template = "base_tier_validation.Collapse";
export const reviewsTableComponent = {
component: ReviewsTable,
supportedTypes: ["one2many"],
relatedFields: [
{name: "id", type: "integer"},
{name: "sequence", type: "integer"},
{name: "name", type: "char"},
{name: "display_status", type: "char"},
{name: "todo_by", type: "char"},
{name: "status", type: "char"},
{name: "reviewed_formated_date", type: "char"},
{name: "comment", type: "char"},
{name: "requested_by", type: "many2one", relation: "partner"},
{name: "done_by", type: "many2one", relation: "partner"},
],
};
registry.category("fields").add("form.tier_validation", reviewsTableComponent);

View File

@@ -0,0 +1,41 @@
ul.o_review {
min-width: 600px;
max-width: 800px;
}
.panel-group {
min-height: auto !important;
margin-top: -6px !important;
padding: 16px 16px 8px 16px !important;
}
.panel-heading {
background-color: initial !important;
}
.panel {
border: 0 !important;
}
.panel-body {
overflow-y: hidden;
overflow-x: auto;
}
.panel-title > a,
.panel-title > a:active {
display: block;
}
.panel-heading a:before {
font-family: FontAwesome;
content: "\f0d7";
float: right;
transition: all 0.5s;
}
.panel-heading.active a:before {
-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
transform: rotate(180deg);
}

View File

@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates>
<t t-name="base_tier_validation.Collapse">
<div class="o_form_sheet panel-group">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a
class="o_info_btn"
data-toggle="state.collapse"
href="#"
data-target="#collapse1"
t-on-click="onToggleCollapse"
>
Reviews
</a>
</h4>
</div>
<div id="collapse1" class="panel-collapse collapse active">
<div class="panel-body o_review">
<table class="oe_mt32 table table-condensed">
<thead>
<tr>
<th
name="th_sequence"
class="text-center"
>Sequence</th>
<th
name="th_requested_by"
class="text-start"
>Requested by</th>
<th name="th_name" class="text-end">Description</th>
<th
name="th_display_status"
class="text-end"
>Status</th>
<th name="th_todo_by" class="text-end">Todo by</th>
<th name="th_done_by" class="text-end">Done by</th>
<th
name="th_reviewed_date"
class="text-end"
>Validation Date</th>
<th name="th_comment" class="text-end">Comment</th>
</tr>
</thead>
<tbody class="sale_tbody">
<t
t-foreach="_getReviewData()"
t-as="review"
t-key="review.sequence"
>
<t
t-if="review.status == 'waiting'"
t-set="status_class"
t-value="'text-muted'"
/>
<t
t-if="review.status == 'pending'"
t-set="status_class"
t-value=""
/>
<t
t-if="review.status == 'approved'"
t-set="status_class"
t-value="'alert alert-success'"
/>
<t
t-if="review.status == 'rejected'"
t-set="status_class"
t-value="'alert alert-danger'"
/>
<tr t-att-class="status_class">
<td name="td_sequence" class="text-center">
<span t-out="review.sequence" />
</td>
<td name="td_requested_by" class="text-start">
<span t-out="review.requested_by[1]" />
</td>
<td name="td_name" class="text-end">
<span t-out="review.name" />
</td>
<td name="td_display_status" class="text-end">
<span t-out="review.display_status" />
</td>
<td name="td_todo_by" class="text-end">
<span t-out="review.todo_by" />
</td>
<td name="td_done_by" class="text-end">
<span t-out="review.done_by[1]" />
</td>
<td name="td_reviewed_date" class="text-end">
<t t-if="review.reviewed_formated_date">
<span
t-out="review.reviewed_formated_date"
/>
</t>
</td>
<td name="td_comment" class="text-start">
<t t-if="review.comment">
<span t-out="review.comment" />
</t>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
</templates>

View File

@@ -0,0 +1,33 @@
import {reactive} from "@odoo/owl";
import {registry} from "@web/core/registry";
export class TierReviewService {
constructor(env, services) {
this.env = env;
this.store = services["mail.store"];
this.busService = services.bus_service;
}
setup() {
this.busService.subscribe("base.tier.validation/updated", (payload) => {
if (payload.review_created) {
this.store.tierReviewCounter++;
}
if (payload.review_deleted) {
this.store.tierReviewCounter--;
}
});
this.busService.start();
}
}
export const tierReviewService = {
dependencies: ["bus_service", "mail.store"],
start(env, services) {
const tier_review_service = reactive(new TierReviewService(env, services));
tier_review_service.setup();
return tier_review_service;
},
};
registry.category("services").add("tierReviewService", tierReviewService);