Reference #14
This commit is contained in:
parent
574bec5c2b
commit
ebdc1d3ca1
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import controllers
|
||||
from . import models
|
||||
from . import wizard
|
||||
from . import report
|
||||
|
|
@ -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'
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import controllers
|
||||
|
|
@ -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
|
||||
# })
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import sos_brm_action
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
from .import sos_brm_summary_report
|
||||
from .import sos_cross_dept_report
|
||||
|
||||
|
|
@ -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 '—'"/>
|
||||
 | 
|
||||
<strong>Status:</strong> <t t-esc="status"/>
|
||||
<t t-if="department">
|
||||
 | 
|
||||
<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 < 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 <= 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&model=sos_brm_action&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) > 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 < 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 <= 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&model=sos_brm_action&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) > 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>
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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&model=sos_brm_action&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 > 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>
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from .import sos_brm_report_wizard
|
||||
from .import sos_cross_dept_report_wizard
|
||||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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'
|
||||
}
|
||||
)
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue