This commit is contained in:
Deena 2025-09-17 15:06:41 +05:30
parent 574bec5c2b
commit ebdc1d3ca1
21 changed files with 1468 additions and 0 deletions

6
sos_brm/__init__.py Executable file
View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from . import controllers
from . import models
from . import wizard
from . import report

35
sos_brm/__manifest__.py Executable file
View File

@ -0,0 +1,35 @@
# -*- coding: utf-8 -*-
{
'name': "SOS BRM",
'summary': "Business Review",
'description': """
Long description of module's purpose
""",
'author': "Deena",
'website': "https://sosaley.com",
'category': 'Inventory',
'version': '17.0.1.0.0',
'depends': ['base','web','mail','sos_inventory','sos_sales','sos_qo_aod'],
'data': [
'security/ir.model.access.csv',
'security/record_rules.xml',
'views/menu.xml',
'views/sos_brm_action_view.xml',
'wizard/sos_brm_report_wizard_view.xml',
'wizard/sos_cross_dept_report_wizard_view.xml',
'report/sos_brm_report_result.xml',
'report/sos_cross_dept_report_result.xml'
],
'demo': [
'demo/demo.xml',
],
'installable': True,
'application': True,
'license': 'LGPL-3'
}

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import controllers

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# from odoo import http
# class SosBrm(http.Controller):
# @http.route('/sos_brm/sos_brm', auth='public')
# def index(self, **kw):
# return "Hello, world"
# @http.route('/sos_brm/sos_brm/objects', auth='public')
# def list(self, **kw):
# return http.request.render('sos_brm.listing', {
# 'root': '/sos_brm/sos_brm',
# 'objects': http.request.env['sos_brm.sos_brm'].search([]),
# })
# @http.route('/sos_brm/sos_brm/objects/<model("sos_brm.sos_brm"):obj>', auth='public')
# def object(self, obj, **kw):
# return http.request.render('sos_brm.object', {
# 'object': obj
# })

30
sos_brm/demo/demo.xml Executable file
View File

@ -0,0 +1,30 @@
<odoo>
<data>
<!--
<record id="object0" model="sos_brm.sos_brm">
<field name="name">Object 0</field>
<field name="value">0</field>
</record>
<record id="object1" model="sos_brm.sos_brm">
<field name="name">Object 1</field>
<field name="value">10</field>
</record>
<record id="object2" model="sos_brm.sos_brm">
<field name="name">Object 2</field>
<field name="value">20</field>
</record>
<record id="object3" model="sos_brm.sos_brm">
<field name="name">Object 3</field>
<field name="value">30</field>
</record>
<record id="object4" model="sos_brm.sos_brm">
<field name="name">Object 4</field>
<field name="value">40</field>
</record>
-->
</data>
</odoo>

3
sos_brm/models/__init__.py Executable file
View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import sos_brm_action

225
sos_brm/models/sos_brm_action.py Executable file
View File

