Compare commits

...

2 Commits

Author SHA1 Message Date
Deena ceb5e58e3e Reference #11 2025-09-17 15:08:29 +05:30
Deena ebdc1d3ca1 Reference #14 2025-09-17 15:06:41 +05:30
111 changed files with 3862 additions and 460 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>

View File

@ -11,7 +11,7 @@
'version': '17.0.1.0.0',
# any module necessary for this one to work correctly
'depends': ['base','web','mail'],
'depends': ['base','web','mail','one2many_search_widget'],
# always loaded
'data': [
@ -19,6 +19,7 @@
'security/record_rules.xml',
'security/ir.model.access.csv',
'data/cron_jobs.xml',
'views/pareto_chart_widget_view.xml',
'views/sos_audit_log_view.xml',
'views/menu.xml',
'views/sos_quote_generation.xml',
@ -26,6 +27,7 @@
'views/sos_fg_view.xml',
'views/sos_sfg_view.xml',
'views/sos_material_view.xml',
'views/sos_budget_plan_view.xml',
'views/sos_material_bom_view.xml',
'views/sos_sfg_bom_view.xml',
'views/sos_fg_bom_view.xml',
@ -81,6 +83,8 @@
'views/sos_service_call_log_report.xml',
'views/sos_transfer_challan_return_from_customer_view.xml',
'views/sos_shelflife_register_view.xml',
'views/sos_prf_view.xml',
'wizard/sfg_bom_bulk_upload_view.xml',
'wizard/mon_bulk_upload_view.xml',
'wizard/missing_component_wizard.xml',
@ -115,6 +119,7 @@
'report/sos_boq_labels.xml',
'report/shelflife_report.xml',
'report/sos_boq_report.xml',
'report/sos_budget_plan_summary.xml',
'data/send_indent_plan_email_template.xml',
'data/selection_item.xml'
@ -130,6 +135,8 @@
'sos_inventory/static/src/components/**/*.js',
'sos_inventory/static/src/components/**/*.xml',
'sos_inventory/static/src/components/**/*.scss',
'sos_inventory/static/src/js/pareto_chart_widget.js',
'sos_inventory/static/src/xml/pareto_chart_template.xml',

View File

@ -4,7 +4,9 @@ import io
import xlsxwriter
class MaterialBackupExportController(http.Controller):
@http.route('/supplier/form', auth='public')
def index(self, **kw):
return "Hello, world"
@http.route(['/download/material/backup/<string:year>/<string:item_type>'], type='http', auth='user')
def download_backup_by_year(self, year, item_type, **kwargs):
# Safely map table names

View File

@ -59,3 +59,5 @@ from . import sos_service_call_log_report
from . import sos_inhouse_validation_reports_files
from . import sos_transfer_challan_return_from_customer
from . import sos_shelflife_register
from . import sos_budget_plan
from . import sos_prf

View File

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.exceptions import UserError
from math import ceil
class SOS_Budget_Plan(models.Model):
_name = 'sos_budget_plan'
_description = 'Budget Plan'
name= fields.Char(string="Plan Name")
sfg_option = fields.Boolean('Semi-Finished Goods')
fg_option = fields.Boolean('Finished Goods',default=True)
fg_name = fields.Many2one('sos_fg', string='FG Name')
sfg_name = fields.Many2one('sos_sfg', string="SFG Name")
quantity = fields.Integer(string="Quantity",required=True)
@api.onchange('sfg_option')
def _onchange_sfg_option(self):
if self.sfg_option:
self.fg_option = False
@api.onchange('fg_option')
def _onchange_fg_option(self):
if self.fg_option:
self.sfg_option = False
def create_budget_plan(self):
self.ensure_one()
if not self.quantity:
raise UserError("Quantity must be greater than zero.")
if not (self.fg_option or getattr(self, 'sfg_option', False)):
raise UserError("Select Finished Goods or SFG option.")
# --- Accumulator keyed by product/component id ---
# { prod_id: {'name': str, 'needed': float, 'inhand': float, 'price': float} }
agg = {}
def _accumulate(line, needed_qty):
"""Accumulate needed qty for a line's primary component."""
prod = line.primary_component_id
if not prod:
return
pid = prod.id
rec = agg.get(pid)
if rec:
rec['needed'] += float(needed_qty or 0.0)
else:
agg[pid] = {
'name': prod.part_no or prod.name or '',
'needed': float(needed_qty or 0.0),
'inhand': float(prod.inhand_stock_qty or 0.0), # take once per product
'price': float(prod.unit_price or 0.0),
'pack': float(getattr(prod, 'std_packing_qty', 0.0) or 0.0),
}
label_name = "" # what we'll display in the report header
if self.fg_option:
# -------- FG → explode SFGs + add direct materials --------
product_bom = self.env['sos_fg_bom'].search([
('fg_name', '=', self.fg_name.id),
('is_primary', '=', True)
], limit=1)
if not product_bom:
raise UserError("BOM for product is Missing")
label_name = self.fg_name.display_name
# 1) FG BOM lines that reference SFG BOMs
fg_sfg_lines = self.env['sos_fg_bom_line'].search([('bom_id', '=', product_bom.id)])
for sfg_line in fg_sfg_lines:
sfg_bom = sfg_line.sfg_bom_id # Many2one to SFG BOM
if not sfg_bom:
continue
# Required SFG qty = (FG qty * SFG-per-FG) - SFG in-hand (clamped ≥ 0)
inhand_sfg = float((sfg_bom.name and sfg_bom.name.inhand_stock_qty) or 0.0)
requested = float(self.quantity or 0.0) * float(sfg_line.quantity or 0.0)
required_sfg_qty = max(requested - inhand_sfg, 0.0)
if not required_sfg_qty:
continue
# Explode SFG BOM to components for the required SFG qty
for comp in sfg_bom.sfg_bom_line_ids:
needed_qty = float(comp.quantity or 0.0) * required_sfg_qty
_accumulate(comp, needed_qty)
# 2) Direct materials under the FG BOM
direct_material_lines = self.env['sos_sfg_bom_line'].search([('fg_bom_id', '=', product_bom.id)])
for mat in direct_material_lines:
needed_qty = float(mat.quantity or 0.0) * float(self.quantity or 0.0)
_accumulate(mat, needed_qty)
else:
# -------- SFG-only path --------
if not getattr(self, 'sfg_option', False):
raise UserError("SFG option must be selected.")
sfg_bom = self.env['sos_sfg_bom'].search([('name', '=', self.sfg_name.id)], limit=1)
if not sfg_bom:
raise UserError("SFG BOM is missing.")
label_name = self.sfg_name.display_name
# Required SFG qty = requested - in-hand (clamped ≥ 0)
inhand_sfg = float((sfg_bom.name and sfg_bom.name.inhand_stock_qty) or 0.0)
requested = float(self.quantity or 0.0)
required_sfg_qty = max(requested - inhand_sfg, 0.0)
if required_sfg_qty:
for comp in sfg_bom.sfg_bom_line_ids:
needed_qty = float(comp.quantity or 0.0) * required_sfg_qty
_accumulate(comp, needed_qty)
# ---- Build final list from aggregator (merge duplicates; consider inhand once) ----
materials = []
for rec in agg.values():
raw_to_purchase = max(rec['needed'] - rec['inhand'], 0.0)
if raw_to_purchase <= 0:
continue
pack = float(getattr(rec, 'pack', 0.0) or rec.get('pack') or 0.0)
if pack > 0:
pack_count = ceil(raw_to_purchase / pack)
to_purchase = pack_count * pack
else:
pack_count = 0
to_purchase = raw_to_purchase
materials.append({
'material_name': rec['name'],
'needed_quantity': rec['needed'],
'inhand_quantity': rec['inhand'],
'actual_needed': raw_to_purchase,
'std_packing_qty': pack,
'packs_to_buy': pack_count,
'to_purchase': to_purchase,
'price': rec['price'],
'cost': round(to_purchase * rec['price'], 2),
})
if not materials:
raise UserError("No materials need to be purchased.")
# Optional ordering: most to purchase first
materials.sort(key=lambda r: (-r['to_purchase'], r['material_name']))
total_cost = sum(m['cost'] for m in materials)
action = self.env.ref('sos_inventory.action_material_budget_summary')
return action.report_action(
self,
data={
'fg_name': label_name, # shown as header label
'total_quantity': self.quantity,
'materials': materials,
'total_cost': total_cost,
'currency_symbol': self.env.company.currency_id.symbol or '',
}
)

View File

@ -41,7 +41,7 @@ class SOS_CCRF(models.Model):
batch_release_date = fields.Date(string="Invoice Date")
brcoa_no = fields.Char(string="BRCOA No")
brcoa_date = fields.Date(string="BRCOA Date")
other_complaints = fields.Text(string="Details of any other Complaints received from this Production run")
other_complaints = fields.Text(string="Defective Details/Symptom")
data_collected_by = fields.Many2one('res.users', string='Data Collected By')
data_collected_image = fields.Image(related="data_collected_by.signature_image",string='Data Collected Sign',readonly=True)
data_collected_on = fields.Datetime(string="Data Collected On")
@ -56,12 +56,17 @@ class SOS_CCRF(models.Model):
investigation_carriedout_image = fields.Image(related="investigation_carriedout_by.signature_image",string='Investigation Carried Out Sign',readonly=True)
investigation_carriedout_on = fields.Datetime(string="Investigation Carried Out On")
root_cause = fields.Html(string="Root Cause",
default="<p>Why #1</p><p>Why #2</p><p>Why #3</p>")
default="<p>Why #1 :</p><p>Why #2 :</p><p>Why #3 :</p><p>Why #4 :</p><p>Why #5 :</p>")
#team_formation = fields.Html(string="Team Formation",
#default="<p>Name :</p><br><p>Department :</p><br><p>Roles :</p>")
#problem_description = fields.Html(string="Problem Description",
#default="<p>Clear Statement :</p><br><p>Photo :</p><br><p>What :</p><br><p>Where :</p><br><p>When :</p><br><p>Why :</p><br><p>Who :</p><br><p>How :</p><br><p>How many :</p>")
rootcause_carriedout_by = fields.Many2one('res.users', string='Root Cause Carried Out By')
rootcause_carriedout_image = fields.Image(related="rootcause_carriedout_by.signature_image",string='Root Cause Carried Out Sign',readonly=True)
rootcause_carriedout_on = fields.Datetime(string="Root Cause Carried Out On")
corrective_action = fields.Html(string="Corrective Action")
preventive_action = fields.Html(string="Preventive Action")
corrective_action = fields.Html(string="D5:Permanent Corrective Action")
implement_validate_corrective_action = fields.Html(string="D6:Implement & Validate Corrective Actions")
preventive_action = fields.Html(string="D7:Preventive Recurrence")
capa_status = fields.Selection([ ('open', 'OPEN'),('close', 'Close')], default='open' , string="Status")
status_reviewed_by = fields.Many2one('res.users', string='Status Reviewed By')
status_reviewed_image = fields.Image(related="status_reviewed_by.signature_image",string='Status Reviewed Out Sign',readonly=True)
@ -76,6 +81,11 @@ class SOS_CCRF(models.Model):
rootcause_verifiedout_image = fields.Image(related="rootcause_verifiedout_by.signature_image",string='Root Cause Verified By Sign',readonly=True)
rootcause_verifiedout_on = fields.Datetime(string="Root Cause Verified On")
helper_field = fields.Many2many('sos_fg', string="Helper Field")
team_recognition = fields.Html(string="Team Recognition (Image)")
capa_line_ids = fields.One2many('sos_ccrf_capa_line', 'ccrf_id', string="CAPA Line Ids",copy=True)
team_formation_line_ids = fields.One2many('sos_ccrf_team_formation_line', 'ccrf_id', string="Team Formation Line Ids",copy=True)
problem_description_line_ids = fields.One2many('sos_ccrf_problem_description_line', 'ccrf_id', string="Problem Description Line Ids",copy=True)
@api.onchange('fg_name')
def _onchange_fg_name(self):
if self.fg_name:
@ -141,3 +151,37 @@ class SOS_CCRF(models.Model):
'rootcause_verifiedout_on'
)
class CCRF_Model_CAPA_Line(models.Model):
_name = 'sos_ccrf_capa_line'
_description = 'CAPA Lines'
ccrf_id = fields.Many2one('sos_ccrf', string="CCRF Reference", ondelete="cascade")
issue = fields.Char(string="Issue")
corrective_action = fields.Html(string="D5:Permanent Corrective Action")
implement_validate_corrective_action = fields.Html(string="D6:Implement & Validate Corrective Actions")
preventive_action = fields.Html(string="D7:Preventive Recurrence")
class CCRF_Model_TEAM_FORMATION_Line(models.Model):
_name = 'sos_ccrf_team_formation_line'
_description = 'Team Formation Lines'
ccrf_id = fields.Many2one('sos_ccrf', string="CCRF Reference", ondelete="cascade")
name = fields.Char(string="Name")
department = fields.Char(string="Department")
role = fields.Char(string="Role")
class CCRF_Model_PROBLEM_DESCRIPTION_Line(models.Model):
_name = 'sos_ccrf_problem_description_line'
_description = 'Problem Description Lines'
ccrf_id = fields.Many2one('sos_ccrf', string="CCRF Reference", ondelete="cascade")
clear_statement = fields.Text(string="Clear Statement")
photos = fields.Html(string="Photos")
what = fields.Text(string="What")
where = fields.Text(string="Where")
when = fields.Text(string="When")
why = fields.Text(string="Why")
who = fields.Text(string="Who")
how = fields.Text(string="How")
how_many = fields.Text(string="How many")

View File

