feed added

This commit is contained in:
raman 2025-07-23 11:56:46 +05:30
parent 613206bbda
commit f45b9d2c71
21 changed files with 991 additions and 36 deletions

View File

@ -1,6 +1,9 @@
from odoo import models, fields, api, _ from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError from odoo.exceptions import UserError, ValidationError
import datetime from collections import defaultdict
from functools import reduce
class HrPayslipRun(models.Model): class HrPayslipRun(models.Model):
_inherit = 'hr.payslip.run' _inherit = 'hr.payslip.run'
@ -287,6 +290,8 @@ class HrPayslipRun(models.Model):
class HrPayslip(models.Model): class HrPayslip(models.Model):
_inherit = 'hr.payslip' _inherit = 'hr.payslip'
def get_payslip_lines_data(self, payslip_id): def get_payslip_lines_data(self, payslip_id):
payslip = self.browse(payslip_id) payslip = self.browse(payslip_id)
return [{ return [{
@ -296,4 +301,12 @@ class HrPayslip(models.Model):
'amount': line.amount, 'amount': line.amount,
'quantity': line.quantity, 'quantity': line.quantity,
'rate': line.rate 'rate': line.rate
} for line in payslip.line_ids] } for line in payslip.line_ids]
def action_open_payslips(self):
self.ensure_one()
action = self.env["ir.actions.actions"]._for_xml_id("hr_payroll.action_view_hr_payslip_month_form")
action['views'] = [[False, "form"]]
action['res_id'] = self.id
action['target'] = 'new'
return action

View File