@ -0,0 +1,225 @@
from odoo import models, fields, api
class sos_brm_action(models.Model):
_name = 'sos_brm_action'
_description = 'Business Review'
todo_id = fields.Char(string="To-Do ID", readonly= True)
name = fields.Char(string="Action Point")
priority = fields.Selection([
('0', '🟢 Low'),
('1', '🟡 Medium'),
('2', '🔴 High'),
('3', '🚨 Urgent')
], string='Priority', default='0')
start_date = fields.Date(string="Start Date",default=fields.Date.today)
target_date = fields.Date(string="Target Date")
end_date = fields.Date(string="Actual end Date")
status = fields.Selection([ ('open', 'Open'),('close', 'Closed'),('hold', 'Hold')], default='open' , string="Status")
line_ids = fields.One2many('sos_brm_action_lines', 'ref_id', string="Action Details",copy=True)
target_date_line_ids = fields.One2many('sos_brm_action_revised_targets', 'ref_id', string="Revised Target Details",copy=True)
result = fields.Html(string="Remarks")
cross_dept_action = fields.Selection(
selection=[
('cross_dept', 'Cross-Dept'),
('inter_dept', 'Intra-Dept')
],
string='Type',
default='inter_dept',
required=True
)
department = fields.Many2one('sos_departments', string='Self Department',required=True, default=lambda self: self._default_department())
responsible_person = fields.Many2one(
'res.users',
string='Assigned To',required=True,
domain="[('id', 'in', allowed_user_ids)]"
)
assigned_by = fields.Many2one(
'res.users',
string='Assigned By',
default=lambda self: self.env.user
)
assigned_from_dept = fields.Many2one('sos_departments', string='Assigned By Dept', default=lambda self: self._default_department())
assigned_to_dept = fields.Many2one('sos_departments', string='Assigned To Dept')
latest_target_date = fields.Date(
string="Latest Revised Target",
compute='_compute_latest_target',
store=True, compute_sudo=True, readonly=True,
)
latest_target_line_id = fields.Many2one(
'sos_brm_action_lines',
string="Latest Target Line",
compute='_compute_latest_target',
store=True, compute_sudo=True, readonly=True,
)
is_sales_user_created = fields.Boolean(
compute='_compute_is_sales_user_created',
store=True,
string='Created by Sales User'
)
allowed_user_ids = fields.Many2many('res.users', compute='_compute_allowed_users')
reporting_to = fields.Many2one('res.users',related="responsible_person.reporting_to", string='Reporting To')
@api.model
def _default_department(self):
dept = self.env['sos_departments'].search(
[('users_line_ids.users', '=', self.env.user.id)],
limit=1
)
if dept:
return dept.id
return False
@api.onchange('department')
def _onchange_department(self):
if self.department:
if self.cross_dept_action == "cross_dept":
self.assigned_from_dept = self.department.id
@api.depends('department','cross_dept_action','assigned_to_dept')
def _compute_allowed_users(self):
for rec in self:
if rec.cross_dept_action == "inter_dept":
rec.allowed_user_ids = rec.department.users_line_ids.mapped('users')
else:
rec.allowed_user_ids = rec.assigned_to_dept.users_line_ids.mapped('users')
@api.depends('create_uid')
def _compute_is_sales_user_created(self):
sales_groups = [
self.env.ref('sos_inventory.sos_sales_user').id,
self.env.ref('sos_inventory.sos_inside_sales_user').id
]
for record in self:
record.is_sales_user_created = any(
gid in record.create_uid.groups_id.ids for gid in sales_groups
)
def _generate_id(self):
sequence_util = self.env['sos_common_scripts']
scope = 'Cross' if self.cross_dept_action == 'cross_dept' else 'Intra'
dept_label = (
getattr(self.department, 'short_form', False)
or getattr(self.department, 'name', 'NoDept')
)
type_id = f"{scope}/{dept_label}"
return sequence_util.generate_sequence('sos_brm_action', type_id, 'todo_id')
@api.depends('line_ids.target_date', 'line_ids.create_date')
def _compute_latest_target(self):
Line = self.env['sos_brm_action_lines']
for rec in self:
latest = Line.search([
('ref_id', '=', rec.id),
('target_date', '!=', False),
], order='target_date desc, id desc', limit=1)
if not latest:
latest = Line.search([('ref_id', '=', rec.id)],
order='create_date desc, id desc', limit=1)
rec.latest_target_line_id = latest.id or False
rec.latest_target_date = latest.target_date or False
def action_revise_target(self):
print("bye")
@api.model
def create(self, vals):
record = super().create(vals)
if record.priority == 0 :
priority_emo='🟢 Low'
elif record.priority == 1 :
priority_emo='🟡 Medium'
elif record.priority == 2 :
priority_emo='🔴 High'
else:
priority_emo='🚨 Urgent'
# Set assigned_by
record.assigned_by = self.env.user.id
# Send email if responsible person exists and is different
responsible_id = record.responsible_person
process_incharge = record.reporting_to
if record.cross_dept_action == "cross_dept":
if process_incharge.id != self.env.user.id:
cc_mail_id = process_incharge.login
else:
cc_mail_id = ""
if responsible_id and responsible_id.id != self.env.user.id:
body_html = f"""
<p>Below <b>Action Plan</b> is assigned to your Department</p>
<p><b>Action Plan : </b> {record.name}</p>
<p><b>Priority : </b> {priority_emo}</p>
<p><b>Assigned By Dept : </b> {record.assigned_from_dept.name}</p>
<p><b>Assigned By : </b> {self.env.user.name}</p>
"""
subject = "Action Plan - Notification"
self.env['sos_common_scripts'].send_direct_email(
self.env,
"sos_brm_action",
record.id,
responsible_id.login,
subject,
body_html,
cc_mail_id
)
else:
if responsible_id and responsible_id.id != self.env.user.id:
body_html = f"""
<p>Below <b>Action Plan</b> is assigned to you</p>
<p><b>Action Plan : </b> {record.name}</p>
<p><b>Priority : </b> {priority_emo}</p>
<p><b>Assigned By : </b> {self.env.user.name}</p>
"""
subject = "Action Plan - Notification"
self.env['sos_common_scripts'].send_direct_email(
self.env,
"sos_brm_action",
record.id,
responsible_id.login,
subject,
body_html
)
# Now department is guaranteed → Generate ID
dept_label = record.department.short_form or record.department.name or 'NoDept'
scope = 'Cross' if record.cross_dept_action == 'cross_dept' else 'Intra'
type_id = f"{scope}/{dept_label}"
record.todo_id = self.env['sos_common_scripts'].generate_sequence('sos_brm_action', type_id, 'todo_id')
return record
def action_assign_action_btn(self):
return {
'name': "Assign Action to Other Department",
'type': 'ir.actions.act_window',
'res_model': 'sos_brm_action',
'view_mode': 'form',
'view_id': self.env.ref('sos_brm.view_form_sos_brm_action_cross_dept').id,
'target': 'new',
'context': {
'from_wizard': True,
'default_cross_dept_action': 'cross_dept'
}
}
class sos_brm_action_lines(models.Model):
_name = 'sos_brm_action_lines'
_description = 'Business Review'
ref_id = fields.Many2one('sos_brm_action', string="BRM action lines", ondelete="cascade")
name = fields.Char(string="Action Plan")
start_date = fields.Date(string="Entry Date")
target_date = fields.Date(string="Revised Target Date")
result = fields.Text(string="Result")
status = fields.Selection([ ('open', 'Open'),('close', 'Closed'),('hold', 'Hold')], default='open' , string="Status")
class sos_revised_targets(models.Model):
_name = 'sos_brm_action_revised_targets'
_description = 'Business Review'
ref_id = fields.Many2one('sos_brm_action', string="BRM action lines", ondelete="cascade")
revised_date = fields.Date(string="Revised Date")

3
sos_brm/report/__init__.py Executable file
View File

@ -0,0 +1,3 @@
from .import sos_brm_summary_report
from .import sos_cross_dept_report

View File