@ -288,7 +288,7 @@ class Sequence_Generator(models.AbstractModel):
reporting_user = env['res.users'].search([('id', '=', users.reporting_to.id)])
email_to = reporting_user.login
else:
email_to = "ramachandran.r@sosaley.in"
email_to = "ramachandran.r@sosaley.com"
mail_values = {
'subject': subject,
'body_html': body_html,

View File

@ -60,7 +60,8 @@ class sos__dc(models.Model):
dept_in_charge_image = fields.Image(related="dept_in_charge_name.signature_image",string='Department In-Charge Sign',readonly=True)
dept_in_charge_approved_on = fields.Datetime(string="Approved On")
remarks = fields.Text(string="Remarks")
courier = fields.Char(string="Courier Name")
lr_no = fields.Char(string="LR No")
@api.onchange('dock_audit_no')
def _onchange_dock_audit_no(self):
if self.dock_audit_no:
@ -220,7 +221,7 @@ class sos__dc(models.Model):
"""
subject = f"Delivery Challan Approval Request - {self.dc_no}"
send_email = self.env['sos_common_scripts']
send_email.send_direct_email(self.env,"sos_dc",self.id,"ramachandran.r@sosaley.in",subject,body_html)
send_email.send_direct_email(self.env,"sos_dc",self.id,"ramachandran.r@sosaley.com",subject,body_html)
# Email part ends
sequence_util = self.env['sos_common_scripts']
sequence_util.action_assign_signature(

View File

@ -22,7 +22,12 @@ class SosTestingParameters(models.Model):
communication_type = fields.Selection([
('wired', 'Wired'),
('wireless', 'Wireless')
], string="Communication Type", default='wired')
], string="Communication Type")
slave_type = fields.Selection([
('15S Individual Slave', '15S Individual Slave'),
('90S Electrical Panel', '90S Electrical Panel'),
('60S Electrical Panel', '60S Electrical Panel')
], string="Slave Type")
fg_ids = fields.One2many('sos_fg_deliverables', 'ref_id', string= 'FG Deliverables',copy=True)
sfg_ids = fields.One2many('sos_sfg_deliverables', 'ref_id', string= 'SFG Deliverables',copy=True)
material_ids = fields.One2many('sos_material_deliverables', 'ref_id', string='Material Deliverables',copy=True)
@ -57,6 +62,7 @@ class SOS_SFG_Deliverables(models.Model):
item_type = fields.Selection([
('Master Panel', 'Master Panel'),
('CT Module', 'CT Module'),
('Battery Count', 'Battery Count'),
('Slave Module', 'Slave Module'),
('Internet Module', 'Internet Module')
], string="Type",default="Master Panel")
@ -82,6 +88,7 @@ class SOS_Material_Deliverables(models.Model):
item_type = fields.Selection([
('Master Panel', 'Master Panel'),
('CT Module', 'CT Module'),
('Battery Count', 'Battery Count'),
('Slave Module', 'Slave Module'),
('Internet Module', 'Internet Module')
], string="Type",default="Master Panel")

View File

@ -8,4 +8,34 @@ class sos_department(models.Model):
name = fields.Char(string="Department Name")
short_form = fields.Char(string="Short Text")
process_incharge = fields.Many2one('res.users', string='Process Incharge')
users_line_ids = fields.One2many('sos_departments_user_lines','ref_id', string='Users')
_sql_constraints = [
('uniq_department_name', 'unique(name)', 'Department name must be unique.'),
]
class sos_department_line(models.Model):
_name = 'sos_departments_user_lines'
_description = 'Users in department'
ref_id = fields.Many2one('sos_departments', string="Departments", ondelete="cascade")
users = fields.Many2one('res.users', string='Users')
# @api.onchange('users')
# def _onchange_users(self):
# if self.users:
# domain = [('users', '=', self.users.id)]
# if self.id and isinstance(self.id, int): # Only add if it's a real ID
# domain.append(('id', '!=', self.id))
# existing_line = self.env['sos_departments_user_lines'].search(domain, limit=1)
# if existing_line:
# return {
# 'warning': {
# 'title': 'User Already Assigned',
# 'message': f"This user is already assigned to {existing_line.ref_id.name}."
# }
# }

View File

@ -96,7 +96,7 @@ class sos_Disposal_Register(models.Model):
<p>Below Item is waiting for your approval to Dispose</p>
"""
sequence_util.send_direct_email(self.env,"sos_disposal_register",self.id,"ramachandran.r@sosaley.in","Disposal Approved",body_html)
sequence_util.send_direct_email(self.env,"sos_disposal_register",self.id,"ramachandran.r@sosaley.com","Disposal Approved",body_html)
# Email part ends
return sequence_util.action_assign_signature(
self,

View File

@ -30,7 +30,9 @@ class SOS_Dock_Audit(models.Model):
],
string="Product Type",required=True)
quantity = fields.Integer(string="Quantity")
warranty = fields.Integer(string="Warranty(In Months)")
invoice_no = fields.Char(string="Invoice No")
invoice_date = fields.Date(string="Invoice Date")
customer_name = fields.Char(string="Customer Name")
lead_time = fields.Datetime(string="Lead Time")
customer_po_no = fields.Char(string="PO No")
@ -140,6 +142,18 @@ class SOS_Dock_Audit(models.Model):
})
def action_acc_esign_btn(self):
required_fields = ['payment_status', 'invoice_date','invoice_no', 'billing_address', 'gst_no', 'shipping_address']
missing_fields = []
for field in required_fields:
if not self[field]: # Check if field is empty/False
missing_fields.append(field)
# If any fields are missing, show warning
if missing_fields:
warning_msg = "Please fill the following required fields before proceeding:\n"
warning_msg += "\n".join([f"👉 {field.replace('_', ' ').title()}" for field in missing_fields])
raise UserError(warning_msg)
sequence_util = self.env['sos_common_scripts']
sequence_util.action_assign_signature(
self,
@ -151,7 +165,7 @@ class SOS_Dock_Audit(models.Model):
body_html = f"""
<p>Below Dock Audit is waiting for your Approval</p>
"""
sequence_util.send_direct_email(self.env,"sos_dock_audit",self.id,"ramachandran.r@sosaley.in","Dock Audit Approval",body_html)
sequence_util.send_direct_email(self.env,"sos_dock_audit",self.id,"ramachandran.r@sosaley.com","Dock Audit Approval",body_html)
# Email part ends
def action_auditor_esign_btn(self):
sequence_util = self.env['sos_common_scripts']
@ -206,7 +220,7 @@ class SOS_Dock_Audit(models.Model):
self.shipping_address = self.deliverables_boq_id.sales_id.shipping_address
self.gst_no = self.deliverables_boq_id.sales_id.gst_no
self.payment_status = self.deliverables_boq_id.sales_id.payment_status
self.customer_name = self.deliverables_boq_id.sales_id.customer_name
self.customer_name = self.deliverables_boq_id.sales_id.customer_name.customer_name
self.fg_name = self.deliverables_boq_id.sales_id.fg_name
self.quantity = self.deliverables_boq_id.sales_id.qty
self.lead_time = self.deliverables_boq_id.sales_id.lead_time

View File

@ -30,6 +30,11 @@ class SOS_FG_Plan(models.Model):
prepared_by = fields.Many2one('res.users', string='Planned By')
prepared_image = fields.Image(related="prepared_by.signature_image",string='Prepared By Sign',readonly=True)
prepared_on = fields.Datetime(string="Planned On")
accounts_approved_on = fields.Datetime(string="Accounts Approved On")
accounts_approved_name = fields.Many2one('res.users', string='Accounts Sign')
accounts_approved_by_image = fields.Image(related="accounts_approved_name.signature_image",string='Accounts Sign',readonly=True)
blowup = fields.Boolean(string="Blowup Done",default=False)
target_date = fields.Date(string="Target Date",required=True)
indent_start_date = fields.Date(string="Indent Start Date", default=lambda self: fields.Date.context_today(self))
@ -116,30 +121,46 @@ class SOS_FG_Plan(models.Model):
month = today.strftime('%m')
base_sequence_prefix = f"SOS/{form_name}/{fy}/{month}/"
# Search for latest record for this fiscal year (not just this month)
records = self.env[model_name].sudo().search(
[(field_name, 'like', f"{base_sequence_prefix}%")],
[(field_name, 'like', f"SOS/{form_name}/{fy}/%")],
order=f"{field_name} desc",
limit=1
)
if records:
last_sequence = records[0][field_name]
last_suffix = last_sequence.split('/')[-1]
if last_suffix.isdigit():
new_suffix = f"{last_suffix}a"
parts = last_sequence.split('/')
last_month = parts[-2]
last_suffix = parts[-1]
# Extract numeric part only (ignore trailing alphabet)
numeric_part = ''.join(filter(str.isdigit, last_suffix))
if last_month != month:
# New month: increment numeric part, no suffix
new_number = int(numeric_part) + 1
new_suffix = f"{new_number:03d}"
else:
base_num = last_suffix[:-1]
last_alpha = last_suffix[-1]
next_alpha = chr(ord(last_alpha) + 1) if last_alpha < 'z' else 'a'
new_suffix = f"{base_num}{next_alpha}"
# Same month: check for alphabet suffix
alpha_part = ''.join(filter(str.isalpha, last_suffix))
if alpha_part:
next_alpha = chr(ord(alpha_part) + 1) if alpha_part < 'z' else 'a'
new_suffix = f"{numeric_part}{next_alpha}"
else:
new_suffix = '016'
# First duplicate in same month
new_suffix = f"{numeric_part}a"
else:
# No previous records at all
new_suffix = '001'
return f"{base_sequence_prefix}{new_suffix}"
def _get_default_lines(self,model,column):
products = self.env[model].search([])
default_lines = []
@ -170,9 +191,10 @@ class SOS_FG_Plan(models.Model):
def send_indent_plan_email(self, email_ids):
valid_emails = [email for email in email_ids if email]
template = self.env.ref('sos_inventory.send_indent_plan_email_template')
if template:
template.email_to = ','.join(email_ids)
template.email_to = ','.join(valid_emails)
template.send_mail(self.id, force_send=True)
def get_unique_emails(self):
group_refs = [
@ -290,6 +312,21 @@ class SOS_FG_Plan(models.Model):
else:
worksheet.write(row, 1, value)
def action_acc_approver_esign_btn(self):
sequence_util = self.env['sos_common_scripts']
body_html = f"""
<p>Below <b>Indent Budget</b> is waiting for your Approval</p>
"""
send_email = self.env['sos_common_scripts']
send_email.send_direct_email(self.env,"sos_fg_plan",self.id,"ramachandran.r@sosaley.com","Indent Budget Approval",body_html)
result = sequence_util.action_assign_signature(
self,
'accounts_approved_name',
'accounts_approved_on',
'sos_inventory.sos_finance_user'
)
def action_top_approver_esign_btn(self):
sequence_util = self.env['sos_common_scripts']
@ -334,11 +371,9 @@ class SOS_FG_Plan(models.Model):
body_html = f"""
<p>Below <b>Indent Budget</b> is waiting for your Approval</p>
"""
send_email = self.env['sos_common_scripts']
send_email.send_direct_email(self.env,"sos_fg_plan",self.id,"ramachandran.r@sosaley.in","Indent Budget Approval",body_html)
sequence_util = self.env['sos_common_scripts']
result = sequence_util.action_assign_signature(
send_email.send_group_email(self.env,'sos_fg_plan',self.id,"deenalaura.m@sosaley.in","Indent Budget Approval",body_html,'sos_inventory.sos_finance_user')
result = send_email.action_assign_signature(
self,
'prepared_by',
'prepared_on'

View File

@ -125,6 +125,7 @@ class FIR_BRR(models.Model):
sos_record.target_date ,
date.today()
)
week_number = min(week_number, 8)
field_name = f'qc_week_{week_number}'
fgplan_field_name = f'planned_week_{week_number}'
# FG Plan update

View File

@ -77,6 +77,7 @@ class sos__grn(models.Model):
print(f"Failed to find report action: {e}")
def action_report_esign_btn(self):
sequence_util = self.env['sos_common_scripts']
sequence_util.action_assign_signature(
self,
@ -84,6 +85,7 @@ class sos__grn(models.Model):
'stores_approval_on',
'sos_inventory.sos_scg_group_user'
)
self.generate_supplier_service()
if self.received_goods_type == "Materials":
for item in self.line_ids:
component = self.env['sos_material'].browse(item.component_id.id)
@ -148,7 +150,180 @@ class sos__grn(models.Model):
def _compute_sequence(self):
sequence_util = self.env['sos_common_scripts']
return sequence_util.generate_sequence('sos_grn','GRN', 'grn_no')
def generate_supplier_service(self):
grn = self.env['sos_grn'].browse(self.id)
if self.received_goods_type == "Materials":
# Check for existing register line
already_created = self.env['sos_mat_outsourcing_vendor_register_lines'].search([
('ref_id.supplier_name', '=', grn.supplier_name.id),
('po_no', '=', grn.po_no.id)
], limit=1)
# Get or create the main register
outsource = self.env['sos_mat_outsourcing_vendor_register'].search(
[('supplier_name', '=', grn.supplier_name.id)], limit=1)
if not outsource:
outsource = self.env['sos_mat_outsourcing_vendor_register'].create({
'supplier_name': grn.supplier_name.id,
})
ir_records = self.env['sos_ir'].search([('po_no', '=', grn.po_no.id)])
iqi_records = self.env['sos_iqi'].search([('ir_id_unique_id', 'in', ir_records.ids)])
grn_records = self.env['sos_grn'].search([('po_no', '=', grn.po_no.id)])
grn_line_records = self.env['sos_grn_line'].search([('grn_id', 'in', grn_records.ids)])
# Prepare material_names list
mat_record = [line.component_id.id for line in grn_line_records if line.component_id and line.component_id.part_no]
material_names = [(6, 0, mat_record)]
# Sum all GRN line quantities including current
total_received_qty = sum(grn_line_records.mapped('received_qty')) #+ new_received
total_approved_qty = sum(grn_line_records.mapped('approved_qty')) #+ new_approved
total_rejected_qty = sum(grn_line_records.mapped('rejected_qty')) #+ new_rejected
delivery_values = grn_records.mapped('delivery')
delivery_numbers = [float(value) for value in delivery_values if value]
existing_count = len(delivery_numbers)
responsive_values = grn_records.mapped('responsiveness')
responsive_numbers = [float(value) for value in responsive_values if value]
reports_values = grn_records.mapped('reports')
reports_numbers = [float(value) for value in reports_values if value]
avg_delivery_marks = sum(delivery_numbers) / (existing_count)
delivery = (
30 if 27.5 < avg_delivery_marks <= 30 else
25 if 22.5 < avg_delivery_marks <= 27.5 else
10 if avg_delivery_marks < 20 else
0
)
avg_report_marks = sum(reports_numbers) / (existing_count)
report = (
30 if 27.5 < avg_report_marks <= 30 else
25 if 22.5 < avg_report_marks <= 27.5 else
10 if avg_report_marks < 20 else
0
)
avg_responsiveness_marks = sum(responsive_numbers) / (existing_count)
responsiveness = (
10 if 5 <= avg_responsiveness_marks <= 10 else
5 if avg_responsiveness_marks < 5 else
0
)
if already_created:
updated_vals = {
'received_qty': total_received_qty,
'approved_qty': total_approved_qty,
'rejected_qty': total_rejected_qty,
'delivery_marks': delivery,
'responsiveness_marks': responsiveness,
'report_marks': report,
'iqi_references': [(6, 0, iqi_records.ids)],
'grn_references': [(6, 0, grn_records.ids)],
'material_names': material_names,
}
already_created.write(updated_vals)
else:
self.env['sos_mat_outsourcing_vendor_register_lines'].create({
'ref_id': outsource.id,
'po_no': grn.po_no.id,
'received_qty': total_received_qty,
'approved_qty': total_approved_qty,
'rejected_qty': total_rejected_qty,
'iqi_references': [(6, 0, iqi_records.ids)],
'grn_references': [(6, 0, grn_records.ids)],
'material_names': material_names,
'delivery_marks': delivery,
'responsiveness_marks': responsiveness,
'report_marks': report,
})
elif self.received_goods_type == "SFG":
# Check for existing register line
already_created = self.env['sos_outsourcing_vendor_monitoring_register_lines'].search([
('ref_id.service_provider_name', '=', grn.service_provider_name.id),
('wo_no', '=', grn.wo_no.id)
], limit=1)
# Get or create the main register
outsource = self.env['sos_outsourcing_vendor_monitoring_register'].search(
[('service_provider_name', '=', grn.service_provider_name.id)], limit=1)
if not outsource:
outsource = self.env['sos_outsourcing_vendor_monitoring_register'].create({
'service_provider_name': grn.service_provider_name.id,
})
# Related documents
ir_records = self.env['sos_ir'].search([('wo_no', '=', grn.wo_no.id)])
iqi_records = self.env['sos_iqi'].search([('ir_id_unique_id', 'in', ir_records.ids)])
dc_records = self.env['sos_dc'].search([('wo_no', '=', grn.wo_no.id)])
grn_records = self.env['sos_grn'].search([('wo_no', '=', grn.wo_no.id)])
grn_line_records = self.env['sos_grn_line_sfg'].search([('grn_id', 'in', grn_records.ids)])
# Prepare material_names list
sfg_ids = [line.component_id.id for line in grn_line_records if line.component_id and line.component_id.name]
# Include current line component manually if not yet saved in DB
current_component_id = self.line_ids.component_id
if current_component_id and current_component_id not in sfg_ids:
sfg_ids.append(current_component_id)
sfg_names = [(6, 0, sfg_ids)]
total_received_qty = sum(grn_line_records.mapped('received_qty'))
total_approved_qty = sum(grn_line_records.mapped('approved_qty'))
total_rejected_qty = sum(grn_line_records.mapped('rejected_qty'))
delivery_values = grn_line_records.mapped('delivery')
delivery_numbers = [float(value) for value in delivery_values if value]
existing_count = len(delivery_numbers)
responsive_values = grn_line_records.mapped('responsiveness')
responsive_numbers = [float(value) for value in responsive_values if value]
reports_values = grn_line_records.mapped('reports')
reports_numbers = [float(value) for value in reports_values if value]
avg_delivery_marks = sum(delivery_numbers) / (existing_count)
avg_responsiveness_marks = sum(responsive_numbers) / (existing_count)
avg_report_marks = sum(reports_numbers) / (existing_count)
if already_created:
#print(f" sfg_names : if {sfg_names}")
updated_vals = {
'received_qty': total_received_qty,
'approved_qty': total_approved_qty,
'rejected_qty': total_rejected_qty,
'delivery_marks': avg_delivery_marks,
'responsiveness_marks': avg_responsiveness_marks,
'report_marks': avg_report_marks,
'iqi_references': [(6, 0, iqi_records.ids)],
'dc_references': [(6, 0, dc_records.ids)],
'grn_references': [(6, 0, grn_records.ids)],
'sfg_names': sfg_names,
}
already_created.write(updated_vals)
else:
#print(f" sfg_names : else {sfg_names}")
self.env['sos_outsourcing_vendor_monitoring_register_lines'].create({
'ref_id': outsource.id,
'iqi_references': [(6, 0, iqi_records.ids)],
'dc_references': [(6, 0, dc_records.ids)],
'grn_references': [(6, 0, grn_records.ids)],
'sfg_names': sfg_names,
'wo_no': grn.wo_no.id,
'received_qty': total_received_qty,
'approved_qty': total_approved_qty,
'rejected_qty': total_rejected_qty,
'delivery_marks': avg_delivery_marks,
'responsiveness_marks': avg_responsiveness_marks,
'report_marks': avg_report_marks,
})
class sos_grn_line(models.Model):
_name = 'sos_grn_line'
_description = 'GRN Material Lines'
@ -169,14 +344,19 @@ class sos_grn_line(models.Model):
@api.depends('received_qty', 'rejected_qty')
def _compute_quality(self):
for record in self:
if record.rejected_qty != 0:
record.quality_marks = (100 - ((record.rejected_qty / record.received_qty) * 100))
if record.quality_marks >= 80 and record.quality_marks <=99:
record.quality_marks = 25
record.quality_marks = self._calculate_quality_marks(record.received_qty, record.rejected_qty)
def _calculate_quality_marks(self, received, rejected):
if received != 0:
q = 100 - ((rejected / received) * 100)
if 91 <= q <= 100:
return 30
elif 80 <= q <= 90:
return 25
else:
record.quality_marks = 10
else:
record.quality_marks = 100
return 10
return 0
@api.model
def write(self, vals):
@ -295,14 +475,20 @@ class sos_grn_line_sfg(models.Model):
@api.depends('received_qty', 'rejected_qty')
def _compute_sfgquality(self):
for record in self:
if record.rejected_qty != 0:
record.quality_marks = (100 - ((record.rejected_qty / record.received_qty) * 100))
if record.quality_marks >= 80 and record.quality_marks <=99:
record.quality_marks = 25
record.quality_marks = self._calculate_quality_marks(record.received_qty, record.rejected_qty)
def _calculate_quality_marks(self, received, rejected):
if received != 0:
q = 100 - ((rejected / received) * 100)
if 91 <= q <= 100:
return 30
elif 80 <= q <= 90:
return 25
else:
record.quality_marks = 10
else:
record.quality_marks = 100
return 10
return 0
class sos_grn_line_fg(models.Model):
_name = 'sos_grn_line_fg'

View File

@ -276,6 +276,7 @@ class SOS_IR(models.Model):
else:
if item.qty > 0:
iqi_record = self.env['sos_iqi'].create({
'iqi_no': sequence_util.generate_sequence('sos_iqi', 'IQI', 'iqi_no'),
'material_option':True,
@ -383,7 +384,7 @@ class SOS_IR(models.Model):
"""
subject = f"Inward Approval Request - {self.ir_no}"
send_email = self.env['sos_common_scripts']
send_email.send_direct_email(self.env,"sos_ir",self.id,"ramachandran.r@sosaley.in",subject,body_html)
send_email.send_direct_email(self.env,"sos_ir",self.id,"ramachandran.r@sosaley.com",subject,body_html)
# Email part ends
sequence_util = self.env['sos_common_scripts']
return sequence_util.action_assign_signature(

View File

@ -35,11 +35,25 @@ class SOS_Mat_Oursourcing_Monitor_Lines(models.Model):
rejection_percentage = fields.Float(string="Rejected Percentage", compute="_compute_rejected_percentage", store=True)
rejection_percentage_display = fields.Char('Rejection Percentage', compute='_compute_rejected_percentage_display')
ppm = fields.Integer(string="PPM",compute="_compute_ppm")
quality_marks = fields.Float(string="Quality Marks")
quality_marks = fields.Float(string="Quality Marks",compute="_compute_sfgquality")
delivery_marks = fields.Float(string="Delivery Marks")
responsiveness_marks = fields.Float(string="Responsiveness Marks")
report_marks = fields.Float(string="Report Marks")
def _calculate_quality_marks(self, received, rejected):
if received != 0:
q = 100 - ((rejected / received) * 100)
if 91 <= q <= 100:
return 30
elif 80 <= q <= 90:
return 25
else:
return 10
return 0
@api.depends('received_qty', 'rejected_qty')
def _compute_sfgquality(self):
for record in self:
record.quality_marks = self._calculate_quality_marks(record.received_qty, record.rejected_qty)
@api.depends('rejection_percentage')
def _compute_rejected_percentage_display(self):

View File

@ -16,6 +16,7 @@ class sos__mon(models.Model):
filled_by = fields.Many2one('res.users', string='Filled By', readonly=True,required=True,default=lambda self: self.env.user)
logged_inuser_group=fields.Boolean(string='Group Name',compute='compute_user_grp',store=True)
dept = fields.Many2one('sos_departments',string="Department")
customer_name = fields.Many2one('sos_inventory_customers',string="Customer Name")
purpose = fields.Char(string="Purpose")
product_name = fields.Many2one('sos_fg', string='Material/Product Name & No')
indent_ref_no = fields.Many2one('sos_fg_plan',string="Indent Reference No")
@ -33,7 +34,7 @@ class sos__mon(models.Model):
top_management_name = fields.Many2one('res.users', string='Top Management Approver')
top_management_approval_image = fields.Image(related="top_management_name.signature_image",string='Top Management Approval',readonly=True)
top_management_approved_on = fields.Datetime(string="Approved On")
deliverables_boq_id = fields.Many2one('sos_deliverables_boq', string="Load From Deliverables/BOQ Id")
stores_approved_by = fields.Many2one('res.users', string='Stores Approved By')
stores_approved_image = fields.Image(related="stores_approved_by.signature_image",string='Stores Approval Sign',readonly=True)
stores_approved_on = fields.Datetime(string="Approved On")
@ -43,9 +44,9 @@ class sos__mon(models.Model):
('fg', 'FG')
], string='Auto Load Items' ,default=False)
material_option = fields.Boolean('Materials', default=True)
sfg_option = fields.Boolean('Semi-Finished Goods')
fg_option = fields.Boolean('Finished Goods')
order_type = fields.Char(string='order_type',copy=True)
sfg_option = fields.Boolean('Semi-Finished Goods', default=False)
fg_option = fields.Boolean('Finished Goods', default=False)
order_type = fields.Char(string='order_type',copy=True,compute="_compute_order_type")
line_ids_material = fields.One2many('sos_mon_line_material', 'mon_id', string="Materials",copy=True)
line_ids_sfg = fields.One2many('sos_mon_line_sfg', 'mon_id', string="Semi-Finished Goods",copy=True)
line_ids_fg = fields.One2many('sos_mon_line_fg', 'mon_id', string="Finished Goods",copy=True)
@ -73,6 +74,74 @@ class sos__mon(models.Model):
approx_value = fields.Monetary(compute='_compute_approx_value', string="Approximate Value", currency_field='currency_id', readonly=True,store=True)
status = fields.Selection([ ('open', 'Open'),('close', 'Closed')], default='open' , string="Status")
active = fields.Boolean(default=True)
is_ce_user_created = fields.Boolean(
compute='_compute_is_ce_user_created',
store=True,
string='Created by CE User'
)
@api.depends('create_uid')
def _compute_is_ce_user_created(self):
ce_groups = [
self.env.ref('sos_inventory.sos_ce_user').id
]
for record in self:
record.is_ce_user_created = any(
gid in record.create_uid.groups_id.ids for gid in ce_groups
)
@api.depends('material_option', 'sfg_option', 'fg_option')
def _compute_order_type(self):
for rec in self:
parts = []
if rec.material_option:
parts.append('Material')
if rec.sfg_option:
parts.append('SFG')
if rec.fg_option:
parts.append('FG')
rec.order_type = ",".join(parts) if parts else False
@api.onchange('deliverables_boq_id')
def _onchange_deliverables_boq_id(self):
self.material_option = True
self.sfg_option = True
self.fg_option = True
if self.deliverables_boq_id:
self.line_ids_material = [(5, 0, 0)] # Clear existing records
self.line_ids_material = [
(0, 0, {
'component_id': line.component_id, # Replace 'field1' with the actual field names
'uom': line.uom,
'material_code': line.material_code,
'quantity': line.quantity
# Add other fields to copy here
}) for line in self.deliverables_boq_id.line_ids_installation_kit
]
self.line_ids_fg = [(5, 0, 0)] # Clear existing records
self.line_ids_fg = [
(0, 0, {
'component_id': line.component_id, # Replace 'field1' with the actual field names
'quantity': line.quantity
}) for line in self.deliverables_boq_id.line_ids_fg
]
self.line_ids_sfg = [(5, 0, 0)] # Clear existing records
self.line_ids_sfg = [
(0, 0, {
'component_id': line.component_id, # Replace 'field1' with the actual field names
'quantity': line.quantity
# Add other fields to copy here
}) for line in self.deliverables_boq_id.line_ids_sfg
]
self.line_ids_material = [
(0, 0, {
'uom': line.uom,
'material_code': line.material_code,
'component_id': line.component_id,
'quantity': line.quantity
# Add other fields to copy here
}) for line in self.deliverables_boq_id.line_ids_material
]
@api.constrains('indent_ref_no', 'auto_load_fg_items')
def _check_duplicate_fg_items_per_indent(self):
for record in self:
@ -461,7 +530,7 @@ class Mon_Line_Material(models.Model):
uom = fields.Selection([('meters', 'Meters'),('Nos', 'Nos'),('coils', 'Coils'), ('litre', 'litre'), ('kg', 'Kilogram'), ('Packs', 'Packs')], string="Uom")
specifications = fields.Char(string="Specifications")
quantity = fields.Float(string="Quantity",required=True,default=1)
location = fields.Char(string="Location")
location = fields.Char(string="Location",related='component_id.location')
company_id = fields.Many2one('res.company', store=True, copy=False,
string="Company",
default=lambda self: self.env.user.company_id.id)

View File

@ -46,6 +46,20 @@ class sos__mrn(models.Model):
default=lambda
self: self.env.user.company_id.currency_id.id)
approx_value = fields.Monetary(compute='_compute_approx_value', string="Approximate Value", currency_field='currency_id', readonly=True)
is_ce_user_created = fields.Boolean(
compute='_compute_is_ce_user_created',
store=True,
string='Created by CE User'
)
@api.depends('create_uid')
def _compute_is_ce_user_created(self):
ce_groups = [
self.env.ref('sos_inventory.sos_ce_user').id
]
for record in self:
record.is_ce_user_created = any(
gid in record.create_uid.groups_id.ids for gid in ce_groups
)
@api.onchange('min_no')
def _onchange_min_no(self):
if self.min_no:

View File

@ -52,7 +52,8 @@ class NCMR_Model(models.Model):
finished_fg_assy = fields.Char()
finished_fg_assy_responsibility = fields.Text(string="Production Assy FG Responsibility")
description_of_nc = fields.Html(string="Description of Non-Conformities")
root_cause_of_nc = fields.Html(string="Root Cause of Non-Conformities")
root_cause_of_nc = fields.Html(string="Root Cause",
default="<p>Why #1 :</p><p>Why #2 :</p><p>Why #3 :</p><p>Why #4 :</p><p>Why #5 :</p>")
containment_action_of_nc = fields.Html(string="Containment Action to close the Non-Conformities")
comments_on_capa = fields.Html(string="Comments on Corrective / Preventive Action ")
qa_comments=fields.Text(string="QA Comments")
@ -105,6 +106,14 @@ class NCMR_Model(models.Model):
rework_rd_approval_sign = fields.Image(related="rework_rd_approval_by.signature_image",string='Rework - R&D In-Charge',readonly=True)
rework_rd_approval_on = fields.Datetime(string="Approved On")
responsible_department = fields.Many2one('sos_departments', string='Department')
responsible_name = fields.Many2one('res.users',string='Responsible Person',domain="[('id', 'in', allowed_user_ids)]")
allowed_user_ids = fields.Many2many('res.users', compute='_compute_allowed_users')
rca_responsible_department = fields.Many2many('sos_departments', string='Department')
rca_responsible_name = fields.Many2many('res.users',string='Responsible Person',relation='sos_ncmr_rca_responsible_name_rel',domain="[('id', 'in', rca_allowed_user_ids)]")
rca_allowed_user_ids = fields.Many2many('res.users', compute='_rca_compute_allowed_users')
combined_incoming_doc_ref = fields.Reference(
selection=[
('sos_iqi', 'IQI Ref No'),
@ -120,8 +129,155 @@ class NCMR_Model(models.Model):
supplier_name = fields.Many2one('sos_suppliers',string="Supplier Name",compute="_get_supplier_name")
service_provider_name = fields.Many2one('sos_service_providers',string="Service Provider Name",compute="_get_service_provider_name")
customer_name = fields.Many2one('sos_inventory_customers', string="Customer Name",compute="_get_customer_name")
outsourcing_return_ref_no = fields.Many2one('sos_sfg_outsourcing_return_register',string="Outsourcing Return Ref No")
problem_description_line_ids = fields.One2many('sos_ncmr_problem_description_line', 'ncmr_id', string="Problem Description Line Ids",copy=True)
@api.depends('responsible_department')
def _compute_allowed_users(self):
for rec in self:
rec.allowed_user_ids = rec.responsible_department.users_line_ids.mapped('users')
@api.depends('rca_responsible_department')
def _rca_compute_allowed_users(self):
for rec in self:
rec.rca_allowed_user_ids = rec.rca_responsible_department.users_line_ids.mapped('users')
@api.model
def get_service_suppliers_by_goods_type(self,goods_type=False,startDate=False, endDate=False):
if goods_type == 'Material':
query = """
SELECT id,supplier_name
FROM sos_suppliers
WHERE id IN (
SELECT b.supplier_name
FROM sos_ncmr a, sos_iqi b
WHERE a.incoming_doc_ref = b.id
AND a.material_option = 't'
AND a.ncmr_date BETWEEN %s AND %s
)
"""
elif goods_type == 'SFG':
query = """
SELECT id,service_provider_name
FROM sos_service_providers
WHERE id IN (
SELECT b.service_provider_name
FROM sos_ncmr a, sos_iqi b
WHERE a.incoming_doc_ref = b.id
AND a.sfg_option = 't'
AND a.ncmr_date BETWEEN %s AND %s
)
"""
self.env.cr.execute(query,(startDate, endDate))
rows = self.env.cr.fetchall()
result = [{'id': '', 'supplier_name': 'Select Supplier/Service'}]
result += [{'id': row[0], 'supplier_name': row[1]} for row in rows]
return result
@api.model
def get_pareto_data(self, start_date=False, end_date=False, goodstype=False, categoryId=False, servicesupplier=False):
if not start_date or not end_date:
raise ValueError("Start date and end date must be provided.")
base_where_clause = "a.ncmr_date BETWEEN %s AND %s"
where_params = [start_date, end_date]
base_groupby_clause = "d.name"
if goodstype == 'Material':
join_clause = """
JOIN sos_material_defective_line_sos_ncmr_line_rel c ON b.id = c.sos_ncmr_line_id
JOIN sos_material_defective_line d ON c.sos_material_defective_line_id = d.id
JOIN sos_material_configuration e ON a.material_category = e.id
"""
if categoryId and categoryId != 'Select Category':
join_clause += " JOIN sos_material f ON a.material_name = f.id JOIN sos_material_types g ON f.material_type_id = g.id"
base_where_clause += " AND g.name = %s"
#base_groupby_clause += ",g.name"
where_params.append(categoryId)
if servicesupplier and servicesupplier !='Select Supplier/Service' and servicesupplier !='undefined':
join_clause += " LEFT JOIN sos_iqi h ON a.incoming_doc_ref = h.id LEFT JOIN sos_suppliers i ON h.supplier_name = i.id"
base_where_clause += " AND h.supplier_name = %s"
base_groupby_clause += ",h.supplier_name"
where_params.append(servicesupplier)
elif goodstype == 'SFG':
join_clause = """
JOIN sos_ncmr_line_sos_sfg_defective_line_rel c ON b.id = c.sos_ncmr_line_id
JOIN sos_sfg_defective_line d ON c.sos_sfg_defective_line_id = d.id
JOIN sos_sfg_configuration e ON a.sfg_category = e.id
"""
if categoryId and categoryId != 'Select Category':
join_clause += " JOIN sos_sfg f ON a.sfg_name = f.id"
base_where_clause += " AND f.category = %s"
base_groupby_clause += ",f.category"
where_params.append(categoryId)
if servicesupplier and servicesupplier !='Select Supplier/Service' and servicesupplier !='undefined':
join_clause += " LEFT JOIN sos_iqi g ON a.incoming_doc_ref = g.id LEFT JOIN sos_service_providers h ON g.service_provider_name = h.id"
base_where_clause += " AND g.service_provider_name = %s"
base_groupby_clause += ",g.service_provider_name"
where_params.append(servicesupplier)
elif goodstype == 'FG':
join_clause = """
JOIN sos_defectives_sos_ncmr_line_rel c ON b.id = c.sos_ncmr_line_id
JOIN sos_defectives d ON c.sos_defectives_id = d.id
JOIN sos_testing_parameters f ON a.fg_category = f.id
JOIN sos_fg e ON e.id = f.fg_name
"""
if categoryId and categoryId != 'Select Category':
base_where_clause += " AND e.fg_type = %s"
base_groupby_clause += ",e.fg_type"
where_params.append(categoryId)
# Build the full SQL query dynamically
query = f"""
SELECT
defect_name,
count,
ROUND(count * 100.0 / total, 2) AS individual_percent,
ROUND(SUM(count) OVER (ORDER BY count DESC ROWS UNBOUNDED PRECEDING) * 100.0 / total, 2) AS cumulative_percent
FROM (
SELECT
d.name AS defect_name,
COUNT(*) AS count,
SUM(COUNT(*)) OVER () AS total
FROM
sos_ncmr a
JOIN sos_ncmr_line b ON a.id = b.ncmr_id
{join_clause}
WHERE
{base_where_clause}
GROUP BY
{base_groupby_clause}
) AS sub
ORDER BY
count DESC;
"""
self.env.cr.execute(query, tuple(where_params))
result = self.env.cr.fetchall()
data = []
for row in result:
data.append({
'defect_name': row[0], # defect_name
'count': row[1], # count
'cumulative_percent': row[3], # cumulative_percent
})
return data
#Excel Write
def action_ncmr_report_orm_btn(self, from_date, to_date,force_download=True):
output = io.BytesIO()
@ -559,17 +715,18 @@ class NCMR_Model(models.Model):
'sos_inventory.sos_qa_user'
)
else:
if all_closed or record.aodr_no:
self.status = 'closed'
if self.rework_action == "inhouse":
s_no_list = self.line_ids.mapped('s_no') # Collect all s_no values
s_no_str = ', '.join(map(str, s_no_list))
iqi_record = self.env['sos_iqi'].create({
'iqi_no': send_email.generate_sequence('sos_iqi', 'R-IQI', 'iqi_no'),
'material_option':self.material_option,
'sfg_option':self.sfg_option,
'received_qty': self.rejected_qty,
'iqi_type':'rework',
'material_name': self.material_name,
'material_name': self.material_name.id,
'material_code': self.material_name.material_code,
'sfg_name': self.sfg_name.id,
'sfg_code': self.sfg_name.sfg_code,
@ -577,7 +734,9 @@ class NCMR_Model(models.Model):
'service_provider_name':self.incoming_doc_ref.service_provider_name.id,
'old_iqi_ref': self.incoming_doc_ref.id,
'ir_id_unique_id':self.incoming_doc_ref.ir_id_unique_id.id,
'in_tact':self.incoming_doc_ref.in_tact
'in_tact':self.incoming_doc_ref.in_tact,
'ncmr_ref':self.id,
'serial_no':s_no_str
# 'test_report':self.incoming_doc_ref.test_report,
# 'test_report_no':self.incoming_doc_ref.test_report_no,
# 'test_report_doc':self.incoming_doc_ref.test_report_doc,
@ -585,7 +744,7 @@ class NCMR_Model(models.Model):
})
print(iqi_record)
# Rework Iteration starts
sos_record = self.env['sos_iqi'].search([('id', '=', self.incoming_doc_ref.id)], limit=1)
if sos_record:
@ -622,7 +781,27 @@ class NCMR_Model(models.Model):
'sos_inventory.sos_scg_group_user'
)
if self.rework_action == "inhouse":
# Email part
if self.material_option:
orr_record = self.env['sos_sfg_outsourcing_return_register'].create({
'orr_no': orr_no,
'iqi_no':self.incoming_doc_ref.id,
'iqi_date':self.incoming_doc_ref.iqi_date,
'material_name':self.incoming_doc_ref.material_name,
'goods_type':'Materials',
'returned_qty':self.incoming_doc_ref.rejected_qty,
'rework_type':'inhouse'
})
else:
orr_record = self.env['sos_sfg_outsourcing_return_register'].create({
'orr_no': orr_no,
'iqi_no':self.incoming_doc_ref.id,
'iqi_date':self.incoming_doc_ref.iqi_date,
'sfg_name':self.incoming_doc_ref.sfg_name,
'goods_type':'SFG',
'returned_qty':self.incoming_doc_ref.rejected_qty,
'rework_type':'inhouse'
})
self.outsourcing_return_ref_no = orr_record.id
body_html = f"""
<p>Below <b>NCMR</b> is waiting for your Action</p>
"""
@ -636,7 +815,8 @@ class NCMR_Model(models.Model):
'material_name':self.incoming_doc_ref.material_name,
'supplier_name':self.incoming_doc_ref.supplier_name.id,
'goods_type':'Materials',
'returned_qty':self.incoming_doc_ref.rejected_qty
'returned_qty':self.incoming_doc_ref.rejected_qty,
'rework_type':'outsourcing'
})
else:
if self.material_option:
@ -647,7 +827,8 @@ class NCMR_Model(models.Model):
'material_name':self.incoming_doc_ref.material_name,
'supplier_name':self.incoming_doc_ref.supplier_name.id,
'goods_type':'Materials',
'returned_qty':self.incoming_doc_ref.rejected_qty
'returned_qty':self.incoming_doc_ref.rejected_qty,
'rework_type':'outsourcing'
})
else:
orr_record = self.env['sos_sfg_outsourcing_return_register'].create({
@ -657,7 +838,8 @@ class NCMR_Model(models.Model):
'service_provider_name':self.incoming_doc_ref.service_provider_name,
'sfg_name':self.incoming_doc_ref.sfg_name,
'goods_type':'SFG',
'returned_qty':self.incoming_doc_ref.rejected_qty
'returned_qty':self.incoming_doc_ref.rejected_qty,
'rework_type':'outsourcing'
})
self.outsourcing_return_ref_no = orr_record.id
@ -685,6 +867,26 @@ class NCMR_Model(models.Model):
)
def action_qa_esign_btn(self):
if self.responsible_department:
if self.material_option:
material = self.material_name.part_no
elif self.sfg_option:
material = self.sfg_name.name
else:
material = self.fg_name.name
result_col = f"NCMR Ref No :{getattr(self, 'ncmr_no', '')}"
new_action_record = self.env['sos_brm_action'].create({
'cross_dept_action': 'cross_dept',
'department': 3,
'responsible_person': getattr(self.responsible_name, 'id', False),
'assigned_by': getattr(self.qa_by, 'id', False),
'assigned_from_dept': 3,
'assigned_to_dept': getattr(self.responsible_department, 'id', False),
'name': f"NCMR Assigned : {material}",
'result': result_col
})
if self.action_group:
sequence_util = self.env['sos_common_scripts']
sequence_util.action_assign_signature(
@ -767,8 +969,9 @@ class NCMR_Model_CAPA_Line(models.Model):
ncmr_id = fields.Many2one('sos_ncmr', string="NCMR Reference", ondelete="cascade")
issue = fields.Char(string="Issue")
corrective_action = fields.Html(string="Corrective Action")
preventive_action = fields.Html(string="Preventive Action")
corrective_action = fields.Html(string="D5: Permanent Corrective Action")
implement_validate_corrective_action = fields.Html(string="D6:Implement & Validate Corrective Actions")
preventive_action = fields.Html(string="D7:Preventive Recurrence")
class NCMR_Model_Line(models.Model):
_name = 'sos_ncmr_line'
@ -897,3 +1100,18 @@ class NCMR_Model_Line(models.Model):
def _compute_fg_defective_count(self):
for record in self:
record.fg_defective_count = len(record.fg_defectives)
class NCMR_Model_PROBLEM_DESCRIPTION_Line(models.Model):
_name = 'sos_ncmr_problem_description_line'
_description = 'Problem Description Lines'
ncmr_id = fields.Many2one('sos_ncmr', string="NCMR Reference", ondelete="cascade")
clear_statement = fields.Html(string="Clear Statement")
photos = fields.Html(string="Photos")
what = fields.Text(string="What")
where = fields.Text(string="Where")
when = fields.Text(string="When")
why = fields.Text(string="Why")
who = fields.Text(string="Who")
how = fields.Text(string="How")
how_many = fields.Text(string="How many")

View File

@ -182,7 +182,7 @@ class SOS_Order_Delivery_Plan(models.Model):
<p>Below <b>Order Delivery Plan</b> is waiting for your Approval</p>
"""
send_email = self.env['sos_common_scripts']
send_email.send_direct_email(self.env,"sos_order_delivery_plan",self.id,"ramachandran.r@sosaley.in","Order Delivery Plan",body_html)
send_email.send_direct_email(self.env,"sos_order_delivery_plan",self.id,"ramachandran.r@sosaley.com","Order Delivery Plan",body_html)
# Email part ends
sequence_util = self.env['sos_common_scripts']
return sequence_util.action_assign_signature(

View File

@ -36,12 +36,25 @@ class SOS_Oursourcing_Monitor_Lines(models.Model):
rejection_percentage = fields.Float(string="Rejected Percentage", compute="_compute_rejected_percentage", store=True)
rejection_percentage_display = fields.Char('Rejection Percentage', compute='_compute_rejected_percentage_display')
ppm = fields.Integer(string="PPM",compute="_compute_ppm")
quality_marks = fields.Float(string="Quality Marks")
quality_marks = fields.Float(string="Quality Marks",compute="_compute_sfgquality")
delivery_marks = fields.Float(string="Delivery Marks")
responsiveness_marks = fields.Float(string="Responsiveness Marks")
report_marks = fields.Float(string="Report Marks")
def _calculate_quality_marks(self, received, rejected):
if received != 0:
q = 100 - ((rejected / received) * 100)
if 91 <= q <= 100:
return 30
elif 80 <= q <= 90:
return 25
else:
return 10
return 0
@api.depends('received_qty', 'rejected_qty')
def _compute_sfgquality(self):
for record in self:
record.quality_marks = self._calculate_quality_marks(record.received_qty, record.rejected_qty)
@api.depends('rejection_percentage')
def _compute_rejected_percentage_display(self):
for record in self:

View File

@ -143,7 +143,7 @@ class sos__po(models.Model):
"""
subject = f"Purchase Order Approval Request - {self.po_no}"
send_email = self.env['sos_common_scripts']
send_email.send_direct_email(self.env,"sos_po",self.id,"ramachandran.r@sosaley.in",subject,body_html)
send_email.send_direct_email(self.env,"sos_po",self.id,"ramachandran.r@sosaley.com",subject,body_html)
# Email part ends
sequence_util = self.env['sos_common_scripts']
return sequence_util.action_assign_signature(

79
sos_inventory/models/sos_prf.py Executable file
View File

@ -0,0 +1,79 @@
from odoo import models, fields, api
class SOS_prf(models.Model):
_name = 'sos_prf'
_description = 'Purchase Requisition Form'
_rec_name = 'prf_no'
_order = 'id desc'
prf_no = fields.Char(string="PRF No", readonly= True, required= True, default=lambda self: self._generate_id())
prf_date = fields.Date(string="PRF Date", required=True, default=fields.Date.today)
line_ids = fields.One2many('sos_prf_lines', 'ref_id', string="PRF Lines",copy=True)
requested_by_name = fields.Many2one('res.users', string='Requested by')
requested_by_image = fields.Image(related="requested_by_name.signature_image",string='Requested by Sign',readonly=True)
requested_on = fields.Datetime(string="Requested On")
dept_in_charge_name = fields.Many2one('res.users', string='Department In-Charge')
dept_in_charge_image = fields.Image(related="dept_in_charge_name.signature_image",string='Department In-Charge Sign',readonly=True)
dept_in_charge_approved_on = fields.Datetime(string="Approved On")
top_management_name = fields.Many2one('res.users', string='Top Management Approver')
top_management_approval_image = fields.Image(related="top_management_name.signature_image",string='Top Management Approval',readonly=True)
top_management_approved_on = fields.Datetime(string="Approved On")
status = fields.Selection([('open', 'Open'),('close', 'Closed')], default='open' , string="Status")
def action_top_esign_btn(self):
sequence_util = self.env['sos_common_scripts']
subject = f"PRF Approved - {self.prf_no}"
body_html = f"""
<p>Below <b>PRF</b> got approved</p>
"""
sequence_util.send_direct_email(self.env,"sos_prf",self.id,"praveenkumar.m@sosaley.com",subject,body_html)
sequence_util.action_assign_signature(
self,
'top_management_name',
'top_management_approved_on',
'sos_inventory.sos_management_user'
)
def action_dept_esign_btn(self):
# Email part
body_html = f"""
<p>Below <b>PRF</b> is waiting for your Approval</p>
"""
sequence_util = self.env['sos_common_scripts']
subject = f"PRF Approval Request - {self.prf_no}"
sequence_util.send_direct_email(self.env,"sos_prf",self.id,"ramachandran.r@sosaley.com",subject,body_html)
# Email part ends
sequence_util.action_assign_signature(
self,
'dept_in_charge_name',
'dept_in_charge_approved_on',
'sos_inventory.sos_scg_group_manager'
)
def action_stores_esign_btn(self):
# Email part
body_html = f"""
<p>Below <b>PRF</b> is waiting for your Approval</p>
"""
sequence_util = self.env['sos_common_scripts']
sequence_util.send_group_email(self.env,'sos_prf',self.id,"deenalaura.m@sosaley.in","PRF Approval Request",body_html,'sos_inventory.sos_scg_group_manager')
# Email part ends
sequence_util.action_assign_signature(
self,
'requested_by_name',
'requested_on',
'sos_inventory.sos_scg_group_user'
)
def _generate_id(self):
sequence_util = self.env['sos_common_scripts']
return sequence_util.generate_sequence('sos_prf','PRF', 'prf_no')
class SOS_prf_Lines(models.Model):
_name = 'sos_prf_lines'
_description = 'Purchase Requisition Form Lines'
ref_id = fields.Many2one('sos_prf', string="PRF", ondelete="cascade")
com_type = fields.Selection([('exits', 'In-stock'),('new', 'New')], string="Availability", default='exits')
component_id = fields.Many2one('sos_material', string="Material Name")
new_component_id = fields.Char(string="New Part No")
qty = fields.Integer(string="Required Qty")
req_date = fields.Date(string="Required Date")
mon_ref_no = fields.Many2one('sos_mon',string="MON Ref No")

View File

@ -68,35 +68,35 @@ class SOS_Quote_Generation(models.Model):
supplier_dict = {}
for record in self.line_ids:
if record.supplier1_name:
supplier = record.supplier1_name
if record.final_supplier1_name:
supplier = record.final_supplier1_name
if supplier not in supplier_dict:
supplier_dict[supplier] = []
supplier_dict[supplier].append({
'name': record.material_name,
'product_qty': record.supplier1_qty,
'price_unit': record.supplier1_quoted_price
'product_qty': record.final_supplier1_qty,
'price_unit': record.final_supplier1_quoted_price
})
if record.supplier2_name:
supplier = record.supplier2_name
if supplier not in supplier_dict:
supplier_dict[supplier] = []
supplier_dict[supplier].append({
'name': record.material_name,
'product_qty': record.supplier2_qty,
'price_unit': record.supplier2_quoted_price
})
# if record.supplier2_name:
# supplier = record.supplier2_name
# if supplier not in supplier_dict:
# supplier_dict[supplier] = []
# supplier_dict[supplier].append({
# 'name': record.material_name,
# 'product_qty': record.supplier2_qty,
# 'price_unit': record.supplier2_quoted_price
# })
if record.supplier3_name:
supplier = record.supplier3_name
if supplier not in supplier_dict:
supplier_dict[supplier] = []
supplier_dict[supplier].append({
'name': record.material_name,
'product_qty': record.supplier3_qty,
'price_unit': record.supplier3_quoted_price
})
# if record.supplier3_name:
# supplier = record.supplier3_name
# if supplier not in supplier_dict:
# supplier_dict[supplier] = []
# supplier_dict[supplier].append({
# 'name': record.material_name,
# 'product_qty': record.supplier3_qty,
# 'price_unit': record.supplier3_quoted_price
# })
for x, y in supplier_dict.items():
sequence_util = self.env['sos_common_scripts']
po_no = sequence_util.generate_sequence('sos_po','PO', 'po_no')
@ -112,6 +112,7 @@ class SOS_Quote_Generation(models.Model):
})
message = 'Purchase Order(s) successfully generated.'
return {
'type': 'ir.actions.client',
'tag': 'display_notification',

View File

@ -26,6 +26,7 @@ class SOS_SalesOrder(models.Model):
string="Product Name",required=True)
line_ids = fields.One2many('sos_sales_order_line', 'ref_id',copy=True)
customer_name = fields.Many2one('sos_inventory_customers',string="Customer Name")
customer_location = fields.Char(string="Customer Location")
lead_time = fields.Datetime(string="Lead Time")
customer_po_no = fields.Char(string="PO No")
customer_po_date = fields.Datetime(string="PO Date")

View File

@ -106,7 +106,7 @@ class Sequence_Generator(models.AbstractModel):
reporting_user = env['res.users'].search([('id', '=', users.reporting_to.id)])
# email_to = reporting_user.login
else:
# email_to = "ramachandran.r@sosaley.in"
# email_to = "ramachandran.r@sosaley.com"
print("test")
mail_values = {
'subject': subject,

View File

@ -23,7 +23,17 @@ class SOS_Service_Call_Log_Report(models.Model):
action_taken = fields.Text(string="Action taken or required action to be taken")
Date_of_action_Completed = fields.Datetime(string="Date of action completed")
status = fields.Selection([('open', 'Open'),('close', 'Closed')], default='open' , string="Status")
dock_audit_ref_no = fields.Many2one('sos_dock_audit',string="Dock Audit Ref No")
warranty = fields.Integer(string="Warranty(In Months)")
invoice_no = fields.Char(string="Invoice No")
invoice_date = fields.Date(string="Invoice Date")
@api.onchange('dock_audit_ref_no')
def _onchange_dock_audit_ref_no(self):
if self.dock_audit_ref_no:
self.warranty = self.dock_audit_ref_no.warranty
self.invoice_no = self.dock_audit_ref_no.invoice_no
self.invoice_date = self.dock_audit_ref_no.invoice_date
def _generate_id(self):
sequence_util = self.env['sos_common_scripts']
return sequence_util.generate_sequence('sos_service_call_log_report','SCLR', 'ref_no')

View File

@ -111,6 +111,6 @@ class SOS_SFG_Line(models.Model):
ir_no = fields.Many2one('sos_ir', string="Material Inward Ref No")
min_no = fields.Many2one('sos_min', string="Material Issue Ref No")
mrn_no = fields.Many2one('sos_mrn', string="Material Return Ref No")
iqi_no = fields.Many2one('sos_iqi', string="In-house Inspection Ref No")
iqi_no = fields.Many2one('sos_iqi', string="IQI Ref No")

View File

@ -22,6 +22,10 @@ class sos_Outsourcing_Return(models.Model):
material_name = fields.Many2one('sos_material',string="Material Name",related="iqi_no.material_name")
returned_qty = fields.Integer(string="Returned Quantity",related="iqi_no.rejected_qty")
remarks = fields.Text(string="Remarks")
rework_type = fields.Selection([
('outsourcing', 'Out-Sourcing'),
('inhouse', 'In-House')
], string='Rework Type',default="outsourcing",copy=True)
def _generate_id(self):
sequence_util = self.env['sos_common_scripts']
return sequence_util.generate_sequence('sos_sfg_outsourcing_return_register','ORR', 'orr_no')

View File

@ -138,7 +138,7 @@ class sos__wo(models.Model):
"""
subject = f"Work Order Approval Request - {self.wo_no}"
send_email = self.env['sos_common_scripts']
send_email.send_direct_email(self.env,"sos_wo",self.id,"ramachandran.r@sosaley.in",subject,body_html)
send_email.send_direct_email(self.env,"sos_wo",self.id,"ramachandran.r@sosaley.com",subject,body_html)
# Email part ends
sequence_util = self.env['sos_common_scripts']
return sequence_util.action_assign_signature(

View File

@ -13,126 +13,133 @@
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.basic_layout">
<!-- Include Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous"/>
<style>
/* Reset default margins and ensure full-page layout */
<style type="text/css">
/* Avoid clipping; wkhtmltopdf + overflow hidden can break pagination */
.page {
margin: 3mm; /* Reduced margins */
margin: 3mm;
padding: 0;
width: 100%;
box-sizing: border-box;
overflow: hidden; /* Prevent content from overflowing */
overflow: visible;
}
/* Container for each set of tables (FG, SFG, Materials) */
/* Keep each whole set on one page when possible */
.label-set {
page-break-inside: avoid; /* Try to keep the entire set on one page */
page-break-after: always; /* Force a page break after each set */
}
/* Ensure tables stay within their column */
.table-container {
width: 100%; /* Full width of the column */
page-break-inside: avoid; /* Prevent table from splitting across pages */
box-sizing: border-box;
}
/* Clearfix for the last table if odd number of tables */
.label-set::after {
content: "";
display: block;
clear: both;
}
/* Compact table styling */
.label-table {
width: 100%;
border-collapse: collapse;
page-break-inside: avoid !important;
break-inside: avoid !important;
page-break-after: always; /* one set per page; remove if not desired */
}
/* Pair wrapper: keep both columns together */
.pair-block,
.row,
.col-6,
.table-container,
.label-table,
.label-table tr,
.label-table td {
font-size: 15px; /* Smaller font size for compactness */
padding: 2mm; /* Reduced padding */
border: 1px solid #000;
box-sizing: border-box;
page-break-inside: avoid !important;
break-inside: avoid !important;
}
.label-table .column {
font-weight: bold;
font-size: 14px;
width:20%
}
/* Adjust Bootstrap row margins for PDF */
/* Replace Bootstrap flex with inline-block for PDF reliability */
.row {
margin-left: 0;
margin-right: 0;
margin-bottom:40px;
display: block !important;
margin: 0 0 12px 0;
}
/* Adjust Bootstrap column padding for PDF */
[class*="col-"] {
.col-6 {
display: inline-block !important;
vertical-align: top;
width: 49.8%;
box-sizing: border-box;
padding-left: 5px;
padding-right: 5px;
}
/* Style for empty column placeholder */
.empty-col {
font-size: 10px;
text-align: center;
/* Table layout and text wrapping for long content */
.table-container { width: 100%; box-sizing: border-box; }
.label-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed; /* keep widths predictable */
}
.label-table td {
font-size: 15px;
padding: 2mm;
border: 1px solid #000;
box-sizing: border-box;
white-space: normal; /* allow wrapping */
word-wrap: break-word;
word-break: break-word; /* break long tokens */
}
.label-table .column {
font-weight: bold;
font-size: 14px;
width: 20%;
}
.empty-col { font-size: 10px; text-align: center; padding: 2mm; }
/* Vendor-ish safety (wkhtmltopdf is WebKit) */
@media print {
.pair-block, .row, .col-6, .table-container, .label-table, .label-table tr, .label-table td {
-webkit-region-break-inside: avoid;
}
}
</style>
<div class="page">
<div class="page">
<t t-set="counter" t-value="0"/>
<t t-foreach="range(o.master_quantity)" t-as="line_items">
<t t-set="counter" t-value="counter + 1"/>
<div class="label-set">
<!-- Combine all tables into a single list -->
<!-- Build a single list with: (category_id, record, index) -->
<t t-set="all_tables" t-value="[]"/>
<!-- Add FG tables -->
<!-- FG -->
<t t-set="fg_counter" t-value="0"/>
<t t-foreach="o.line_ids_fg or []" t-as="fg_item">
<t t-set="fg_counter" t-value="fg_counter + 1"/>
<t t-set="all_tables" t-value="all_tables + [(1, fg_item, fg_counter)]"/>
</t>
<!-- Add SFG tables -->
<!-- SFG -->
<t t-set="sfg_counter" t-value="0"/>
<t t-foreach="o.line_ids_sfg or []" t-as="sfg_item">
<t t-set="sfg_counter" t-value="sfg_counter + 1"/>
<t t-set="all_tables" t-value="all_tables + [(2, sfg_item, sfg_counter)]"/>
</t>
<!-- Add Materials tables -->
<!-- Materials -->
<t t-set="material_counter" t-value="0"/>
<t t-foreach="o.line_ids_material or []" t-as="material_item">
<t t-set="material_counter" t-value="material_counter + 1"/>
<t t-set="all_tables" t-value="all_tables + [(3, material_item, material_counter)]"/>
</t>
<!-- Add Installation Kit tables -->
<!-- Installation Kit -->
<t t-set="installation_kit_counter" t-value="0"/>
<t t-foreach="o.line_ids_installation_kit or []" t-as="installation_kit_item">
<t t-set="installation_kit_counter" t-value="installation_kit_counter + 1"/>
<t t-set="all_tables" t-value="all_tables + [(4, installation_kit_item, installation_kit_counter)]"/>
</t>
<!-- Add Miscelleneous tables -->
<!-- Miscellaneous -->
<t t-set="miscellaneous_counter" t-value="0"/>
<t t-foreach="o.line_ids_miscellaneous or []" t-as="miscellaneous_item">
<t t-set="miscellaneous_counter" t-value="miscellaneous_counter + 1"/>
<t t-set="all_tables" t-value="all_tables + [(5, miscellaneous_item, miscellaneous_counter)]"/>
</t>
<!-- Process all tables in pairs -->
<!-- Render in pairs -->
<t t-foreach="range(0, len(all_tables), 2)" t-as="pair_index">
<div class="pair-block">
<div class="row">
<!-- First table in the pair -->
<!-- Left column -->
<div class="col-6">
<t t-set="table_info" t-value="all_tables[pair_index]"/>
<t t-set="category" t-value="table_info[0]"/>
@ -157,7 +164,7 @@
<tr>
<td class="column">Name</td>
<td>
<t t-if="'component_id' in item">
<t t-if="'component_id' in item._fields">
<t t-esc="item.component_id.name or item.component_id.part_no or item.name or 'N/A'"/>
</t>
<t t-else="">
@ -168,7 +175,7 @@
<tr>
<td class="column">UOM</td>
<td>
<t t-if="'uom' in item">
<t t-if="'uom' in item._fields">
<t t-esc="item.uom or 'N/A'"/>
</t>
<t t-else="">
@ -179,15 +186,19 @@
<tr>
<td class="column">Qty</td>
<td>
<t t-if="'singet_set_qty' in item">
<t t-if="category == 5">
<t t-esc="(item.quantity or 0) // (o.master_quantity or 1)"/>
</t>
<t t-else="">
<t t-if="'singet_set_qty' in item._fields">
<t t-esc="item.singet_set_qty or 'N/A'"/>
</t>
<t t-else="">
<t t-esc="item.quantity or 'N/A'"/>
</t>
</t>
</td>
</tr>
</tr>
<tr>
<td class="column">S.No</td>
@ -198,13 +209,14 @@
</div>
</div>
<!-- Second table in the pair (if exists) -->
<!-- Right column -->
<div class="col-6">
<t t-if="pair_index + 1 &lt; len(all_tables)">
<t t-set="table_info" t-value="all_tables[pair_index + 1]"/>
<t t-set="category" t-value="table_info[0]"/>
<t t-set="item" t-value="table_info[1]"/>
<t t-set="index" t-value="table_info[2]"/>
<div class="table-container">
<table class="label-table">
<tbody>
@ -223,18 +235,24 @@
<tr>
<td class="column">Name</td>
<td>
<t t-if="'component_id' in item">
<t t-esc="item.component_id.name or item.component_id.part_no or item.name or 'N/A'"/>
<t t-if="'component_id' in item._fields">
<t t-if="category in [3, 4]">
<t t-esc="item.component_id.part_no or item.name or 'N/A'"/>
</t>
<t t-else="">
<t t-esc="item.component_id.name or item.name or 'N/A'"/>
</t>
</t>
<t t-else="">
<t t-esc="item.name or 'N/A'"/>
</t>
</td>
</tr>
</tr>
<tr>
<td class="column">UOM</td>
<td>
<t t-if="'uom' in item">
<t t-if="'uom' in item._fields">
<t t-esc="item.uom or 'N/A'"/>
</t>
<t t-else="">
@ -245,14 +263,20 @@
<tr>
<td class="column">Qty</td>
<td>
<t t-if="'singet_set_qty' in item">
<t t-if="category == 5">
<t t-esc="(item.quantity or 0) // (o.master_quantity or 1)"/>
</t>
<t t-else="">
<t t-if="'singet_set_qty' in item._fields">
<t t-esc="item.singet_set_qty or 'N/A'"/>
</t>
<t t-else="">
<t t-esc="item.quantity or 'N/A'"/>
</t>
</t>
</td>
</tr>
</tr>
<tr>
<td class="column">S.No</td>
<td></td>
@ -265,13 +289,16 @@
<div class="empty-col"></div>
</t>
</div>
</div> <!-- Close row -->
</div> <!-- /row -->
</div> <!-- /pair-block -->
</t>
</div> <!-- /label-set -->
</t>
</div> <!-- /page -->
</t>
</t>
</div>
</t>
</div>
</t>
</t>
</t>
</template>
</odoo>

View File

@ -0,0 +1,91 @@
<odoo>
<record id="action_material_budget_summary" model="ir.actions.report">
<field name="name">Material Budget Summary</field>
<field name="model">sos_budget_plan</field> <!-- Adjust model if needed -->
<field name="report_type">qweb-html</field>
<field name="report_name">sos_inventory.report_material_budget_summary</field>
<field name="print_report_name">Material_Budget_Summary</field>
</record>
<template id="report_material_budget_summary">
<t t-call="web.basic_layout">
<t t-call="web.html_container">
<style>
body { font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; color: #1f2937; }
.page { max-width: 1200px; margin: 0 auto; padding: 20px; }
.header { border-bottom: 2px solid #e5e7eb; padding-bottom: 16px; margin-bottom: 24px; }
.header h3 { font-size: 24px; font-weight: 700; margin: 0; color: #1e40af; }
.header .info { font-size: 14px; color: #4b5563; margin-top: 8px; }
.header .info strong { font-weight: 600; }
.section-title { font-size: 18px; font-weight: 600; color: #1e40af; margin: 24px 0 12px; }
.count { font-size: 14px; color: #4b5563; margin-bottom: 8px; }
.table-custom { width: 100%; border-collapse: separate; border-spacing: 0; background-color: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border-radius: 8px; overflow: hidden; }
.table-custom th { background-color: #e4e0f9; color: #1e40af; font-weight: 600; padding: 12px; text-align: left; border-bottom: 1px solid #d1d5db; }
.table-custom td { padding: 12px; border-bottom: 1px solid #e5e7eb; }
.table-custom tr:last-child td { border-bottom: none; }
.table-custom th.center, .table-custom td.center { text-align: center; }
.no-data { text-align: center; color: #6b7280; padding: 16px; font-style: italic; }
.total-cost{
display:flex; justify-content:flex-end; align-items:center; gap:8px;
padding:8px 10px; margin:10px 0 6px;
background:#fff; border:1px solid #e5e7eb; border-radius:8px;
font-size:14px;
}
.total-cost .label{ color:#4b5563; }
.total-cost .amount{ font-weight:700; font-size:16px; }
</style>
<div class="page">
<div class="header">
<h3>Budget Summary</h3>
<div class="info">
<strong>Name:</strong> <t t-esc="fg_name or '—'"/>
| <strong>Planned Quantity:</strong> <t t-esc="total_quantity or '—'"/>
</div>
</div>
<!-- Material Quantities Table -->
<h4 class="section-title">Materials to Purchase</h4>
<div class="count">Total Materials: <t t-esc="len(materials)"/></div>
<div class="total-cost">
<span class="label">Estimated Purchase Cost:</span>
<span class="amount">
<t t-esc="currency_symbol"/> <t t-esc="'{:,.2f}'.format(total_cost)"/>
</span>
</div>
<table class="table-custom">
<thead>
<tr>
<th>Material Name</th>
<th class="center">Needed Qty</th>
<th class="center">In-Hand Qty</th>
<th class="center">Actual Needed Qty</th>
<th class="center">Standard Packing Qty</th>
<th class="center">To Purchase</th>
<th class="center">cost</th>
</tr>
</thead>
<tbody>
<t t-if="materials">
<t t-foreach="materials" t-as="material">
<tr>
<td><t t-esc="material['material_name']"/></td>
<td class="center"><t t-esc="material['needed_quantity']"/></td>
<td class="center"><t t-esc="material['inhand_quantity']"/></td>
<td class="center"><t t-esc="material['actual_needed']"/></td>
<td class="center"><t t-esc="material['std_packing_qty']"/></td>
<td class="center"><t t-esc="material['to_purchase']"/></td>
<td class="center"><t t-esc="material['cost']"/></td>
</tr>
</t>
</t>
<t t-if="not materials or len(materials) == 0">
<tr><td colspan="4" class="no-data">No materials need to be purchased.</td></tr>
</t>
</tbody>
</table>
</div>
</t>
</t>
</template>
</odoo>

View File

@ -72,15 +72,17 @@
<table class="table table-bordered">
<tbody>
<t t-if="o.material_option == True">
<tr style="background-color:#ccc"><td class="column" colspan="6">Materials</td></tr>
<tr style="background-color:#ccc"><td class="column" colspan="7">Materials</td></tr>
<tr class="column">
<td>S.No</td>
<td class="column">Material Code</td>
<td class="column">Name</td>
<td class="column">Location</td>
<td class="column">In-Hand Quantity</td>
<td class="column">Requested Quantity</td>
<td class="column">Given Quantity</td>
</tr>
<t t-foreach="o.line_ids_material" t-as="line_items">
@ -95,6 +97,9 @@
<td><t t-esc="line_items.new_component_id"/></td>
</t>
<t t-if="line_items.com_type == 'exits'">
<td><t t-esc="line_items.location"/></td>
</t>
<t t-if="line_items.com_type == 'exits'">
<td><t t-esc="line_items.inhand_stock_qty"/></td>
</t>
<t t-if="line_items.com_type == 'new'">

View File

@ -56,6 +56,9 @@ access_ir_act_window_healthcare_user,Access Action Window for Health Care Users,
access_ir_act_window_sos_inside_sales_user,Access Action Window for Inside sales Users,base.model_ir_actions_act_window,sos_inventory.sos_inside_sales_user,1,0,0,0
access_ir_act_window_hr_user,Access Action Window for Human Care Resources Users,base.model_ir_actions_act_window,sos_inventory.sos_hr_user,1,0,0,0
access_ir_act_window_sos_marketing_user,Access Action Window for Marketing Users,base.model_ir_actions_act_window,sos_inventory.sos_marketing_user,1,0,0,0
access_ir_act_window_sos_sales_reviewer,Access Action Window for Sales Reviewer,base.model_ir_actions_act_window,sos_inventory.sos_sales_reviewer,1,0,0,0
access_ir_act_window_finance_head_user,Access Action Window for Finance Head,base.model_ir_actions_act_window,sos_inventory.sos_finance_head_user,1,0,0,0
access_ir_act_window_sales_sapl_user,Access Action Window for Sales SAPL Users,base.model_ir_actions_act_window,sos_inventory.sos_sales_sapl_user,1,0,0,0
access_sos_return_fir,sos_return_fir access,model_sos_return_fir,base.group_user,1,1,1,1
access_sos_return_fir_line,sos_return_fir_line access,model_sos_return_fir_line,base.group_user,1,1,1,1
access_sos_return_calibration_devices_fir,sos_return_calibration_devices_fir access,model_sos_return_calibration_devices_fir,base.group_user,1,1,1,1
@ -182,6 +185,7 @@ access_sos_transfer_challan_specification_lines,sos_transfer_challan_specificati
access_sos_fir_serial_no_lines,sos_fir_serial_no_lines access,model_sos_fir_serial_no_lines,base.group_user,1,1,1,1
access_sos_miscellaneous_deliverables,sos_miscellaneous_deliverables access,model_sos_miscellaneous_deliverables,base.group_user,1,1,1,1
access_sos_mat_outsourcing_vendor_register,sos_mat_outsourcing_vendor_register access,model_sos_mat_outsourcing_vendor_register,base.group_user,1,1,1,1
access_sos_mat_outsourcing_vendor_register_lines,sos_mat_outsourcing_vendor_register_lines access,model_sos_mat_outsourcing_vendor_register_lines,base.group_user,1,1,1,1
access_stock_movement_report_wizard,stock_movement_report_wizard access,model_stock_movement_report_wizard,base.group_user,1,1,1,1
access_sos_inventory_customers,sos_inventory_customers access,model_sos_inventory_customers,base.group_user,1,1,1,1
access_sos_master_list_customer,sos_master_list_customer access,model_sos_master_list_customer,base.group_user,1,1,1,1
@ -197,5 +201,11 @@ access_sos_parameter_fir,sos_parameter_fir access,model_sos_parameter_fir,base.g
access_sos_transfer_challan_return_from_customer_lines,sos_transfer_challan_return_from_customer_lines access,model_sos_transfer_challan_return_from_customer_lines,base.group_user,1,1,1,1
access_sos_transfer_challan_return_from_customer,sos_transfer_challan_return_from_customer access,model_sos_transfer_challan_return_from_customer,base.group_user,1,1,1,1
access_sos_shelflife_register,sos_shelflife_register access,model_sos_shelflife_register,base.group_user,1,1,1,1
access_sos_budget_plan,sos_budget_plan access,model_sos_budget_plan,base.group_user,1,1,1,1
access_sos_prf,sos_prf access,model_sos_prf,base.group_user,1,1,1,1
access_sos_prf_lines,sos_prf_lines access,model_sos_prf_lines,base.group_user,1,1,1,1
access_sos_departments_user_lines,sos_departments_user_lines access,model_sos_departments_user_lines,base.group_user,1,1,1,1
access_sos_ncmr_problem_description_line,access_sos_ncmr_problem_description_line access,model_sos_ncmr_problem_description_line,base.group_user,1,1,1,1
access_sos_ccrf_capa_line,access_sos_ccrf_capa_line access,model_sos_ccrf_capa_line,base.group_user,1,1,1,1
access_sos_ccrf_team_formation_line,access_sos_ccrf_team_formation_line access,model_sos_ccrf_team_formation_line,base.group_user,1,1,1,1
access_sos_ccrf_problem_description_line,access_sos_ccrf_problem_description_line access,model_sos_ccrf_problem_description_line,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
56 access_ir_act_window_sos_inside_sales_user Access Action Window for Inside sales Users base.model_ir_actions_act_window sos_inventory.sos_inside_sales_user 1 0 0 0
57 access_ir_act_window_hr_user Access Action Window for Human Care Resources Users base.model_ir_actions_act_window sos_inventory.sos_hr_user 1 0 0 0
58 access_ir_act_window_sos_marketing_user Access Action Window for Marketing Users base.model_ir_actions_act_window sos_inventory.sos_marketing_user 1 0 0 0
59 access_ir_act_window_sos_sales_reviewer Access Action Window for Sales Reviewer base.model_ir_actions_act_window sos_inventory.sos_sales_reviewer 1 0 0 0
60 access_ir_act_window_finance_head_user Access Action Window for Finance Head base.model_ir_actions_act_window sos_inventory.sos_finance_head_user 1 0 0 0
61 access_ir_act_window_sales_sapl_user Access Action Window for Sales SAPL Users base.model_ir_actions_act_window sos_inventory.sos_sales_sapl_user 1 0 0 0
62 access_sos_return_fir sos_return_fir access model_sos_return_fir base.group_user 1 1 1 1
63 access_sos_return_fir_line sos_return_fir_line access model_sos_return_fir_line base.group_user 1 1 1 1
64 access_sos_return_calibration_devices_fir sos_return_calibration_devices_fir access model_sos_return_calibration_devices_fir base.group_user 1 1 1 1
185 access_sos_fir_serial_no_lines sos_fir_serial_no_lines access model_sos_fir_serial_no_lines base.group_user 1 1 1 1
186 access_sos_miscellaneous_deliverables sos_miscellaneous_deliverables access model_sos_miscellaneous_deliverables base.group_user 1 1 1 1
187 access_sos_mat_outsourcing_vendor_register sos_mat_outsourcing_vendor_register access model_sos_mat_outsourcing_vendor_register base.group_user 1 1 1 1
188 access_sos_mat_outsourcing_vendor_register_lines sos_mat_outsourcing_vendor_register_lines access model_sos_mat_outsourcing_vendor_register_lines base.group_user 1 1 1 1
189 access_stock_movement_report_wizard stock_movement_report_wizard access model_stock_movement_report_wizard base.group_user 1 1 1 1
190 access_sos_inventory_customers sos_inventory_customers access model_sos_inventory_customers base.group_user 1 1 1 1
191 access_sos_master_list_customer sos_master_list_customer access model_sos_master_list_customer base.group_user 1 1 1 1
201 access_sos_transfer_challan_return_from_customer_lines sos_transfer_challan_return_from_customer_lines access model_sos_transfer_challan_return_from_customer_lines base.group_user 1 1 1 1
202 access_sos_transfer_challan_return_from_customer sos_transfer_challan_return_from_customer access model_sos_transfer_challan_return_from_customer base.group_user 1 1 1 1
203 access_sos_shelflife_register sos_shelflife_register access model_sos_shelflife_register base.group_user 1 1 1 1
204 access_sos_budget_plan sos_budget_plan access model_sos_budget_plan base.group_user 1 1 1 1
205 access_sos_prf sos_prf access model_sos_prf base.group_user 1 1 1 1
206 access_sos_prf_lines sos_prf_lines access model_sos_prf_lines base.group_user 1 1 1 1
207 access_sos_departments_user_lines sos_departments_user_lines access model_sos_departments_user_lines base.group_user 1 1 1 1
208 access_sos_ncmr_problem_description_line access_sos_ncmr_problem_description_line access model_sos_ncmr_problem_description_line base.group_user 1 1 1 1
209 access_sos_ccrf_capa_line access_sos_ccrf_capa_line access model_sos_ccrf_capa_line base.group_user 1 1 1 1
210 access_sos_ccrf_team_formation_line access_sos_ccrf_team_formation_line access model_sos_ccrf_team_formation_line base.group_user 1 1 1 1
211 access_sos_ccrf_problem_description_line access_sos_ccrf_problem_description_line access model_sos_ccrf_problem_description_line base.group_user 1 1 1 1

View File

@ -153,4 +153,24 @@
</record>
<record id="sos_mon_ce_team_rule" model="ir.rule">
<field name="name">MON: CE Team View CE Records</field>
<field name="model_id" ref="model_sos_mon"/>
<field name="domain_force">[('is_ce_user_created', '=', True)]</field>
<field name="groups" eval="[(4, ref('sos_inventory.sos_ce_user'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="0"/>
</record>
<record id="sos_mrn_ce_team_rule" model="ir.rule">
<field name="name">MRN: CE Team View CE Records</field>
<field name="model_id" ref="model_sos_mrn"/>
<field name="domain_force">[('is_ce_user_created', '=', True)]</field>
<field name="groups" eval="[(4, ref('sos_inventory.sos_ce_user'))]"/>
<field name="perm_read" eval="1"/>
<field name="perm_write" eval="1"/>
<field name="perm_create" eval="1"/>
<field name="perm_unlink" eval="0"/>
</record>
</odoo>

View File

@ -75,6 +75,12 @@
<field name="category_id"
ref="sos_inventory.module_category_sos_inventory"/>
</record>
<!-- Finance Head Group -->
<record id="sos_finance_head_user" model="res.groups">
<field name="name">Finance Head</field>
<field name="category_id"
ref="sos_inventory.module_category_sos_inventory"/>
</record>
<!-- Logistics Group -->
<record id="sos_logistics_user" model="res.groups">
<field name="name">Logistics User</field>
@ -97,7 +103,7 @@
<field name="category_id"
ref="sos_inventory.module_category_sos_inventory"/>
</record>
<!-- Finance Group -->
<!-- Haelthcare Group -->
<record id="sos_healthcare_user" model="res.groups">
<field name="name">Healthcare User</field>
<field name="category_id"
@ -121,6 +127,17 @@
<field name="category_id"
ref="sos_inventory.module_category_sos_inventory"/>
</record>
<!-- Sales/Inside Reviewer -->
<record id="sos_sales_reviewer" model="res.groups">
<field name="name">Sales Reviewer</field>
<field name="category_id"
ref="sos_inventory.module_category_sos_inventory"/>
</record>
<!-- Sales/Inside Reviewer -->
<record id="sos_sales_sapl_user" model="res.groups">
<field name="name">Sales SAPL</field>
<field name="category_id"
ref="sos_inventory.module_category_sos_inventory"/>
</record>
</data>
</odoo>

View File

@ -2,6 +2,13 @@ body{
font-size: 15px;
font-family: sans-serif;
}
.modal.o_technical_modal .modal-content .modal-header .modal-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: #fff;
}
.btn-primary {
color: #FFFFFF;
background-color: #71639e !important;

View File

@ -0,0 +1,344 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { Component, onWillStart, useRef, onMounted } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
export class ParetoChartWidget extends Component {
static template = "sos_inventory.ParetoChartWidget";
setup() {
this.orm = useService("orm");
this.action = useService("action");
this.chartRef = useRef("paretoChart");
this.startDateRef = useRef("startDate");
this.endDateRef = useRef("endDate");
this.noDataMessageRef = useRef("endDate");
this.goodsTypeRef = useRef("goodsType");
this.categoryRef = useRef("category");
this.servicesupplierRef = useRef("servicesupplier");
this.chartInstance = null; // Track chart instance
// 👉 Pre-fill start & end date when component is mounted
onMounted(() => {
const today = new Date();
// First and last day of the month
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
this.startDateRef.el.value = formatDate(firstDay);
this.endDateRef.el.value = formatDate(lastDay);
//this.onGoodsTypeChange();
//this.onServiceSupplierChange();
this.loadData();
});
}
async onServiceSupplierChange() {
const goods_type = this.goodsTypeRef.el.value;
const servicesupplierSelect = this.servicesupplierRef.el;
const startDate = this.startDateRef.el.value || false;
const endDate = this.endDateRef.el.value || false;
//alert(selectedGoodsType+" FFFF :::: "+categorySelect);
// Clear existing options
servicesupplierSelect.innerHTML = '';//last 12.16 22/08/2025
const defaultOption = document.createElement('option');
defaultOption.text = 'Select Supplier/Service';
defaultOption.disabled = true;
defaultOption.selected = true;
servicesupplierSelect.appendChild(defaultOption);
//alert(" selectedGoodsType :::: "+selectedGoodsType);
if (goods_type != "FG"){
try {
// Call backend route to get categories by goodsType
const categories = await this.orm.call('sos_ncmr', 'get_service_suppliers_by_goods_type', [goods_type, startDate, endDate]);
if (!categories.length) {
servicesupplierSelect.innerHTML = '';
const option = document.createElement('option');
option.text = 'No Data Found';
option.disabled = true;
servicesupplierSelect.appendChild(option);
} else {
servicesupplierSelect.innerHTML = '';
categories.forEach(cat => {
const option = document.createElement('option');
option.text = cat.supplier_name;
option.value = cat.id;
servicesupplierSelect.appendChild(option);
});
}
} catch (error) {
console.error('Failed to fetch categories:', error);
const option = document.createElement('option');
option.text = 'Error loading categories';
option.disabled = true;
servicesupplierSelect.appendChild(option);
}
}
}
async onGoodsTypeChange() {
this.onServiceSupplierChange();
const selectedGoodsType = this.goodsTypeRef.el.value;
const categorySelect = this.categoryRef.el;
categorySelect.innerHTML = '';
const defaultOption = document.createElement('option');
defaultOption.text = 'Select Category';
defaultOption.disabled = true;
defaultOption.selected = true;
categorySelect.appendChild(defaultOption);
let categories = [];
if (selectedGoodsType == 'Material'){
categories = [
{'id': '', 'name': 'Select Category'},
{'id': 'Active Components', 'name': 'Active Components'},
{'id': 'Passive Components', 'name': 'Passive Components'},
{'id': 'Fabrication', 'name': 'Fabrication'},
{'id': 'Accessories', 'name': 'Accessories'},
{'id': 'Cables & Connectors', 'name': 'Cables & Connectors'},
{'id': 'Metal', 'name': 'Metal'},
]
}else if (selectedGoodsType == 'SFG'){
categories = [
{'id': '', 'name': 'Select Category'},
{'id': 'pcba', 'name': 'PCB Board'},
{'id': 'cables', 'name': 'Cables & Connectors'},
{'id': 'others', 'name': 'Others'},
]
}else if (selectedGoodsType == 'FG'){
categories = [
{'id': '', 'name': 'Select Category'},
{'id': 'Health Care', 'name': 'Health Care'},
{'id': 'BHMS', 'name': 'BHMS'},
{'id': 'LV-BMS', 'name': 'BMS-LV'},
{'id': 'HV-BMS', 'name': 'BMS-HV'},
{'id': 'SBMS', 'name': 'SBMS'},
{'id': 'Motor Controller', 'name': 'Motor Controller'},
]
}
try {
// Call backend route to get categories by goodsType
if (!categories.length) {
const option = document.createElement('option');
option.text = 'No Categories Found';
option.disabled = true;
categorySelect.appendChild(option);
} else {
categorySelect.innerHTML = '';
categories.forEach(cat => {
const option = document.createElement('option');
option.text = cat.name;
option.value = cat.id;
categorySelect.appendChild(option);
});
}
} catch (error) {
console.error('Failed to fetch categories:', error);
const option = document.createElement('option');
option.text = 'Error loading categories';
option.disabled = true;
categorySelect.appendChild(option);
}
}
async onFilterClick() {
await this.loadData();
}
showNoDataMessage() {
this.noDataMessageRef.el.style.display = "block";
if (this.chartInstance) {
this.chartInstance.destroy();
this.chartInstance = null;
}
}
hideNoDataMessage() {
this.noDataMessageRef.el.style.display = "none";
}
async loadData() {
const startDate = this.startDateRef.el.value || false;
const endDate = this.endDateRef.el.value || false;
const goodstype = this.goodsTypeRef.el.value || false;
const categoryId = this.categoryRef.el.value || false;
const servicesupplier = this.servicesupplierRef.el.value || false;
//alert(" categoryId :: "+categoryId);
if (!startDate || !endDate) {
// Optionally show a message or just return
console.warn("Start and end date are required.");
return;
}
const domain = [];
if (startDate) domain.push(['date', '>=', startDate]);
if (endDate) domain.push(['date', '<=', endDate]);
let records = [];
try {
records = await this.orm.call("sos_ncmr", "get_pareto_data", [startDate, endDate, goodstype, categoryId, servicesupplier]); //, goodstype , categoryId
} catch (error) {
console.error("Failed to fetch pareto data:", error);
//this.showNoDataMessage();
//return;
}
// if (!records || records.length === 0) {
// this.showNoDataMessage();
// return;
// }
//this.hideNoDataMessage();
this.renderChart(records); // ✅ cleaner and reusable
// const records = await this.orm.searchRead("pareto.defect.report", [], [
// "defect_name", "count", "individual_percent", "cumulative_percent"
// ]);
}
renderChart(records){
const labels = records.map(r => r.defect_name);
const counts = records.map(r => r.count);
const cumulative = records.map(r => r.cumulative_percent);
//const ctx = this.el.querySelector("#paretoChart").getContext("2d");
const threshold = 80;
let thresholdLine = [];
let thresholdReached = false;
for (let i = 0; i < cumulative.length; i++) {
if (!thresholdReached) {
thresholdLine.push(threshold);
if (cumulative[i] >= threshold) {
// Insert drop to bottom after this
thresholdLine.push(0); // drop vertically
thresholdLine.push(null); // stop the line
break;
}
}
}
const ctx = this.chartRef.el.getContext("2d");
// Plugin to draw lines after bars
const drawLinesLastPlugin = {
id: 'drawLinesLast',
afterDatasetsDraw(chart) {
const ctx = chart.ctx;
chart.data.datasets.forEach((dataset, index) => {
if (dataset.type === 'line') {
const meta = chart.getDatasetMeta(index);
meta.controller.draw(ctx);
}
});
}
};
// ✅ Destroy previous chart if exists
if (this.chartInstance) {
this.chartInstance.destroy();
}
// ✅ Create new chart and store it
this.chartInstance = new Chart(ctx, {
type: 'bar',
data: {
labels,
datasets: [
{
label: "Defect Qty",
data: counts,
backgroundColor: "steelblue",
yAxisID: 'y',
order: 1,
},
{
label: "Cumulative %",
data: cumulative,
type: "line",
borderColor: "green",
yAxisID: 'y1',
tension: 0.4,
order: 2,
},
{
label: "80% Threshold",
data: new Array(records.length).fill(80),
//data: thresholdLine,
type: "line",
borderColor: "red",
borderDash: [5, 5],
pointRadius: 0,
fill: false,
yAxisID: 'y1',
spanGaps: false,
stepped: true,
order: 3,
}
]
},
options: {
responsive: true,
plugins: {
legend: { position: "bottom" },
title: {
display: true,
text: "" //Pareto Chart - cables, Cables & Connectors
}
},
scales: {
y: {
type: 'linear',
position: 'left',
title: { display: true, text: 'Defect Count' },
beginAtZero: true
},
y1: {
type: 'linear',
position: 'right',
title: { display: true, text: 'Percentage' },
beginAtZero: true,
min: 0,
max: 120,
ticks: {
callback: function (value) {
return value + "%";
}
},
grid: { drawOnChartArea: false }
}
}
},
plugins: [drawLinesLastPlugin],
});
}
}
//ParetoChartWidget.template = "sos_inventory.ParetoChartWidget";
registry.category("actions").add("pareto_chart_widget", ParetoChartWidget);

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="sos_inventory.ParetoChartWidget">
<!-- <div class="container mt-3">
<canvas t-ref="paretoChart" id="paretoChart" width="100%" height="40"></canvas>
</div> -->
<div class="o_pareto_chart_container">
<div class="o_date_filter_controls" style="margin-left:20px;">
<table>
<tr>
<td>
<div class="form-group">
<label>Start Date:</label><br />
<input type="date" t-ref="startDate" class="form-control" style="min-width: 150px;" />
</div>
</td>
<td>
<div class="form-group">
<label>End Date:</label><br />
<input type="date" t-ref="endDate" class="form-control" style="min-width: 150px;" />
</div>
</td>
<td>
<div class="form-group">
<label>Goods Type:</label><br />
<select t-ref="goodsType" t-on-change="onGoodsTypeChange" class="form-select" style="min-width: 150px;">
<option selected="selected">Material</option>
<option>SFG</option>
<option>FG</option>
</select>
</div>
</td>
<td>
<div class="form-group">
<label>Category:</label><br />
<select t-ref="category" class="form-select" style="min-width: 150px;">
<option selected="selected" disabled="disabled">Select Category</option>
</select>
</div>
</td>
<td>
<div class="form-group">
<label>Supplier/Service:</label><br />
<select t-ref="servicesupplier" class="form-select" style="min-width: 150px;">
<option selected="selected" disabled="disabled">Select Supplier/Service</option>
</select>
</div>
</td>
<td>
<div class="form-group" style="margin-top: 1.5rem;">
<button t-on-click="onFilterClick" class="btn btn-primary">Graph</button>
</div>
</td>
</tr>
</table>
</div>
<!-- Placeholder for No Data -->
<div t-ref="noDataMessage" class="o_no_data_message" style="display: none; text-align: center; font-weight: bold; color: red; margin: 2rem;">
No Data Available for Selected Dates
</div>
<!-- <button class="btn btn-primary o-default-button" style="display:none;">Ok</button> -->
<canvas t-ref="paretoChart" id="paretoChart" width="1600" height="600"></canvas>
</div>
</t>
</templates>

View File

@ -46,10 +46,7 @@
<menuitem id="mop_forms_menu_root"
name="MOP FORMS"
parent="forms_menu_root"/>
<!-- CE Forms -->
<menuitem id="ce_forms_menu_root"
name="CE FORMS"
parent="forms_menu_root"/>
<!-- End CE Forms -->
<menuitem id="mme_forms_menu_root"
name="MME FORMS"

View File

@ -0,0 +1,8 @@
<odoo>
<record id="action_pareto_chart" model="ir.actions.client">
<field name="name">Pareto Chart Analysis</field>
<field name="tag">pareto_chart_widget</field>
</record>
</odoo>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="action_budget_plan_list" model="ir.actions.act_window">
<field name="name">Budget Plan</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sos_budget_plan</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No Plan Found
</p>
</field>
</record>
<record id="sos_budget_plan_view_tree" model="ir.ui.view">
<field name="name">sos_budget_plan.view.tree</field>
<field name="model">sos_budget_plan</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="quantity"/>
<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="sos_budget_plan_form_view" model="ir.ui.view">
<field name="name">Form</field>
<field name="model">sos_budget_plan</field>
<field name="arch" type="xml">
<form string="Budget Plan">
<sheet>
<h2 style="text-align: center;text-transform: uppercase;text-shadow: 1px 1p 1px #140718;color: #65407c;padding:5px;">Budget Plan</h2><hr></hr><br></br>
<table class="table table" style="box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;">
<tr><td><group><field name="sfg_option"/></group></td>
<td><group><field name="fg_option"/></group></td></tr>
</table>
<br></br>
<group>
<!-- Left Column -->
<group>
<field name="name"/>
<field name="quantity"/>
</group>
<!-- Right Column -->
<group>
<field name="fg_name" invisible="sfg_option"/>
<field name="sfg_name" invisible="fg_option"/>
</group>
<button class="btn btn-primary" type="object" name="create_budget_plan" string="Execute"/>
</group>
</sheet>
</form>
</field>
</record>
<menuitem id="budget_plan_menu"
name="Budget Plan"
parent="indent_menu_root"
action = "action_budget_plan_list"/>
</odoo>

View File

@ -59,22 +59,24 @@
<field name="helper_field" invisible="1"/>
<field name="sub_fg_name" domain="[('id', 'in', helper_field)]"/>
<field name="batch_no"/>
<field name="invoice_no"/>
<field name="batch_release_date"/>
<field name="quantity_dispatched"/>
</group>
</group>
<notebook>
<page string="Data Collection">
<group>
<page string="D0: Defective Details/Symptom">
<group>
<!-- <group>
<field name="invoice_no"/>
<field name="batch_release_date"/>
<field name="quantity_dispatched"/>
</group>
</group> -->
<group>
<field name="brcoa_no"/>
<field name="brcoa_date"/>
<!-- <field name="brcoa_no"/>
<field name="brcoa_date"/> -->
<field name="other_complaints"/>
</group>
</group>
@ -102,13 +104,13 @@
</div>
</templates>
</page>
<page string="Investigation Details">
<page string="D0:Investigation Details">
<group>
<group>
<field name="device_failed_to_meet_specification"/>
<field name="labelling_error"/>
<field name="ifu"/>
<field name="interin_containment_actions"/>
<!-- <field name="interin_containment_actions"/> -->
</group>
<group>
<field name="packaging_details"/>
@ -140,8 +142,59 @@
</div>
</templates>
</page>
<page string="Root Cause">
<page string="D1:Team Formation &amp; Document Reference">
<!-- <field name="team_formation" /> -->
<field name="team_formation_line_ids">
<tree editable="bottom">
<field name="name"/>
<field name="department"/>
<field name="role"/>
</tree>
</field>
<div class="oe_note">
<strong>Note:</strong> <div>Form a cross-functional team with process knowledge and authority. Assign roles and responsibilities.</div>
</div>
</page>
<page string="D2:Problem Description">
<!-- <field name="problem_description"/> -->
<field name="problem_description_line_ids">
<tree editable="bottom">
<field name="clear_statement"/>
<field name="photos"/>
<field name="what"/>
<field name="where"/>
<field name="when"/>
<field name="why"/>
<field name="who"/>
<field name="how"/>
<field name="how_many"/>
</tree>
</field>
<div class="oe_note">
<strong>Note:</strong> <div>Clearly define the problem: What, Where, When, Why, Who, How,How Much.Use measurable facts(not assumptions).</div>
</div>
</page>
<page string="D3:Containment Actions">
<group>
<group>
<field name="interin_containment_actions"/>
</group>
</group>
<div class="oe_note">
<strong>Note:</strong> <div>Temporary actions to isolate the problem and protect the customer.Example: Stop shipment,sort defective parts,replace or rework.</div>
</div>
</page>
<page string="D4:Root Cause Analysis">
<field name="root_cause"/>
<div class="oe_note">
<strong>Note:</strong> <div>Identify the true root cause.Tools:5 Whys,Fishbone Diagram,Fault Tree Analysis,FMEA. </div>
</div>
<templates>
<div class="row">
<div class="col-4">
@ -183,7 +236,35 @@
</templates>
</page>
<page string="CAPA">
<group>
<field name="capa_line_ids">
<tree editable="bottom">
<field name="issue"/>
<field name="corrective_action"/>
<field name="implement_validate_corrective_action"/>
<field name="preventive_action"/>
</tree>
</field>
<table>
<tr>
<td>
<div class="oe_note">
<strong>Note:</strong><div> Define and verify permanent corrective actions to eliminate the root cause.Validate effectiveness before full implementation.</div>
</div>
</td>
<td>
<div class="oe_note">
<strong>Note:</strong> <div>Apply the corrective actions,monitor effectiveness, and confirm the issue is resolved.</div>
</div>
</td>
<td>
<div class="oe_note">
<strong>Note:</strong> <div>Update processes, procedures, training, FMEA, control plans, Process Automation flows.Ensure similar issues don't happen elsewhere. </div>
</div>
</td>
</tr>
</table>
<!-- <group>
<group>
<field name="corrective_action"/>
</group>
@ -193,7 +274,7 @@
<group>
<field name="capa_status"/>
</group>
</group>
</group> -->
<templates>
<div class="row">
<div class="col-6"></div>
@ -217,7 +298,7 @@
</div>
</templates>
</page>
<page string="Response">
<page string="D8: Team Recognition">
<group>
<group>
<field name="response_sent_date"/>
@ -228,7 +309,16 @@
<group>
<field name="qa_comments"/>
</group>
<group>
<field name="capa_status"/>
</group>
<group>
<field name="team_recognition" />
</group>
</group>
<div class="oe_note">
<strong>Note:</strong> <div>Appreciate and recognize the team's effort.Helps motivation and organizational learning. </div>
</div>
<templates>
<div class="row">
<div class="col-6"></div>
@ -266,9 +356,5 @@
</form>
</field>
</record>
<menuitem id="ccrf_menu"
name="Customer Complaint Registration Form(CCRF)"
parent="ce_forms_menu_root" action="action_ccrf_list" groups="sos_inventory.sos_production_user,sos_inventory.sos_rd_user,sos_inventory.sos_healthcare_user,sos_inventory.sos_qa_user,sos_inventory.sos_qc_user,sos_inventory.sos_ce_user,sos_inventory.sos_scg_group_user,sos_inventory.sos_management_user,sos_inventory.sos_sales_user"/>
</odoo>

View File

@ -84,7 +84,7 @@ name="action_report_dc_btn"><i class="fa fa-print"></i> Print</button>
<notebook>
<page string="Outsourcing" invisible="dc_type != 'out_souce_return' and dc_type != 'out_souce_noreturn'">
<field name="line_ids" readonly="top_management_name">
<field name="line_ids" widget="one2many_search" readonly="top_management_name">
<tree editable="bottom">
<field name="component_id"/>
<field name="display_name"/>
@ -97,7 +97,7 @@ name="action_report_dc_btn"><i class="fa fa-print"></i> Print</button>
</field>
</page>
<page string="Materials" invisible="dc_type == 'out_souce_return' or dc_type == 'out_souce_noreturn'">
<field name="line_ids_materials" readonly="top_management_name">
<field name="line_ids_materials" widget="one2many_search" readonly="top_management_name">
<tree editable="bottom">
<field name="component_id"/>
<field name="display_name"/>
@ -143,7 +143,10 @@ name="action_report_dc_btn"><i class="fa fa-print"></i> Print</button>
</field>
</page>
<page string="Logistice Details">
<field name="courier"/>
<field name="lr_no"/>
</page>
</notebook>
<br></br>
@ -217,7 +220,7 @@ name="action_report_dc_btn"><i class="fa fa-print"></i> Print</button>
</record>
<menuitem id="delivery_challan_menu"
name="Delivery Challan (DC)"
parent="scg_forms_menu_root" action="action_dc_form_list" groups="sos_inventory.sos_healthcare_user,sos_inventory.sos_scg_group_manager,sos_inventory.sos_logistics_user,sos_inventory.sos_scg_group_user,sos_inventory.sos_management_user,sos_inventory.sos_finance_user,sos_inventory.sos_sys_admin_user"/>
parent="scg_forms_menu_root" action="action_dc_form_list" groups="sos_inventory.sos_healthcare_user,sos_inventory.sos_scg_group_manager,sos_inventory.sos_logistics_user,sos_inventory.sos_scg_group_user,sos_inventory.sos_management_user,sos_inventory.sos_finance_user,sos_inventory.sos_sys_admin_user,sos_inventory.sos_ce_user,sos_inventory.sos_sales_user"/>
</odoo>

View File

@ -363,7 +363,7 @@
</record>
<menuitem id="sos_deliverables_boq_menu"
name="Deliverables List / BOQ"
parent="sales_root_menu" action="action_sos_deliverables_boq_form_list" groups="sos_inventory.sos_scg_group_user,sos_inventory.sos_ce_user,sos_inventory.sos_finance_user,sos_inventory.sos_scg_group_manager,sos_inventory.sos_rd_user,sos_inventory.sos_production_user,sos_inventory.sos_qc_user,sos_inventory.sos_qa_user" />
parent="sales_root_menu" action="action_sos_deliverables_boq_form_list" groups="sos_inventory.sos_scg_group_user,sos_inventory.sos_ce_user,sos_inventory.sos_finance_user,sos_inventory.sos_scg_group_manager,sos_inventory.sos_rd_user,sos_inventory.sos_production_user,sos_inventory.sos_qc_user,sos_inventory.sos_qa_user,sos_inventory.sos_logistics_user" />
</odoo>

View File

@ -8,7 +8,8 @@
<sheet>
<group>
<field name="fg_name"/>
<field name="communication_type"/>
<field name="communication_type" invisible="fg_name != 'BHMS 12V'"/>
<field name="slave_type" invisible="fg_name != 'BHMS 1.2V'"/>
<!-- <field name="customer_name"/>
<field name="customer_location"/> -->
</group>
@ -91,6 +92,7 @@
<tree string="Deliverables">
<field name="fg_name"/>
<field name="communication_type"/>
<field name="slave_type"/>
<field name="fg_ids"/>
<field name="sfg_ids"/>
<field name="material_ids"/>

View File

@ -11,6 +11,7 @@
<field name="model">sos_departments</field>
<field name="arch" type="xml">
<tree>
<field name="short_form"/>
<field name="name"/>
</tree>
</field>
@ -20,9 +21,18 @@
<field name="model">sos_departments</field>
<field name="arch" type="xml">
<form string="Model Form">
<sheet>
<group><field name="name"/></group>
</sheet>
<sheet>
<group><field name="name"/></group>
<group><field name="short_form"/></group>
<group><field name="process_incharge"/></group>
<field name="users_line_ids">
<tree editable="bottom">
<field name="users"/>
</tree>
</field>
</sheet>
</form>
</field>
</record>

View File

@ -216,13 +216,15 @@
<field name="payment_status"/>
<field name="billing_address"/>
<field name="shipping_address"/>
</group>
<group>
<field name="invoice_no"/>
<field name="invoice_date"/>
<field name="warranty"/>
<field name="gst_no"/>
<field name="shipping_address"/>
</group>
</group>

View File

@ -12,6 +12,8 @@
decoration-warning="indent_status == 'hold'"
decoration-danger="indent_status == 'open' or indent_status == 'cancel'"
/>
<field name="prepared_by" string="Prepared By" widget="many2one_avatar_user"/>
<field name="approved_by" string="Approved By" widget="many2one_avatar_user"/>
</tree>
@ -184,7 +186,8 @@
<templates>
<div class="row" style="margin-top:100px">
<div class="col-4"> <table class="table_custom">
<div class="col-4"> <table class="table_custom" style="box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;background-color: #fff;border: solid 4px #9689c1;">
<tr style="border-bottom: solid 1px #ccc;">
<td style="padding: 8px;" class="column"><b>Prepared By Sign</b>
<button string="Approve" invisible="prepared_image" class="btn-primary custom_btn" type="object" name="action_approve_esign_btn"></button>
@ -200,9 +203,26 @@
<td><field name="prepared_by" readonly="1"/></td>
</tr>
</table> </div>
<div class="col-4"> </div>
<div class="col-4"> <table class="table_custom" style="box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;background-color: #fff;border: solid 4px #9689c1;">
<tr style="border-bottom: solid 1px #ccc;">
<td style="padding: 8px;" class="column"><b>Accounts Approval Sign</b>
<button string="Approve" invisible="accounts_approved_by_image" class="btn-primary custom_btn" type="object" name="action_acc_approver_esign_btn"></button>
</td>
<td><field name="accounts_approved_by_image" widget="image"/></td>
</tr>
<tr invisible="accounts_approved_by_image == False">
<td style="padding: 8px;" class="column"><b>Approved On</b></td>
<td><field name="accounts_approved_on" readonly="1"/></td>
</tr>
<tr invisible="accounts_approved_by_image == False">
<td style="padding: 8px;" class="column"><b>Approved By</b></td>
<td><field name="accounts_approved_name" readonly="1"/></td>
</tr>
</table> </div>
<div class="col-4">
<table class="table_custom">
<table class="table_custom" style="box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;background-color: #fff;border: solid 4px #9689c1;">
<tr style="border-bottom: solid 1px #ccc;">
<td style="padding: 8px;" class="column"><b>Top Management Approval Sign</b>
<button string="Approve" invisible="approval_image" class="btn-primary custom_btn" type="object" name="action_top_approver_esign_btn"></button>
@ -234,4 +254,5 @@
parent="indent_menu_root"
action = "action_fg_plan_list"/>
</odoo>

View File

@ -10,8 +10,11 @@
<field name="fg_type" string="Type" icon="fa-list-ul" enable_counters="1"/>
</searchpanel>
<field name="display_name" string="Display Name"/>
<field name="name" string="Name"/>
</search>
</field>
</record>

View File

@ -248,7 +248,7 @@
</field>
</record>
<!-- Menuitem -->
<menuitem id="menu_sos_fir" sequence="5" name="Final Inspection Report (FIR)" parent="mop_forms_menu_root" action="action_sos_fir" groups="sos_inventory.sos_healthcare_user,sos_inventory.sos_scg_group_user,sos_inventory.sos_qc_user,sos_inventory.sos_management_user,sos_inventory.sos_ce_user"/>
<menuitem id="menu_sos_fir" sequence="5" name="Final Inspection Report (FIR)" parent="mop_forms_menu_root" action="action_sos_fir" groups="sos_inventory.sos_healthcare_user,sos_inventory.sos_scg_group_user,sos_inventory.sos_qc_user,sos_inventory.sos_management_user,sos_inventory.sos_ce_user,sos_inventory.sos_sales_user"/>
</data>
</odoo>

Some files were not shown because too many files have changed in this diff Show More