@ -4,6 +4,7 @@ import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl";
import { registry } from "@web/core/registry"; import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks"; import { useService } from "@web/core/utils/hooks";
import { loadJS, loadCSS } from "@web/core/assets"; import { loadJS, loadCSS } from "@web/core/assets";
import { rpc } from "@web/core/network/rpc";
export class ConsolidatedPayslipGrid extends Component { export class ConsolidatedPayslipGrid extends Component {
static props = { static props = {
@ -15,6 +16,7 @@ export class ConsolidatedPayslipGrid extends Component {
setup() { setup() {
this.orm = useService("orm"); this.orm = useService("orm");
this.gridRef = useRef("gridContainer"); this.gridRef = useRef("gridContainer");
this.action = useService("action");
this.state = useState({ this.state = useState({
rows: [], rows: [],
payslipRunId: this.props.record.resId || this.props.record.evalContext.id || false payslipRunId: this.props.record.resId || this.props.record.evalContext.id || false
@ -36,16 +38,15 @@ export class ConsolidatedPayslipGrid extends Component {
async loadDependencies() { async loadDependencies() {
try { try {
await Promise.all([ await loadJS("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js");
loadJS("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"), await loadJS("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js");
loadJS("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js"), await loadCSS("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/themes/base/jquery-ui.min.css");
loadCSS("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/themes/base/jquery-ui.min.css"), await loadJS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/pqgrid.min.js");
loadJS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/pqgrid.min.js"), await loadJS("https://cdnjs.cloudflare.com/ajax/libs/jszip/2.6.1/jszip.min.js");
loadJS("https://cdnjs.cloudflare.com/ajax/libs/jszip/2.6.1/jszip.min.js"), await loadJS("https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js");
loadJS("https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"), await loadCSS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/pqgrid.min.css");
loadCSS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/pqgrid.min.css"), await loadCSS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/themes/Office/pqgrid.min.css");
loadCSS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/themes/Office/pqgrid.min.css")
]);
// Set global jQuery references // Set global jQuery references
window.$ = window.jQuery = window.$ || window.jQuery; window.$ = window.jQuery = window.$ || window.jQuery;
@ -145,24 +146,18 @@ export class ConsolidatedPayslipGrid extends Component {
editable: true, editable: true,
stripeRows:false, stripeRows:false,
editModel: { saveKey: $.ui.keyCode.ENTER }, editModel: { saveKey: $.ui.keyCode.ENTER },
filterModel: { filterModel: {on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1},
on: true, dataModel: {data: this.state.rows, location: "local", sorting: "local", paging: "local"},
mode: "AND", cellSave: function (evt, ui) {
header: true, const payload = {
autoSearch: true, id: ui.rowData.id,
type: 'local', field: ui.dataIndx,
minLength: 1 value: ui.newVal
}, };
dataModel: { updateData(payload);
data: this.state.rows, },
location: "local",
sorting: "local",
paging: "local"
},
menuIcon: true, menuIcon: true,
menuUI:{ menuUI:{tabs: ['hideCols']},
tabs: ['hideCols']
},
colModel: columns, colModel: columns,
postRenderInterval: -1, postRenderInterval: -1,
toolbar: { toolbar: {
@ -212,6 +207,21 @@ export class ConsolidatedPayslipGrid extends Component {
] ]
}, },
}; };
function updateData(data){
$.ajax({
url: "/slip/update",
type: "POST",
contentType: "application/json",
data: JSON.stringify(data),
success: function (response) {
console.log("Update successful:", response);
},
error: function (xhr) {
console.error("Update failed:", xhr.responseText);
}
});
};
// Apply CSS and initialize grid // Apply CSS and initialize grid
$(this.gridRef.el) $(this.gridRef.el)
@ -428,7 +438,47 @@ export class ConsolidatedPayslipGrid extends Component {
return `<span class="badge badge-${state === 'done' ? 'success' : 'warning'}">${state}</span>`; return `<span class="badge badge-${state === 'done' ? 'success' : 'warning'}">${state}</span>`;
} }
}, },
...subCols ...subCols,
{
title: "View",
width: 120,
editable: false,
summary:false,
render: function (ui) {
return "<button class='row-btn-view' type='button' >View</button>"
},
postRender: function (ui) {
var grid = this,
$cell = grid.getCell(ui);
$cell.find(".row-btn-view")
.button({ icons: { primary: 'ui-icon-extlink'} })
.on("click", async function (evt) {
const res = await odoo.__WOWL_DEBUG__.root.orm.call('hr.payslip','action_open_payslips',[ui.rowData.id])
// res.views = [[false, "form"]],
await odoo.__WOWL_DEBUG__.root.actionService.doAction(res)
});
}
},
{
title: "Edit",
width: 120,
editable: false,
render: function (ui) {
return "<button class='row-btn-edit' type='button'>Edit</button>"
},
postRender: function (ui) {
var grid = this,
$cell = grid.getCell(ui);
$cell.find(".row-btn-edit")
.button({ icons: { primary: 'ui-icon-pencil'} })
.on("click", async function (evt) {
const res = await odoo.__WOWL_DEBUG__.root.orm.call('hr.payslip','action_edit_payslip_lines',[ui.rowData.id])
res.views = [[false, "form"]],
await odoo.__WOWL_DEBUG__.root.actionService.doAction(res)
});
}
}
]; ];
} }

View File

@ -0,0 +1 @@
from . import models, controllers

View File

@ -0,0 +1,19 @@
{
'name': 'FTP Feed',
'summary': 'Feed Module',
'category': 'Reporting',
'version': '1.0.0',
'author': 'Pranay',
'website': 'https://ftprotech.in',
'license': 'LGPL-3',
'sequence': -100,
'depends': [
'base', 'web', 'hr'
],
'data': [
'security/ir.model.access.csv',
'security/security.xml',
'views/feed.xml',
],
'images': ['static/description/banner.png'],
}

View File

@ -0,0 +1 @@
from . import controller

View File

@ -0,0 +1,22 @@
from odoo import http
from odoo.http import request
class FeedController(http.Controller):
@http.route('/feed/attachment/<int:attachment_id>', auth='user')
def get_feed_attachment(self, attachment_id, **kwargs):
attachment = request.env['ir.attachment'].sudo().search([
('id', '=', attachment_id),
'|',
('res_model', '=', 'ftp.feed'),
('res_model', '=', 'ftp.feed.comments')
])
if not attachment:
return request.not_found()
return http.send_file(
attachment._full_path(attachment.store_fname),
filename=attachment.name,
as_attachment=True
)

View File

@ -0,0 +1 @@
from . import feed

View File

@ -0,0 +1,147 @@
from odoo import fields, api, _, models
from odoo.exceptions import UserError, ValidationError
from datetime import datetime
class FtpFeed(models.Model):
_name = 'ftp.feed'
_description = 'Social Feed Posts'
_order = 'create_date desc'
description = fields.Text(string='Post Content')
images = fields.Many2many('ir.attachment', string='Post Images')
user_id = fields.Many2one('res.users', default=lambda self: self.env.user, string='Author')
employee_id = fields.Many2one('hr.employee', string='Employee', compute='_compute_employee', store=True)
feed_data = fields.One2many('ftp.feed.data', 'feed_id', string='Reactions')
feed_comments = fields.One2many('ftp.feed.comments', 'feed_id', string='Comments')
like_count = fields.Integer(compute='_compute_like_count', string='Likes')
comment_count = fields.Integer(compute='_compute_comment_count', string='Comments')
post_time = fields.Char(compute='_compute_post_time', string='Posted')
@api.depends('user_id')
def _compute_employee(self):
for record in self:
employee = self.env['hr.employee'].search([('user_id', '=', record.user_id.id)], limit=1)
record.employee_id = employee.id if employee else False
@api.depends('feed_data.feed_type')
def _compute_like_count(self):
for record in self:
record.like_count = len(record.feed_data.filtered(lambda x: x.feed_type == 'like'))
@api.depends('feed_comments')
def _compute_comment_count(self):
for record in self:
record.comment_count = len(record.feed_comments)
@api.depends('create_date')
def _compute_post_time(self):
for record in self:
if record.create_date:
delta = datetime.now() - record.create_date
if delta.days > 0:
record.post_time = f"{delta.days}d ago"
elif delta.seconds > 3600:
record.post_time = f"{delta.seconds // 3600}h ago"
elif delta.seconds > 60:
record.post_time = f"{delta.seconds // 60}m ago"
else:
record.post_time = "Just now"
else:
record.post_time = ""
@api.model
def create(self, vals):
vals['user_id'] = self.env.uid
if not vals.get('images') and not vals.get('description'):
raise ValidationError(_("Can't create an empty post"))
# Handle images to ensure they are public
if vals.get('images'):
# images is a list of commands like [(6, 0, [id1, id2])]
image_ids = []
for command in vals['images']:
if command[0] == 6: # REPLACE command
image_ids = command[2]
elif command[0] in (4, 1): # LINK or CREATE command
image_ids.append(command[1] if command[0] == 4 else command[0])
# Update all attachments to be public
if image_ids:
self.env['ir.attachment'].browse(image_ids).write({'public': True})
return super().create(vals)
def write(self, vals):
for record in self:
if self.env.user != record.user_id:
if 'description' in vals or 'images' in vals:
raise UserError(_("Only the author can modify post content"))
if vals.get('images'):
# Get all new image IDs being added
new_image_ids = []
for command in vals['images']:
if command[0] == 6: # REPLACE command
new_image_ids = command[2]
elif command[0] in (4, 1): # LINK or CREATE command
new_image_ids.append(command[1] if command[0] == 4 else command[0])
# Update all new attachments to be public
if new_image_ids:
self.env['ir.attachment'].browse(new_image_ids).write({'public': True})
return super().write(vals)
def unlink(self):
for record in self:
if self.env.user != record.user_id:
raise UserError(_("Only the author can delete this post"))
return super().unlink()
class FtpFeedData(models.Model):
_name = 'ftp.feed.data'
_description = 'Feed Reactions'
feed_type = fields.Selection([
('like', 'Like'),
('dislike', 'Dislike')
], default='like', required=True)
user_id = fields.Many2one('res.users', default=lambda self: self.env.user, required=True)
feed_id = fields.Many2one('ftp.feed', required=True, ondelete='cascade')
_sql_constraints = [
('unique_user_feed', 'unique(user_id, feed_id)', 'You can only react once per post!'),
]
class FtpFeedComments(models.Model):
_name = 'ftp.feed.comments'
_description = 'Feed Comments'
_order = 'create_date desc'
feed_comment = fields.Text(string='Comment', required=True)
user_id = fields.Many2one('res.users', default=lambda self: self.env.user, string='Author')
employee_id = fields.Many2one('hr.employee', string='Employee', compute='_compute_employee', store=True)
feed_id = fields.Many2one('ftp.feed', required=True, ondelete='cascade')
comment_time = fields.Char(compute='_compute_comment_time', string='Commented')
@api.depends('user_id')
def _compute_employee(self):
for record in self:
employee = self.env['hr.employee'].search([('user_id', '=', record.user_id.id)], limit=1)
record.employee_id = employee.id if employee else False
@api.depends('create_date')
def _compute_comment_time(self):
for record in self:
if record.create_date:
delta = datetime.now() - record.create_date
if delta.days > 0:
record.comment_time = f"{delta.days}d ago"
elif delta.seconds > 3600:
record.comment_time = f"{delta.seconds // 3600}h ago"
elif delta.seconds > 60:
record.comment_time = f"{delta.seconds // 60}m ago"
else:
record.comment_time = "Just now"
else:
record.comment_time = ""

View File

@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ftp_feed,access_ftp_feed,model_ftp_feed,,1,1,1,1
access_ftp_feed_data,access_ftp_feed_data,model_ftp_feed_data,,1,1,1,1
access_ftp_feed_comments,access_ftp_feed_comments,model_ftp_feed_comments,,1,1,1,1
access_ftp_feed_attachment,ftp.feed.attachment.access,base.model_ir_attachment,base.group_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ftp_feed access_ftp_feed model_ftp_feed 1 1 1 1
3 access_ftp_feed_data access_ftp_feed_data model_ftp_feed_data 1 1 1 1
4 access_ftp_feed_comments access_ftp_feed_comments model_ftp_feed_comments 1 1 1 1
5 access_ftp_feed_attachment ftp.feed.attachment.access base.model_ir_attachment base.group_user 1 0 0 0

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<!-- Allow everyone to read all -->
<record id="ftp_feed_read_rule" model="ir.rule">
<field name="name">Feed - Read</field>
<field name="model_id" ref="model_ftp_feed"/>
<field name="domain_force">[(1, '=', 1)]</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_create" eval="1"/>
<field name="perm_unlink" eval="1"/>
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_ftp_feed_form" model="ir.ui.view">
<field name="name">ftp.feed.form</field>
<field name="model">ftp.feed</field>
<field name="arch" type="xml">
<form string="Feed Post">
<sheet>
<div class="oe_title">
<h1>
<field name="user_id" class="oe_inline" readonly="1"/>
</h1>
<h2>
<field name="post_time" readonly="1"/>
</h2>
</div>
<group>
<group>
<field name="description" placeholder="What's on your mind?"/>
</group>
<group>
<field name="images" widget="many2many_binary" options="{'preview_images': true}"/>
</group>
</group>
<notebook>
<page string="Reactions">
<field name="like_count" widget="stat_info" string="Total Likes"/>
<field name="feed_data">
<list editable="bottom">
<field name="user_id"/>
<field name="feed_type"/>
<field name="create_date" readonly="1"/>
</list>
</field>
</page>
<page string="Comments">
<field name="comment_count" widget="stat_info" string="Total Comments"/>
<field name="feed_comments">
<list editable="bottom">
<field name="user_id"/>
<field name="feed_comment"/>
<field name="comment_time" readonly="1"/>
</list>
<form>
<group>
<field name="user_id" readonly="1"/>
<field name="feed_comment"/>
<field name="comment_time" readonly="1"/>
</group>
</form>
</field>
</page>
<page string="Author Info">
<field name="employee_id">
<form>
<group>
<field name="name"/>
<field name="create_date" readonly="1"/>
<field name="write_date" readonly="1"/>
</group>
</form>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_ftp_feed_list" model="ir.ui.view">
<field name="name">ftp.feed.list</field>
<field name="model">ftp.feed</field>
<field name="arch" type="xml">
<list string="Feed Posts" decoration-info="like_count > 0" decoration-danger="like_count == 0">
<field name="user_id"/>
<field name="description" widget="text_emojis"/>
<field name="post_time"/>
<field name="like_count" widget="badge" invisible="1"/>
<field name="comment_count" widget="badge" invisible="1"/>
</list>
</field>
</record>
<record id="view_ftp_feed_kanban" model="ir.ui.view">
<field name="name">ftp.feed.kanban</field>
<field name="model">ftp.feed</field>
<field name="arch" type="xml">
<kanban default_group_by="user_id">
<field name="description"/>
<field name="images"/>
<field name="user_id"/>
<field name="post_time"/>
<field name="like_count"/>
<field name="comment_count"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_global_click o_kanban_record">
<div class="o_kanban_record_header">
<div class="o_kanban_record_title">
<field name="user_id" widget="hr_employee"/>
<small class="float-right">
<field name="post_time"/>
</small>
</div>
</div>
<div class="o_kanban_record_body">
<div t-if="record.description.raw_value"
widget="text_emojis" class="mb-2"/>
<div t-if="record.images.raw_value" class="d-flex flex-wrap">
<t t-foreach="record.images.raw_value.slice(0, 3)" t-as="img">
<img t-att-src="'/web/image/ir.attachment/' + img + '/datas/300x300'"
class="img-fluid m-1" style="max-height: 100px;"/>
</t>
<t t-if="record.images.raw_value.length > 3">
<div class="o_kanban_image_more ml-1">
+<t t-esc="record.images.raw_value.length - 3"/>
</div>
</t>
</div>
<div class="d-flex justify-content-between mt-2">
<div>
<i class="fa fa-thumbs-up mr-1"/>
<field name="like_count" widget="badge"/>
</div>
<div>
<i class="fa fa-comments mr-1"/>
<field name="comment_count" widget="badge"/>
</div>
</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<!-- Actions -->
<record id="action_ftp_feed" model="ir.actions.act_window">
<field name="name">Social Feed</field>
<field name="res_model">ftp.feed</field>
<field name="view_mode">kanban,list,form</field>
<field name="context">{'default_user_id': uid}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Share updates with your team!
</p>
</field>
</record>
<!-- Menu -->
<menuitem id="menu_ftp_feed_root" name="Social Feed" sequence="10" web_icon="ftp_feed,static/description/banner.png"/>
<menuitem id="menu_ftp_feed" name="Feed" parent="menu_ftp_feed_root" sequence="20" action="action_ftp_feed"/>
</odoo>

View File

@ -66,6 +66,7 @@
'web.assets_backend': [ 'web.assets_backend': [
'hr_payroll/static/src/components/add_payslips/**', 'hr_payroll/static/src/components/add_payslips/**',
'hr_payroll/static/src/views/add_payslips_hook.js', 'hr_payroll/static/src/views/add_payslips_hook.js',
'hr_payroll/static/src/js/payslip_line_one2many.js'
# 'hr_payroll/static/src/**/*', # 'hr_payroll/static/src/**/*',
# ('remove', 'hr_payroll/static/src/js/hr_payroll_report_graph_view.js'), # ('remove', 'hr_payroll/static/src/js/hr_payroll_report_graph_view.js'),
# ('remove', 'hr_payroll/static/src/js/hr_payroll_report_pivot_*'), # ('remove', 'hr_payroll/static/src/js/hr_payroll_report_pivot_*'),

View File

@ -0,0 +1,61 @@
from datetime import timedelta, datetime, date
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.http import request
from odoo import http
import babel.dates
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
@api.model
def get_wage_register_data(self, date):
if not date:
now = fields.Datetime.now()
date = babel.dates.format_datetime(now, "MMMM yyyy", locale='en')
sql = """
SELECT
e.name AS employee,
job.name AS designation,
e.doj AS date_of_joining,
e.birthday AS date_of_birth,
to_char(p.date_from, 'Month YYYY') AS month_of_wages,
e.l10n_in_uan AS uan_no,
e.l10n_in_esic_number AS esic_no,
bank.acc_number AS bank_account_no,
SUM(CASE WHEN pl.code = 'BASIC' THEN pl.total ELSE 0 END) AS basic,
SUM(CASE WHEN pl.code = 'HRA' THEN pl.total ELSE 0 END) AS hra,
SUM(CASE WHEN pl.code = 'SPA' THEN pl.total ELSE 0 END) AS other_allowance,
SUM(CASE WHEN pl.code = 'GROSS' THEN pl.total ELSE 0 END) AS gross_wages,
SUM(CASE WHEN pl.code = 'ESICS' THEN pl.total ELSE 0 END) AS esi,
SUM(CASE WHEN pl.code = 'PF' THEN pl.total ELSE 0 END) AS pf,
SUM(CASE WHEN pl.code = 'PT' THEN pl.total ELSE 0 END) AS pt,
SUM(CASE WHEN pl.code = 'DED' THEN pl.total ELSE 0 END) AS total_deductions,
SUM(CASE WHEN pl.code = 'LTA' THEN pl.total ELSE 0 END) AS lta,
p.net_wage AS net_amount_payable
FROM hr_payslip p
JOIN hr_employee e ON p.employee_id = e.id
LEFT JOIN hr_job job ON e.job_id = job.id
LEFT JOIN hr_payslip_line pl ON pl.slip_id = p.id
LEFT JOIN hr_salary_rule_category cat_earn ON cat_earn.code = 'GROSS'
LEFT JOIN hr_salary_rule_category cat_deduct ON cat_deduct.code = 'DED'
LEFT JOIN res_partner_bank bank ON bank.id = e.bank_account_id
GROUP BY
e.name, job.name, e.doj, e.birthday,
p.date_from, e.l10n_in_uan, e.l10n_in_esic_number, e.bank_account_id, p.net_wage, bank.acc_number
ORDER BY e.name;
"""
self.env.cr.execute(sql)
result = self.env.cr.dictfetchall()
data = []
for i in result:
i['designation'] = i['designation']['en_US']
data.append(i)
return data

View File

@ -0,0 +1,215 @@
/** @odoo-module **/
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { loadJS, loadCSS } from "@web/core/assets";
export class SaleTest extends Component {
static props = {
...standardWidgetProps,
};
static template = "SaleTest";
setup() {
this.orm = useService("orm");
this.gridRef = useRef("wageGrid");
this.state = useState({ rows: [] });
onWillStart(async () => {
await loadJS("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js");
window.$ = window.jQuery = window.$ || window.jQuery;
await loadJS("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js");
await loadCSS("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/themes/base/jquery-ui.min.css");
await loadJS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/pqgrid.min.js");
await loadCSS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/pqgrid.min.css");
await loadCSS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/themes/steelblue/pqgrid.min.css");
await this.loadData();
});
onMounted(() => {
if (this.gridRef.el) {
this.renderGrid();
setTimeout(() => this.initializeDatePicker(), 200);
} else {
console.error("Grid element not found");
}
});
}
renderGrid() {
const data = this.state.rows.length ? this.state.rows : [
{
employee: "John Doe", designation: "Software Engineer", date_of_joining: "2020-01-10",
date_of_birth: "1990-04-15", month_of_wages: "January 2024", days_worked: 22,
uan_no: "100100100", esic_no: "ESIC12345", bank_account_no: "1234567890",
basic: 35000, hra: 15000, lta: 5000, other_allowance: 5000,
gross_wages: 60000, esi: 500, pf: 1800, pt: 200, total_deductions: 2500, net_amount_payable: 57500
}
];
$(this.gridRef.el).pqGrid({
width: "100%",
height: 600,
editable: false,
showSummary: true,
filterModel: {
on: true,
mode: "AND",
header: true
},
toolbar: {
items: [
{
type: "button",
label: "GET",
icon: "fa fa-check",
listener: () => this.onFilterClick()
},
{
type: "button",
label: "Export to Excel",
icon: "ui-icon-document",
listener: () => this.exportExcel()
},
{
type: "button",
label: "Export to PDF",
icon: "ui-icon-print",
listener: () => this.exportPDF()
}
]
},
colModel: [
{ title: "Name of the Employee", dataIndx: "employee", width: 220, filter: {type: 'textbox', condition: 'begin', listeners: ['keyup']} },
{ title: "Designation / Category", dataIndx: "designation", width: 150, filter: {type: 'textbox', condition: 'begin', listeners: ['keyup']} },
{ title: "Date of Joining", dataIndx: "date_of_joining", width: 90 },
{ title: "Date of Birth", dataIndx: "date_of_birth", width: 90 },
{ title: "Month of Wages", dataIndx: "month_of_wages", width: 150 },
{ title: "No. of Days Worked", dataIndx: "days_worked", width: 90 },
{ title: "UAN No.", dataIndx: "uan_no", width: 150 },
{ title: "ESIC No.", dataIndx: "esic_no", width: 150 },
{ title: "Bank Account No.", dataIndx: "bank_account_no", width: 150, filter: {type: 'textbox', condition: 'begin', listeners: ['keyup']} },
{ title: "Basic", dataIndx: "basic", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "HRA", dataIndx: "hra", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "LTA", dataIndx: "lta", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "Other Allowance", dataIndx: "other_allowance", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "Gross Wages", dataIndx: "gross_wages", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "ESI", dataIndx: "esi", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "PF", dataIndx: "pf", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "PT", dataIndx: "pt", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "Total Deductions", dataIndx: "total_deductions", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "Net Amount Payable", dataIndx: "net_amount_payable", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" }
],
dataModel: {
data: data,
location: "local"
},
groupModel: {
on: true,
dataIndx: ["month_of_wages"],
showSummary: true
}
});
}
initializeDatePicker() {
$("#fromDate").datepicker({
changeMonth: true,
changeYear: true,
showButtonPanel: true,
dateFormat: 'MM yy',
onClose: function (dateText, inst) {
const month = $("#ui-datepicker-div .ui-datepicker-month :selected").val();
const year = $("#ui-datepicker-div .ui-datepicker-year :selected").val();
$(this).datepicker('setDate', new Date(year, month, 1));
},
beforeShow: function (input, inst) {
$(input).datepicker("widget").addClass("monthOnly");
}
});
$("<style>")
.prop("type", "text/css")
.html(".monthOnly .ui-datepicker-calendar { display: none; }")
.appendTo("head");
}
async loadData(fromDate = null) {
try {
const records = await this.orm.call("hr.payslip", "get_wage_register_data", [fromDate]);
this.state.rows = records;
this.renderGrid();
} catch (error) {
console.error("Error loading wage data:", error);
}
}
async onFilterClick() {
const fromDate = $("#fromDate").val();
await this.loadData(fromDate);
}
exportExcel() {
const grid = $(this.gridRef.el).pqGrid("instance");
const data = grid.option("dataModel").data;
const columns = grid.option("colModel").map(col => ({
title: col.title,
dataIndx: col.dataIndx
}));
fetch("/hr_payroll/export_excel", {
method: "POST",
body: JSON.stringify({ data, columns }),
headers: { "Content-Type": "application/json" }
})
.then(response => response.blob())
.then(blob => {
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "payroll_export.xlsx";
link.click();
})
.catch(err => {
console.error("Export failed", err);
alert("Failed to export Excel.");
});
}
exportPDF() {
const grid = $(this.gridRef.el).pqGrid("instance");
const data = grid.option("dataModel").data;
const columns = grid.option("colModel").map(col => ({
title: col.title,
dataIndx: col.dataIndx
}));
fetch("/hr_payroll/export_pdf", {
method: "POST",
body: JSON.stringify({ data, columns }),
headers: { "Content-Type": "application/json" }
})
.then(response => response.blob())
.then(blob => {
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "payroll_export.pdf";
link.click();
})
.catch(err => {
console.error("Export failed", err);
alert("Export to PDF failed.");
});
}
clicked() {
alert("Button clicked!");
}
}
export const saleTest = {
component: SaleTest,
};
registry.category("view_widgets").add("SaleTest", saleTest);

View File

@ -0,0 +1,207 @@
/** @odoo-module **/
import { Component, onMounted, useRef, useState, onWillStart } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { loadJS, loadCSS } from "@web/core/assets";
export class WageRegisterGrid extends Component {
static template = "WageRegisterGridTemplate";
setup() {
this.orm = useService("orm");
this.gridRef = useRef("wageGrid");
this.state = useState({ rows: [] });
onWillStart(async () => {
await loadJS("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js");
window.$ = window.jQuery = window.$ || window.jQuery;
await loadJS("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js");
await loadCSS("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/themes/base/jquery-ui.min.css");
await loadJS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/pqgrid.min.js");
await loadCSS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/pqgrid.min.css");
await this.loadData();
});
onMounted(() => {
if (this.gridRef.el) {
this.renderGrid();
setTimeout(() => this.initializeDatePicker(), 200);
} else {
console.error("Grid element not found");
}
});
}
renderGrid() {
const data = this.state.rows.length ? this.state.rows : [
{
employee: "John Doe", designation: "Software Engineer", date_of_joining: "2020-01-10",
date_of_birth: "1990-04-15", month_of_wages: "January 2024", days_worked: 22,
uan_no: "100100100", esic_no: "ESIC12345", bank_account_no: "1234567890",
basic: 35000, hra: 15000, lta: 5000, other_allowance: 5000,
gross_wages: 60000, esi: 500, pf: 1800, pt: 200, total_deductions: 2500, net_amount_payable: 57500
}
];
$(this.gridRef.el).pqGrid({
width: "100%",
height: 600,
editable: false,
showSummary: true,
filterModel: {
on: true,
mode: "AND",
header: true
},
toolbar: {
items: [
{
type: "textbox",
label: "From Date: ",
attr: "id=fromDate"
},
{
type: "button",
label: "GET",
icon: "fa fa-check",
listener: () => this.onFilterClick()
},
{
type: "button",
label: "Export to Excel",
icon: "ui-icon-document",
listener: () => this.exportExcel()
},
{
type: "button",
label: "Export to PDF",
icon: "ui-icon-print",
listener: () => this.exportPDF()
}
]
},
colModel: [
{ title: "Name of the Employee", dataIndx: "employee", width: 220, filter: {type: 'textbox', condition: 'begin', listeners: ['keyup']} },
{ title: "Designation / Category", dataIndx: "designation", width: 150, filter: {type: 'textbox', condition: 'begin', listeners: ['keyup']} },
{ title: "Date of Joining", dataIndx: "date_of_joining", width: 90 },
{ title: "Date of Birth", dataIndx: "date_of_birth", width: 90 },
{ title: "Month of Wages", dataIndx: "month_of_wages", width: 150 },
{ title: "No. of Days Worked", dataIndx: "days_worked", width: 90 },
{ title: "UAN No.", dataIndx: "uan_no", width: 150 },
{ title: "ESIC No.", dataIndx: "esic_no", width: 150 },
{ title: "Bank Account No.", dataIndx: "bank_account_no", width: 150, filter: {type: 'textbox', condition: 'begin', listeners: ['keyup']} },
{ title: "Basic", dataIndx: "basic", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "HRA", dataIndx: "hra", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "LTA", dataIndx: "lta", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "Other Allowance", dataIndx: "other_allowance", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "Gross Wages", dataIndx: "gross_wages", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "ESI", dataIndx: "esi", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "PF", dataIndx: "pf", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "PT", dataIndx: "pt", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "Total Deductions", dataIndx: "total_deductions", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" },
{ title: "Net Amount Payable", dataIndx: "net_amount_payable", width: 90, dataType: "float", summary: {type: "sum"}, format: "##,##0.00" }
],
dataModel: {
data: data,
location: "local"
},
groupModel: {
on: true,
dataIndx: ["month_of_wages"],
showSummary: true
}
});
}
initializeDatePicker() {
$("#fromDate").datepicker({
changeMonth: true,
changeYear: true,
showButtonPanel: true,
dateFormat: 'MM yy',
onClose: function (dateText, inst) {
const month = $("#ui-datepicker-div .ui-datepicker-month :selected").val();
const year = $("#ui-datepicker-div .ui-datepicker-year :selected").val();
$(this).datepicker('setDate', new Date(year, month, 1));
},
beforeShow: function (input, inst) {
$(input).datepicker("widget").addClass("monthOnly");
}
});
$("<style>")
.prop("type", "text/css")
.html(".monthOnly .ui-datepicker-calendar { display: none; }")
.appendTo("head");
}
async loadData(fromDate = null) {
try {
const records = await this.orm.call("hr.payslip", "get_wage_register_data", [fromDate]);
this.state.rows = records;
this.renderGrid();
} catch (error) {
console.error("Error loading wage data:", error);
}
}
async onFilterClick() {
const fromDate = $("#fromDate").val();
await this.loadData(fromDate);
}
exportExcel() {
const grid = $(this.gridRef.el).pqGrid("instance");
const data = grid.option("dataModel").data;
const columns = grid.option("colModel").map(col => ({
title: col.title,
dataIndx: col.dataIndx
}));
fetch("/hr_payroll/export_excel", {
method: "POST",
body: JSON.stringify({ data, columns }),
headers: { "Content-Type": "application/json" }
})
.then(response => response.blob())
.then(blob => {
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "payroll_export.xlsx";
link.click();
})
.catch(err => {
console.error("Export failed", err);
alert("Failed to export Excel.");
});
}
exportPDF() {
const grid = $(this.gridRef.el).pqGrid("instance");
const data = grid.option("dataModel").data;
const columns = grid.option("colModel").map(col => ({
title: col.title,
dataIndx: col.dataIndx
}));
fetch("/hr_payroll/export_pdf", {
method: "POST",
body: JSON.stringify({ data, columns }),
headers: { "Content-Type": "application/json" }
})
.then(response => response.blob())
.then(blob => {
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "payroll_export.pdf";
link.click();
})
.catch(err => {
console.error("Export failed", err);
alert("Export to PDF failed.");
});
}
}
registry.category("actions").add("WageRegisterGrid", WageRegisterGrid);

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="SaleTest" owl="1">
<button class="btn btn-primary" t-on-click="clicked">Fetch View</button>
<div t-ref="wageGrid"
style="width: 100%; ">
</div>
</t>
</templates>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="WageRegisterGridTemplate">
<div class="o_form_view o_form_edit_mode" style="padding: 20px; background-color: #f9f9f9; border-radius: 10px;">
<div style="margin-bottom: 20px; border-bottom: 2px solid #dcdcdc; padding-bottom: 10px;">
<h2 style="margin: 0; font-size: 24px; font-weight: 600; color: #34495e;">
📊 Register of Wages
</h2>
<p style="margin: 5px 0 0; color: #7f8c8d; font-size: 14px;">
Detailed wage data of employees by pay period
</p>
<div t-ref="wageGrid"
style="width: 100%; ">
</div>
</div>
</div>
</t>
</templates>

View File

@ -4,7 +4,7 @@
<t t-call="web.external_layout_boxed"> <t t-call="web.external_layout_boxed">
<hr class="border-top" /> <hr class="border-top" />
<div class="page" style="font-size: 12px;"> <div class="page" style="font-size: 12px;">
<h2><span t-field="o.name"/></h2> <h2><span t-field="o.name"/></h2>
<div class="employee-section-title" style="font-weight: bold; margin-top: 20px;">Employee Information</div> <div class="employee-section-title" style="font-weight: bold; margin-top: 20px;">Employee Information</div>
<table style="width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 10px;"> <table style="width: 100%; border-collapse: collapse; font-size: 12px; margin-bottom: 10px;">
@ -73,24 +73,24 @@
<tr> <tr>
<td style="border: 1px solid #ccc; padding: 6px;"> <td style="border: 1px solid #ccc; padding: 6px;">
<t t-set="income" t-value="0"/> <t t-set="income" t-value="0"/>
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.code in ['BASIC','HRA','LTA','SPA'])" t-as="l"> <div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code in ['BASIC','SPA','ALW']and l.amount &gt; 0)" t-as="l">
<t t-esc="l.name"/><br/> <t t-esc="l.name"/><br/>
</div> </div>
</td> </td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;"> <td style="border: 1px solid #ccc; padding: 6px; text-align: right;">
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.code in ['BASIC','HRA','LTA','SPA'])" t-as="l"> <div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code in ['BASIC','SPA','ALW'] and l.amount &gt; 0)" t-as="l">
<t t-esc="l.amount"/><br/> <t t-esc="l.amount"/><br/>
<t t-set="income" t-value="income + l.amount"/> <t t-set="income" t-value="income + l.amount"/>
</div> </div>
</td> </td>
<td style="border: 1px solid #ccc; padding: 6px;"> <td style="border: 1px solid #ccc; padding: 6px;">
<t t-set="contribution" t-value="0"/> <t t-set="contribution" t-value="0"/>
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.code in ['PFE','ESICF'])" t-as="l"> <div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code in ['COMP','MA'] and l.amount &gt; 0)" t-as="l">
<t t-esc="l.name"/><br/> <t t-esc="l.name"/><br/>
</div> </div>
</td> </td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;"> <td style="border: 1px solid #ccc; padding: 6px; text-align: right;">
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.code in ['PFE','ESICF'])" t-as="l"> <div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code in ['COMP','MA'] and l.amount &gt; 0)" t-as="l">
<t t-esc="l.amount"/><br/> <t t-esc="l.amount"/><br/>
<t t-set="contribution" t-value="contribution + l.amount"/> <t t-set="contribution" t-value="contribution + l.amount"/>
</div> </div>
@ -107,7 +107,7 @@
</div> </div>
</td> </td>
<td style="border: 1px solid #ccc; padding: 6px; text-align: right;"> <td style="border: 1px solid #ccc; padding: 6px; text-align: right;">
<strong><t t-esc="contribution + income"/></strong><br/><br/> <strong><t t-esc="contribution + income"/></strong><br/><br/><br/>
<div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code == 'DED')" t-as="l"> <div t-foreach="o.line_ids.filtered(lambda l: l.appears_on_payslip and l.category_id.code == 'DED')" t-as="l">
<t t-esc="l.amount"/><br/> <t t-esc="l.amount"/><br/>
<t t-set="ded" t-value="ded + l.amount"/> <t t-set="ded" t-value="ded + l.amount"/>

View File

@ -0,0 +1,10 @@
<odoo>
<record id="action_wage_register_grid" model="ir.actions.client">
<field name="name">Wage Register Grid</field>
<field name="tag">WageRegisterGrid</field>
</record>
<menuitem id="menu_report_payroll" name="Wage Grid" action="action_wage_register_grid" parent="menu_hr_payroll_report"/>
</odoo>