@ -0,0 +1,417 @@
<odoo>
<record id="paperformat_brm_landscape" model="report.paperformat">
<field name="name">BRM Landscape Format</field>
<field name="default" eval="False"/>
<field name="format">A4</field>
<field name="orientation">Landscape</field>
<field name="margin_top">10</field>
<field name="margin_bottom">10</field>
<field name="margin_left">10</field>
<field name="margin_right">10</field>
</record>
<!-- Updated report with paperformat reference -->
<record id="action_brm_summary_report" model="ir.actions.report">
<field name="name">To-Do Reports</field>
<field name="model">sos_brm_action</field>
<field name="report_type">qweb-html</field> <!-- Changed to qweb-pdf for printing -->
<field name="report_name">sos_brm.report_brm_action_plan_summary</field>
<field name="print_report_name">BRM_Plan_Summary</field>
<field name="paperformat_id" ref="paperformat_brm_landscape"/>
</record>
<template id="report_brm_action_plan_summary">
<t t-call="web.basic_layout">
<t t-call="web.html_container">
<!-- Optional: keep your global CSS -->
<link rel="stylesheet" href="/sos_inventory/static/src/css/style.css?v=7"/>
<!-- Polishing CSS just for this report -->
<style>
.subrow td { background:#fff; padding:0; }
.subtable { width:100%; border-collapse:collapse; font-size:12px; margin:4px 0 10px; }
.subtable thead th { background:#f2f3f7; border-bottom:1px solid #e2e4ea; padding:6px; text-align:left; }
.subtable td { padding:6px; border-bottom:1px solid #eee; }
.subtable tr, .subtable { page-break-inside: avoid; }
.status-pill { border-radius:999px; padding:2px 8px; font-size:12px; border:1px solid #ddd; display:inline-block; background-color: #e1375e;
color: #fff;
font-weight: bold; }
.page{
margin-top: 10px;
}
:root {
--ink: #1d1b23;
--muted: #6f6a7d;
--edge: #ece8ff;
--chip: #f4f1ff;
--badge-open: #fbd5ad;
--badge-open-text: #7a4b00;
--badge-close: #e9f7ec;
--badge-close-text: #1e6f3b;
--badge-hold: #f2f2f2;
--badge-hold-text: #606060;
--overdue: #b00020;
--soon: #9a6700;
--ok: #2e7d32;
--head: #eae6ff;
--hover: #f8f7ff;
}
.hdr { display:flex; justify-content:space-between; align-items:flex-start; margin:2px 0 10px; }
.title { margin:0; font-size:18px; color:var(--ink); }
.chips { display:flex; flex-wrap:wrap; gap:8px; margin-top:6px; }
.chip { background:var(--chip); border:1px solid var(--edge); border-radius:999px; padding:4px 10px; font-size:12px; }
.summary-row{width:100%;border-collapse:separate;border-spacing:10px 0;margin:8px 0 12px;}
.summary-row td{box-shadow: rgba(0, 0, 0, 0.12) 0px 2px 6px;width:33.33%;vertical-align:top;padding:10px 12px;border:1px solid var(--edge);background:#fff;border-radius:10px;}
.summary-row .lab{color:var(--muted);font-size:11px;}
.summary-row .val{font-weight:600;font-size:16px;}
@media print {.summary-row{border-spacing:8px 0;}}
.table_custom { width:100%; border-collapse:collapse; font-size:13px; }
.table_custom thead th { background:var(--head); border-bottom:1px solid #cfc8ff; padding:8px; text-align:left; }
.table_custom td { padding:8px; overflow-wrap: anywhere; }
.table_custom tbody tr:hover { background:var(--hover); }
.nowrap { white-space:nowrap; }
.badge { border-radius:999px; padding:2px 8px; font-size:12px; border:1px solid transparent; display:inline-block; }
.b-open { background:var(--badge-open); color:var(--badge-open-text); border-color:#ffe6a8; }
.b-close { background:var(--badge-close); color:var(--badge-close-text); border-color:#bfe8c8; }
.b-hold { background:var(--badge-hold); color:var(--badge-hold-text); border-color:#dedede; }
.date-ok { color:var(--ok); font-weight:600; }
.date-soon { color:var(--soon); font-weight:600; }
.date-overdue { color:var(--overdue); font-weight:700; }
.btn-link { border:1px solid #6a5acd; color:#4b3dbb; padding:4px 10px; border-radius:999px; text-decoration:none; font-size:12px; }
.btn-link:hover { background:#efeaff; }
/* keep rows together on PDF */
.table_custom tr { page-break-inside: avoid; }
h4 { margin:14px 0 6px; }
.section-head { display:flex; align-items:center; gap:10px; }
.pill { background: #202022;
border: 1px solid var(--edge);
border-radius: 999px;
padding: 2px 8px;
font-size: 12px;
color: #ffffff;}
</style>
<div class="page">
<!-- Today helper (remove if you pass 'today' from Python) -->
<t t-set="today" t-value="datetime.date.today()"/>
<!-- Header -->
<div class="hdr">
<h3 class="title">To-Do Summary</h3>
</div>
<div style="font-size:12px; margin-top:4px;">
<strong>Responsible:</strong>
<t t-esc="done_by.name or '—'"/>
&#160;|&#160;
<strong>Status:</strong> <t t-esc="status"/>
<t t-if="department">
&#160;|&#160;
<strong>Department:</strong> <t t-esc="department"/>
</t>
</div>
<!-- KPIs -->
<table class="summary-row">
<tr>
<t t-if="cross_dept_action != 'cross_dept'">
<td>
<div class="lab">Intra-Dept Actions</div>
<div class="val"><t t-esc="count_local"/></div>
</td>
</t>
<t t-if="cross_dept_action != 'inter_dept'">
<td>
<div class="lab">Cross-Dept Actions</div>
<div class="val"><t t-esc="count_cross"/></div>
</td>
</t>
<t t-if="cross_dept_action == 'all'">
<td>
<div class="lab">Total</div>
<div class="val"><t t-esc="count_local + count_cross"/></div>
</td>
</t>
</tr>
</table>
<!-- INTRA-DEPT -->
<t t-if="cross_dept_action != 'cross_dept'">
<div class="section-head">
<h4>Intra-Department Actions</h4>
<span class="pill">Total: <t t-esc="count_local"/></span>
</div>
<table class="table_custom">
<thead>
<tr>
<th>Action Point</th>
<th>Department</th>
<th>Assigned By</th>
<th>Assigned To</th>
<th class="nowrap" style="text-align:center;">Start Date</th>
<th class="nowrap" style="text-align:center;">Target Date</th>
<th class="nowrap" style="text-align:center;">Actual End Date</th>
<!-- <th class="nowrap" style="text-align:center;">Revised Target Date</th> -->
<!-- <th>Remarks</th> -->
<!-- NEW child columns -->
<th>Action Plan</th>
<th>Revised Target Date</th>
<th>Result</th>
<th style="text-align:center;">Status</th>
</tr>
</thead>
<tbody>
<t t-if="local and len(local)">
<t t-foreach="local" t-as="rec">
<t t-set="is_closed" t-value="rec.status == 'close'"/>
<t t-set="is_overdue" t-value="rec.target_date and not is_closed and rec.target_date &lt; today"/>
<t t-set="is_soon" t-value="rec.target_date and not is_closed and not is_overdue and (rec.target_date - today).days &lt;= 7"/>
<!-- fetch child lines and rows to span -->
<t t-set="lines" t-value="lines_map.get(rec.id, [])"/>
<t t-set="rows" t-value="max(1, len(lines))"/>
<!-- first row carries all parent cells (rowspanned) + first child (or dashes) -->
<tr>
<td t-att-rowspan="rows">
<a t-att-href="'/web#id=%d&amp;model=sos_brm_action&amp;view_type=form' % rec.id" target="_blank">
<t t-esc="rec.name or '—'"/>
</a>
</td>
<td t-att-rowspan="rows"><t t-esc="rec.department.name or '—'"/></td>
<td t-att-rowspan="rows">
<div style="display:flex; align-items:center;">
<!-- User Image -->
<img t-att-src="'/web/image/%s/%s/image_1920' % (rec.assigned_by._name, rec.assigned_by.id)"
style="width:24px; height:24px; border-radius:50%; margin-right:5px;"/>
<!-- User Name -->
<t t-esc="rec.assigned_by.name or '—'"/>
</div></td>
<td t-att-rowspan="rows">
<div style="display:flex; align-items:center;">
<!-- User Image -->
<img t-att-src="'/web/image/%s/%s/image_1920' % (rec.responsible_person._name, rec.responsible_person.id)"
style="width:24px; height:24px; border-radius:50%; margin-right:5px;"/>
<!-- User Name -->
<t t-esc="rec.responsible_person.name or '—'"/>
</div>
</td>
<td class="nowrap" style="text-align:center;" t-att-rowspan="rows">
<t t-esc="rec.start_date and rec.start_date.strftime('%d-%m-%Y') or '—'"/>
</td>
<td class="nowrap" style="text-align:center;" t-att-rowspan="rows"
t-attf-class="#{is_overdue and 'date-overdue' or (is_soon and 'date-soon' or 'date-ok')}">
<t t-esc="rec.target_date and rec.target_date.strftime('%d-%m-%Y') or '—'"/>
</td>
<td class="nowrap" style="text-align:center;" t-att-rowspan="rows">
<t t-esc="rec.end_date and rec.end_date.strftime('%d-%m-%Y') or '—'"/>
</td>
<!-- <td class="nowrap" style="text-align:center;" t-att-rowspan="rows">
<t t-esc="rec.latest_target_date and rec.latest_target_date.strftime('%d-%m-%Y') or '—'"/>
</td> -->
<!-- <td style="text-align:center;" t-att-rowspan="rows">
<t t-set="cls" t-value="rec.status == 'open' and 'b-open' or (rec.status == 'close' and 'b-close' or 'b-hold')"/>
<span class="badge" t-attf-class="badge #{cls}">
<t t-esc="dict(rec._fields['status'].selection).get(rec.status, rec.status)"/>
</span>
</td> -->
<!-- <td t-att-rowspan="rows"><t t-esc="rec.result or ''"/></td> -->
<!-- child plan + status (first child or dashes) -->
<td>
<t t-esc="(len(lines) and (lines[0].name or '—')) or '—'"/>
</td>
<td class="nowrap" style="text-align:center;">
<t t-esc="(len(lines) and (lines[0].target_date and lines[0].target_date.strftime('%d-%m-%Y') or '—')) or '—'"/>
</td>
<td>
<t t-esc="(len(lines) and (lines[0].result or '—')) or '—'"/>
</td>
<td style="text-align:center;">
<t t-if="len(lines)">
<span class="status-pill"
t-att-style="'background-color:#2e7d32; border-color:#bfe8c8;' if lines[0].status == 'close' else ''">
<t t-esc="dict(lines[0]._fields['status'].selection).get(lines[0].status, lines[0].status)"/>
</span>
</t>
<t t-else=""></t>
</td>
</tr>
<!-- any remaining child lines as extra rows (only the FOUR child cols) -->
<t t-if="len(lines) &gt; 1">
<t t-foreach="lines[1:]" t-as="ln">
<tr>
<td><t t-esc="ln.name or '—'"/></td>
<td class="nowrap" style="text-align:center;">
<t t-esc="ln.target_date and ln.target_date.strftime('%d-%m-%Y') or '—'"/>
</td>
<td><t t-esc="ln.result or '—'"/></td>
<td style="text-align:center;">
<span class="status-pill"
t-att-style="'background-color:#2e7d32; border-color:#bfe8c8;' if ln.status == 'close' else 'background-color:#e1375e;'">
<t t-esc="dict(ln._fields['status'].selection).get(ln.status, ln.status)"/>
</span>
</td>
</tr>
</t>
</t>
</t>
</t>
<t t-if="not local or len(local) == 0">
<!-- total columns = 8 -->
<tr><td colspan="11" style="text-align:center; color:#666; padding:12px;">No within-department actions.</td></tr>
</t>
</tbody>
</table>
</t>
<div style="height:16px;"></div>
<t t-if="cross_dept_action != 'inter_dept'">
<!-- CROSS-DEPT -->
<div class="section-head">
<h4>Cross-Department Actions</h4>
<span class="pill">Total: <t t-esc="count_cross"/></span>
</div>
<table class="table_custom">
<thead>
<tr>
<th>Action Point</th>
<th>Assigned By Dept</th>
<th>Assigned To Dept</th>
<th>Assigned By</th>
<th>Assigned To</th>
<th class="nowrap" style="text-align:center;">Start Date</th>
<th class="nowrap" style="text-align:center;">Target Date</th>
<th class="nowrap" style="text-align:center;">Actual End Date</th>
<!-- NEW child columns -->
<th>Action Plan</th>
<th>Revised Target Date</th>
<th>Result</th>
<th style="text-align:center;">Status</th>
</tr>
</thead>
<tbody>
<t t-if="cross and len(cross)">
<t t-foreach="cross" t-as="rec">
<t t-set="is_closed" t-value="rec.status == 'close'"/>
<t t-set="is_overdue" t-value="rec.target_date and not is_closed and rec.target_date &lt; today"/>
<t t-set="is_soon" t-value="rec.target_date and not is_closed and not is_overdue and (rec.target_date - today).days &lt;= 7"/>
<t t-set="lines" t-value="lines_map.get(rec.id, [])"/>
<t t-set="rows" t-value="max(1, len(lines))"/>
<tr>
<td t-att-rowspan="rows">
<a t-att-href="'/web#id=%d&amp;model=sos_brm_action&amp;view_type=form' % rec.id" target="_blank">
<t t-esc="rec.name or '—'"/>
</a>
</td>
<td t-att-rowspan="rows"><t t-esc="rec.assigned_from_dept.name or '—'"/></td>
<td t-att-rowspan="rows"><t t-esc="rec.assigned_to_dept.name or '—'"/></td>
<td t-att-rowspan="rows"><div style="display:flex; align-items:center;">
<!-- User Image -->
<img t-att-src="'/web/image/%s/%s/image_1920' % (rec.assigned_by._name, rec.assigned_by.id)"
style="width:24px; height:24px; border-radius:50%; margin-right:5px;"/>
<!-- User Name -->
<t t-esc="rec.assigned_by.name or '—'"/>
</div></td>
<td t-att-rowspan="rows">
<div style="display:flex; align-items:center;">
<!-- User Image -->
<img t-att-src="'/web/image/%s/%s/image_1920' % (rec.responsible_person._name, rec.responsible_person.id)"
style="width:24px; height:24px; border-radius:50%; margin-right:5px;"/>
<!-- User Name -->
<t t-esc="rec.responsible_person.name or '—'"/>
</div>
</td>
<td class="nowrap" style="text-align:center;" t-att-rowspan="rows">
<t t-esc="rec.start_date and rec.start_date.strftime('%d-%m-%Y') or '—'"/>
</td>
<td class="nowrap" style="text-align:center;" t-att-rowspan="rows"
t-attf-class="#{is_overdue and 'date-overdue' or (is_soon and 'date-soon' or 'date-ok')}">
<t t-esc="rec.target_date and rec.target_date.strftime('%d-%m-%Y') or '—'"/>
</td>
<td class="nowrap" style="text-align:center;" t-att-rowspan="rows">
<t t-esc="rec.end_date and rec.end_date.strftime('%d-%m-%Y') or '—'"/>
</td>
<!-- child plan + status (first child or dashes) -->
<td>
<t t-esc="(len(lines) and (lines[0].name or '—')) or '—'"/>
</td>
<td class="nowrap" style="text-align:center;">
<t t-esc="(len(lines) and (lines[0].target_date and lines[0].target_date.strftime('%d-%m-%Y') or '—')) or '—'"/>
</td>
<td>
<t t-esc="(len(lines) and (lines[0].result or '—')) or '—'"/>
</td>
<td style="text-align:center;">
<t t-if="len(lines)">
<span class="status-pill"
t-att-style="'background-color:#2e7d32; border-color:#bfe8c8;' if lines[0].status == 'close' else ''">
<t t-esc="dict(lines[0]._fields['status'].selection).get(lines[0].status, lines[0].status)"/>
</span>
</t>
<t t-else=""></t>
</td>
</tr>
<!-- extra child rows: ONLY the four child columns -->
<t t-if="len(lines) &gt; 1">
<t t-foreach="lines[1:]" t-as="ln">
<tr>
<td><t t-esc="ln.name or '—'"/></td>
<td class="nowrap" style="text-align:center;">
<t t-esc="ln.target_date and ln.target_date.strftime('%d-%m-%Y') or '—'"/>
</td>
<td><t t-esc="ln.result or '—'"/></td>
<td style="text-align:center;">
<span class="status-pill"
t-att-style="'background-color:#2e7d32; border-color:#bfe8c8;' if ln.status == 'close' else 'background-color:#e1375e;'">
<t t-esc="dict(ln._fields['status'].selection).get(ln.status, ln.status)"/>
</span>
</td>
</tr>
</t>
</t>
</t>
</t>
<t t-if="not cross or len(cross) == 0">
<!-- total columns = 12 -->
<tr><td colspan="9" style="text-align:center; color:#666; padding:12px;">No cross-department actions.</td></tr>
</t>
</tbody>
</table>
</t>
</div>
</t>
</t>
</template>
</odoo>

View File

@ -0,0 +1,71 @@
from odoo import models, api
from odoo.exceptions import UserError
from datetime import date, timedelta
from calendar import month_name
from collections import defaultdict
class BRM_WeekSummaryReport(models.AbstractModel):
_name = 'report.sos_brm.report_brm_action_plan_summary'
_description = 'BRM Action Plan Summary Report'
@api.model
def _get_report_values(self, docids, data=None):
if not data:
raise UserError("No filter data provided for the report.")
done_by = data.get('done_by')
status = data.get('status')
department = data.get('department')
department_name = data.get('department_name')
cross_dept_action = data.get('cross_dept_action')
domain = []
if done_by:
domain.append(('responsible_person', '=', int(done_by)))
if department:
domain += [
'|',
('department', '=', department),
('assigned_to_dept', '=', department)
]
if status and status != 'all':
domain.append(('status', '=', status))
if cross_dept_action != "all":
domain.append(('cross_dept_action', '=', cross_dept_action))
# Main actions
docs = self.env['sos_brm_action'].search(domain, order='target_date asc, id asc')
# Subsets
cross = docs.filtered(lambda r: r.cross_dept_action == 'cross_dept')
local = docs.filtered(lambda r: r.cross_dept_action == 'inter_dept')
# Prefetch all child lines once and group by parent id
ActionLines = self.env['sos_brm_action_lines']
lines_map = defaultdict(list) # action_id -> list of line records
if docs:
all_lines = ActionLines.search([
('ref_id', 'in', docs.ids)
], order='ref_id, start_date, id')
for ln in all_lines:
lines_map[ln.ref_id.id].append(ln)
return {
'doc_ids': docs.ids,
'doc_model': 'sos_brm_action',
'done_by': self.env['res.users'].browse(int(done_by)) if done_by else self.env['res.users'],
'status': status or '',
'department': department_name or '',
'docs': docs,
'cross': cross,
'local': local,
'count_cross': len(cross),
'count_local': len(local),
'cross_dept_action':cross_dept_action,
'lines_map': lines_map,
}

View File

@ -0,0 +1,74 @@
from odoo import models, api
from odoo.exceptions import UserError
from datetime import date, timedelta
from calendar import month_name
from collections import defaultdict
class BRM_CrossReport(models.AbstractModel):
_name = 'report.sos_brm.report_cross_dept_summary'
_description = 'Cross Dept Summary Report'
@api.model
def _get_report_values(self, docids, data=None):
if not data:
raise UserError("No filter data provided for the report.")
# Wizard inputs (expect raw IDs / strings, or falsy)
assigned_from_dept = data.get('assigned_from_dept')
assigned_to_dept = data.get('assigned_to_dept')
assigned_by = data.get('assigned_by')
status = data.get('status') or False
# Cross-dept only
domain = [('cross_dept_action', '=', 'cross_dept')]
if assigned_from_dept not in (None, '', 0, '0'):
try:
domain.append(('assigned_from_dept', '=', int(assigned_from_dept)))
except (TypeError, ValueError):
pass
if assigned_to_dept not in (None, '', 0, '0'):
try:
domain.append(('assigned_to_dept', '=', int(assigned_to_dept)))
except (TypeError, ValueError):
pass
if assigned_by not in (None, '', 0, '0'):
try:
domain.append(('assigned_by', '=', int(assigned_by)))
except (TypeError, ValueError):
pass
if status and status != 'all':
domain.append(('status', '=', status))
Action = self.env['sos_brm_action']
ActionLine = self.env['sos_brm_action_lines']
# Main actions
docs = Action.search(domain, order='target_date asc, id asc')
# Prefetch child lines (open only) and group by parent
lines_map = defaultdict(list)
# ensure keys exist for all docs (lets us index lines_map[rec.id] in QWeb)
for rec in docs:
lines_map.setdefault(rec.id, [])
if docs:
all_lines = ActionLine.search(
[('ref_id', 'in', docs.ids)],
order='ref_id, start_date, id'
)
for ln in all_lines:
lines_map[ln.ref_id.id].append(ln)
# Precompute for QWeb (avoid len()/dict() calls there)
count_cross = len(docs)
child_counts = {rid: len(lst) for rid, lst in lines_map.items()}
return {
'doc_ids': docs.ids,
'doc_model': 'sos_brm_action',
'docs': docs,
'status': status or '',
'lines_map': lines_map,
'count_cross': count_cross,
'child_counts': child_counts,
}

View File

@ -0,0 +1,172 @@
<odoo>
<record id="paperformat_brm_landscape" model="report.paperformat">
<field name="name">BRM Landscape Format</field>
<field name="default" eval="False"/>
<field name="format">A4</field>
<field name="orientation">Landscape</field>
<field name="margin_top">10</field>
<field name="margin_bottom">10</field>
<field name="margin_left">10</field>
<field name="margin_right">10</field>
</record>
<!-- Updated report with paperformat reference -->
<record id="action_cross_dept_report" model="ir.actions.report">
<field name="name">BRM Reports</field>
<field name="model">sos_brm_action</field>
<field name="report_type">qweb-html</field> <!-- Changed to qweb-pdf for printing -->
<field name="report_name">sos_brm.report_cross_dept_summary</field>
<field name="print_report_name">Cross Dept Summary</field>
<field name="paperformat_id" ref="paperformat_brm_landscape"/>
</record>
<template id="report_cross_dept_summary">
<t t-call="web.basic_layout">
<t t-call="web.html_container">
<link rel="stylesheet" href="/sos_inventory/static/src/css/style.css?v=7"/>
<style>
.page{ margin-top:10px; }
.hdr { display:flex; justify-content:space-between; align-items:center; margin:2px 0 10px; }
.title { margin:0; font-size:18px; }
.pill { background:#202022; color:#fff; border-radius:999px; padding:2px 8px; font-size:12px; }
.table_custom { width:100%; border-collapse:collapse; font-size:13px; }
.table_custom thead th { background:#eae6ff; border-bottom:1px solid #cfc8ff; padding:8px; text-align:left; }
.table_custom td { padding:8px; overflow-wrap: anywhere; }
.nowrap { white-space:nowrap; }
.status-pill { border-radius:999px; padding:2px 8px; font-size:12px; border:1px solid #ddd; display:inline-block; background-color: #e1375e;
color: #fff;
font-weight: bold; }
</style>
<div class="page">
<div class="hdr">
<h3 class="title">Cross-Department Actions</h3>
<span class="pill">Total: <t t-esc="count_cross"/></span>
</div>
<table class="table_custom">
<thead>
<tr>
<th>Action Point</th>
<th>Assigned By Dept</th>
<th>Assigned To Dept</th>
<th>Assigned By</th>
<th>Assigned To</th>
<th class="nowrap" style="text-align:center;">Start Date</th>
<th class="nowrap" style="text-align:center;">Target Date</th>
<th class="nowrap" style="text-align:center;">Actual End Date</th>
<!-- child columns -->
<th>Action Plan</th>
<th>Revised Target Date</th>
<th>Result</th>
<th style="text-align:center;">Status</th>
</tr>
</thead>
<tbody>
<t t-if="count_cross">
<t t-foreach="docs" t-as="rec">
<!-- No function calls: use dict indexing only -->
<t t-set="lines" t-value="lines_map[rec.id]"/>
<t t-set="rows" t-value="child_counts[rec.id]"/>
<tr>
<td t-att-rowspan="rows or 1">
<a t-att-href="'/web#id=%d&amp;model=sos_brm_action&amp;view_type=form' % rec.id" target="_blank">
<t t-esc="rec.name or '—'"/>
</a>
</td>
<td t-att-rowspan="rows or 1"><t t-esc="rec.assigned_from_dept.name or '—'"/></td>
<td t-att-rowspan="rows or 1"><t t-esc="rec.assigned_to_dept.name or '—'"/></td>
<td t-att-rowspan="rows or 1"><div style="display:flex; align-items:center;">
<!-- User Image -->
<img t-att-src="'/web/image/%s/%s/image_1920' % (rec.assigned_by._name, rec.assigned_by.id)"
style="width:24px; height:24px; border-radius:50%; margin-right:5px;"/>
<!-- User Name -->
<t t-esc="rec.assigned_by.name or '—'"/>
</div></td>
<td t-att-rowspan="rows">
<div style="display:flex; align-items:center;">
<!-- User Image -->
<img t-att-src="'/web/image/%s/%s/image_1920' % (rec.responsible_person._name, rec.responsible_person.id)"
style="width:24px; height:24px; border-radius:50%; margin-right:5px;"/>
<!-- User Name -->
<t t-esc="rec.responsible_person.name or '—'"/>
</div>
</td>
<td class="nowrap" style="text-align:center;" t-att-rowspan="rows or 1">
<t t-esc="rec.start_date and rec.start_date.strftime('%d-%m-%Y') or '—'"/>
</td>
<td class="nowrap" style="text-align:center;" t-att-rowspan="rows or 1">
<t t-esc="rec.target_date and rec.target_date.strftime('%d-%m-%Y') or '—'"/>
</td>
<td class="nowrap" style="text-align:center;" t-att-rowspan="rows or 1">
<t t-esc="rec.end_date and rec.end_date.strftime('%d-%m-%Y') or '—'"/>
</td>
<!-- first child row (or dashes) -->
<td>
<t t-esc="(rows and (lines[0].name or '—')) or '—'"/>
</td>
<td class="nowrap" style="text-align:center;">
<t t-esc="(rows and (lines[0].target_date and lines[0].target_date.strftime('%d-%m-%Y') or '—')) or '—'"/>
</td>
<td>
<t t-esc="(rows and (lines[0].result or '—')) or '—'"/>
</td>
<td style="text-align:center;">
<t t-if="rows">
<span class="status-pill"
t-att-style="'background-color:#2e7d32; border-color:#bfe8c8;' if lines[0].status == 'close' else 'background-color:#e1375e;'">
<t t-esc="dict(lines[0]._fields['status'].selection).get(lines[0].status, lines[0].status)"/>
</span>
</t>
<t t-else=""></t>
</td>
</tr>
<!-- extra child rows -->
<t t-if="rows &gt; 1">
<t t-foreach="lines[1:]" t-as="ln">
<tr>
<td><t t-esc="ln.name or '—'"/></td>
<td class="nowrap" style="text-align:center;">
<t t-esc="ln.target_date and ln.target_date.strftime('%d-%m-%Y') or '—'"/>
</td>
<td><t t-esc="ln.result or '—'"/></td>
<td style="text-align:center;">
<span class="status-pill"
t-att-style="'background-color:#2e7d32; border-color:#bfe8c8;' if ln.status == 'close' else 'background-color:#e1375e;'">
<t t-esc="dict(ln._fields['status'].selection).get(ln.status, ln.status)"/>
</span>
</td>
</tr>
</t>
</t>
</t>
</t>
<t t-else="">
<tr><td colspan="11" style="text-align:center; color:#666; padding:12px;">No cross-department actions.</td></tr>
</t>
</tbody>
</table>
</div>
</t>
</t>
</template>
</odoo>

View File

@ -0,0 +1,7 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_sos_brm_action,sos_brm_action access,model_sos_brm_action,base.group_user,1,1,1,1
access_sos_brm_action_lines,sos_brm_action_lines access,model_sos_brm_action_lines,base.group_user,1,1,1,1
access_sos_brm_action_revised_targets,sos_brm_action_revised_targets access,model_sos_brm_action_revised_targets,base.group_user,1,1,1,1
access_sos_brm_action_report_wizard,sos_brm_action_report_wizard access,model_sos_brm_action_report_wizard,base.group_user,1,1,1,1
access_sos_cross_dept_report_wizard,sos_cross_dept_report_wizard access,model_sos_cross_dept_report_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sos_brm_action sos_brm_action access model_sos_brm_action base.group_user 1 1 1 1
3 access_sos_brm_action_lines sos_brm_action_lines access model_sos_brm_action_lines base.group_user 1 1 1 1
4 access_sos_brm_action_revised_targets sos_brm_action_revised_targets access model_sos_brm_action_revised_targets base.group_user 1 1 1 1
5 access_sos_brm_action_report_wizard sos_brm_action_report_wizard access model_sos_brm_action_report_wizard base.group_user 1 1 1 1
6 access_sos_cross_dept_report_wizard sos_cross_dept_report_wizard access model_sos_cross_dept_report_wizard base.group_user 1 1 1 1

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- FULL control on own/assigned -->
<record id="sos_brm_action_rule_full_own" model="ir.rule">
<field name="name">BRM Action: Full (own/assigned/creator)</field>
<field name="model_id" ref="model_sos_brm_action"/>
<field name="domain_force">[
'|','|','|',
('responsible_person', '=', user.id),
('assigned_by', '=', user.id),
('create_uid', '=', user.id),
('reporting_to', '=', user.id)
]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_unlink" eval="1"/>
<field name="perm_create" eval="0"/>
</record>
<!-- keep a separate unfiltered CREATE rule -->
<record id="sos_brm_action_rule_create_any" model="ir.rule">
<field name="name">BRM Action: Create (any)</field>
<field name="model_id" ref="model_sos_brm_action"/>
<field name="domain_force">[(1,'=',1)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_create" eval="1"/>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_unlink" eval="0"/>
</record>
<!-- CREATE must not be filtered by a domain -->
<record id="sos_brm_action_rule_create_any" model="ir.rule">
<field name="name">BRM Action: Create (any)</field>
<field name="model_id" ref="model_sos_brm_action"/>
<field name="domain_force">[(1,'=',1)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_create" eval="1"/>
<field name="perm_read" eval="0"/>
<field name="perm_write" eval="0"/>
<field name="perm_unlink" eval="0"/>
</record>
<!-- (Optional) Managers/Finance see/edit all -->
<record id="sos_brm_action_rule_full_managers" model="ir.rule">
<field name="name">BRM Action: Full (managers/finance)</field>
<field name="model_id" ref="model_sos_brm_action"/>
<field name="domain_force">[(1,'=',1)]</field>
<field name="groups" eval="[(6, 0, [
ref('sos_inventory.sos_management_user'),
ref('sos_inventory.sos_finance_head_user')
])]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="1"/>
</record>
<record id="sos_brm_action_rule_reviewer_read" model="ir.rule">
<field name="name">BRM Action: Reviewer Read Sales User Created Records</field>
<field name="model_id" ref="model_sos_brm_action"/>
<field name="domain_force">[('is_sales_user_created', '=', True)]</field>
<field name="groups" eval="[(4, ref('sos_inventory.sos_sales_reviewer'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="0"/>
<field name="perm_unlink" eval="0"/>
<field name="perm_create" eval="0"/>
</record>
</odoo>

6
sos_brm/views/menu.xml Executable file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<menuitem id="sos_brm_menu_root" name="To-Do Management (BRM)"/>
<menuitem id="sos_brm_reports_root" sequence="2" name="Reports" parent="sos_brm_menu_root"/>
</odoo>

View File

@ -0,0 +1,155 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="action_brm_action_list" model="ir.actions.act_window">
<field name="name">Actions</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sos_brm_action</field>
<field name="view_mode">tree,form,kanban</field>
</record>
<record id="sos_brm_action_search" model="ir.ui.view">
<field name="name">sos_brm_action.view.search</field>
<field name="model">sos_brm_action</field>
<field name="arch" type="xml">
<search string="Search Actions">
<field name="name" string="Action Point"/>
<filter name="cross_only"
string="Cross Dept Activities"
domain="[('cross_dept_action','=','cross_dept')]"
context="{'show_cross_cols': True}"/>
<searchpanel>
<field name="cross_dept_action" string="Action Type" icon="fa-list-ul" enable_counters="1"/>
<field name="department" string="Department" icon="fa-list-ul" enable_counters="1"/>
</searchpanel>
<field name="responsible_person" string="Assigned To"/>
<field name="assigned_by" string="Assigned By"/>
<field name="department" string="Department"/>
</search>
</field>
</record>
<record id="sos_brm_action_view_tree" model="ir.ui.view">
<field name="name">sos_brm_action.view.tree</field>
<field name="model">sos_brm_action</field>
<field name="arch" type="xml">
<tree>
<!-- <header> <button class="oe_highlight" type="object" name="action_assign_action_btn" display="always" string="Add Cross Dept Activities"></button> </header> -->
<field name="cross_dept_action"/>
<field name="todo_id" invisible="not id"/>
<field name="name"/>
<field name="priority"/>
<field name="assigned_by" widget="many2one_avatar_user"/>
<field name="responsible_person" widget="many2one_avatar_user"/>
<field name="status" widget="badge"
decoration-success="status == 'close'"
decoration-danger="status == 'open'"/>
<!-- Extra columns: shown only when context flag is true -->
<field name="assigned_from_dept"
column_invisible="not context.get('show_cross_cols', False)"/>
<field name="assigned_to_dept"
column_invisible="not context.get('show_cross_cols', False)"/>
<field name="write_uid" string="Last Edited By" optional="hide"/>
<field name="write_date" string="Last Edited On" optional="hide"/>
</tree>
</field>
</record>
<record id="view_form_sos_brm_action_cross_dept" model="ir.ui.view">
<field name="name">Form</field>
<field name="model">sos_brm_action</field>
<field name="priority" eval="90"/>
<field name="arch" type="xml">
<form string="Model Form">
<group>
<group>
<field name="todo_id" invisible="not id"/>
<field name="cross_dept_action" invisible="1"/>
<field name="name"/>
<field name="start_date" invisible="1"/>
<field name="allowed_user_ids" invisible="1"/>
<field name="assigned_to_dept"/>
<field name="result" string="Description"/>
</group>
<group>
<field name="assigned_from_dept"/>
<field name="responsible_person" widget="many2one_avatar_user"/>
</group>
</group>
</form>
</field>
</record>
<record id="sos_brm_action_form_view" model="ir.ui.view">
<field name="name">Form</field>
<field name="model">sos_brm_action</field>
<field name="arch" type="xml">
<form string="Model Form">
<sheet>
<widget name="web_ribbon" text="Open" bg_color="bg-danger" invisible="status == 'close'"/>
<widget name="web_ribbon" text="Closed" bg_color="bg-success" invisible="status == 'open'"/>
<h2 style="text-align: center;text-transform: uppercase;text-shadow: 1px 1p 1px #140718;color: #65407c;padding:5px;">To-Do Management</h2><hr></hr><br></br>
<table class="table" style="box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;">
<tr>
<td><group><field name="cross_dept_action" readonly="id"/></group></td>
</tr>
</table>
<br></br><br></br><br></br>
<group>
<group>
<field name="todo_id" invisible="not id"/>
<field name="name"/>
<field name="priority"/>
<field name="department"/>
<field name="start_date" readonly="id"/>
<field name="target_date"/>
<field name="end_date"/>
</group>
<group>
<field name="status"/>
<field name="cross_dept_action" invisible="1"/>
<field name="assigned_from_dept" invisible="cross_dept_action == 'inter_dept'"/>
<field name="assigned_to_dept" invisible="cross_dept_action == 'inter_dept'"/>
<field name="allowed_user_ids" invisible="1"/>
<field name="responsible_person" widget="many2one_avatar_user"/>
</group>
</group>
<group> <field name="result"/></group>
<br></br><br></br>
<field name="line_ids">
<tree editable="bottom">
<field name="start_date"/>
<field name="name"/>
<field name="target_date"/>
<field name="result"/>
<field name="status"/>
</tree>
</field>
</sheet>
</form>
</field>
</record>
<record id="action_brm_monthly_summary_wizard" model="ir.actions.act_window">
<field name="name">Reports</field>
<field name="res_model">sos_brm_action_report_wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<record id="action_cross_dept_report_wizard" model="ir.actions.act_window">
<field name="name">Reports</field>
<field name="res_model">sos_cross_dept_report_wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem id="sos_brm_action_menu" sequence="1" action="action_brm_action_list" name="Action Plan" parent="sos_brm_menu_root" />
<menuitem id="sos_brm_report_menu" action="action_brm_monthly_summary_wizard" name="To-Do Reports" parent="sos_brm_reports_root" />
<menuitem id="sos_cross_dept_report_menu" action="action_cross_dept_report_wizard" name="Cross-Dept Reports" parent="sos_brm_reports_root" />
<menuitem id="qo_menu"
name="Quality Objectives (QO)"
parent="sos_brm_menu_root" action="sos_qo_aod.action_qo_form_list"/>
</odoo>

2
sos_brm/wizard/__init__.py Executable file
View File

@ -0,0 +1,2 @@
from .import sos_brm_report_wizard
from .import sos_cross_dept_report_wizard

View File

@ -0,0 +1,63 @@
from odoo import models, fields, api
import io
from datetime import date, timedelta
from odoo.exceptions import UserError
class SOS_BRM_Report_Wizard(models.TransientModel):
_name = 'sos_brm_action_report_wizard'
_description = 'BRM Report Wizard'
cross_dept_action = fields.Selection(
selection=[
('all', 'Both'),
('cross_dept', 'Cross-Dept'),
('inter_dept', 'Intra-Dept')
],
string='Type',
default='all'
)
done_by = fields.Many2one(
'res.users',
string='Responsible')
department = fields.Many2one('sos_departments', string='Department')
status = fields.Selection([ ('all', 'All'),('open', 'Open'),('close', 'Closed'),('hold', 'Hold')], default='open' , string="Status")
def generate_report(self):
# Build domain for filtering records
domain = []
if self.done_by:
domain.append(('responsible_person', '=', self.done_by.id))
if self.status != 'all':
domain.append(('status', '=', self.status))
if self.department:
domain.append(('department', '=', self.department.id))
if self.cross_dept_action != "all":
domain.append(('cross_dept_action', '=', self.cross_dept_action))
# Search for records
records = self.env['sos_brm_action'].search(domain)
if not records:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': ('No Records Found'),
'message': ('No Records Found'),
'type': 'warning',
'sticky': False,
}
}
# Generate report with filtered record IDs
return self.env.ref('sos_brm.action_brm_summary_report').report_action(
records.ids,
data={
'done_by' : self.done_by.id,
'department':self.department.id,
'department_name':self.department.name,
'status' : self.status,
'cross_dept_action' : self.cross_dept_action
}
)

View File

@ -0,0 +1,23 @@
<odoo>
<record id="view_sos_brm_action_report_wizard" model="ir.ui.view">
<field name="name">sos_brm_action_report_wizard.form</field>
<field name="model">sos_brm_action_report_wizard</field>
<field name="arch" type="xml">
<form string="Generate Summary Report by Sales Person">
<group>
<field name="cross_dept_action"/>
<field name="department"/>
<field name="done_by"/>
<field name="status"/>
</group>
<footer>
<button type="object" name="generate_report" class="btn-primary"><i class="fa fa-file-text-o"></i> View Report</button>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,56 @@
from odoo import models, fields, api
import io
from datetime import date, timedelta
from odoo.exceptions import UserError
class SOS_BRM_Report_Wizard(models.TransientModel):
_name = 'sos_cross_dept_report_wizard'
_description = 'Cross dept Report Wizard'
assigned_from_dept = fields.Many2one('sos_departments', string='Assigned By')
assigned_to_dept = fields.Many2one('sos_departments', string='Assigned To Dept')
status = fields.Selection([ ('all', 'All'),('open', 'Open'),('close', 'Closed'),('hold', 'Hold')], default='open' , string="Status")
assigned_by = fields.Many2one(
'res.users',
string='Assigned By'
)
def generate_report(self):
# Build domain for filtering records
domain = [('cross_dept_action','=','cross_dept')]
if self.assigned_from_dept:
domain.append(('assigned_from_dept', '=', self.assigned_from_dept.id))
if self.assigned_to_dept:
domain.append(('assigned_to_dept', '=', self.assigned_to_dept.id))
if self.assigned_by:
domain.append(('assigned_by', '=', self.assigned_by.id))
if self.status and self.status != 'all':
domain.append(('status', '=', self.status))
# Search for records
records = self.env['sos_brm_action'].search(domain)
if not records:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': ('No Records Found'),
'message': ('No Records Found'),
'type': 'warning',
'sticky': False,
}
}
# Generate report with filtered record IDs
return self.env.ref('sos_brm.action_cross_dept_report').report_action(
records.ids,
data={
'assigned_from_dept' : self.assigned_from_dept.id,
'assigned_to_dept':self.assigned_to_dept.id,
'assigned_by':self.assigned_by.id,
'status' : self.status,
'cross_dept_action':'cross_dept'
}
)

View File

@ -0,0 +1,23 @@
<odoo>
<record id="view_sos_cross_dept_report_wizard" model="ir.ui.view">
<field name="name">sos_cross_dept_report_wizard.form</field>
<field name="model">sos_cross_dept_report_wizard</field>
<field name="arch" type="xml">
<form>
<group>
<field name="assigned_from_dept" string="Assigned By Department"/>
<field name="assigned_to_dept" string="Assigned To Department"/>
<field name="assigned_by"/>
<field name="status"/>
</group>
<footer>
<button type="object" name="generate_report" class="btn-primary"><i class="fa fa-file-text-o"></i> View Report</button>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
</odoo>