feed added
This commit is contained in:
parent
613206bbda
commit
f45b9d2c71
|
|
@ -1,6 +1,9 @@
|
|||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
from functools import reduce
|
||||
|
||||
|
||||
|
||||
class HrPayslipRun(models.Model):
|
||||
_inherit = 'hr.payslip.run'
|
||||
|
|
@ -287,6 +290,8 @@ class HrPayslipRun(models.Model):
|
|||
class HrPayslip(models.Model):
|
||||
_inherit = 'hr.payslip'
|
||||
|
||||
|
||||
|
||||
def get_payslip_lines_data(self, payslip_id):
|
||||
payslip = self.browse(payslip_id)
|
||||
return [{
|
||||
|
|
@ -297,3 +302,11 @@ class HrPayslip(models.Model):
|
|||
'quantity': line.quantity,
|
||||
'rate': line.rate
|
||||
} 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
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
|
||||
export class ConsolidatedPayslipGrid extends Component {
|
||||
static props = {
|
||||
|
|
@ -15,6 +16,7 @@ export class ConsolidatedPayslipGrid extends Component {
|
|||
setup() {
|
||||
this.orm = useService("orm");
|
||||
this.gridRef = useRef("gridContainer");
|
||||
this.action = useService("action");
|
||||
this.state = useState({
|
||||
rows: [],
|
||||
payslipRunId: this.props.record.resId || this.props.record.evalContext.id || false
|
||||
|
|
@ -36,16 +38,15 @@ export class ConsolidatedPayslipGrid extends Component {
|
|||
|
||||
async loadDependencies() {
|
||||
try {
|
||||
await Promise.all([
|
||||
loadJS("https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"),
|
||||
loadJS("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/jquery-ui.min.js"),
|
||||
loadCSS("https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.13.2/themes/base/jquery-ui.min.css"),
|
||||
loadJS("https://cdnjs.cloudflare.com/ajax/libs/pqGrid/3.5.1/pqgrid.min.js"),
|
||||
loadJS("https://cdnjs.cloudflare.com/ajax/libs/jszip/2.6.1/jszip.min.js"),
|
||||
loadJS("https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"),
|
||||
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/themes/Office/pqgrid.min.css")
|
||||
]);
|
||||
await 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");
|
||||
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 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");
|
||||
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/Office/pqgrid.min.css");
|
||||
|
||||
|
||||
// Set global jQuery references
|
||||
window.$ = window.jQuery = window.$ || window.jQuery;
|
||||
|
|
@ -145,24 +146,18 @@ export class ConsolidatedPayslipGrid extends Component {
|
|||
editable: true,
|
||||
stripeRows:false,
|
||||
editModel: { saveKey: $.ui.keyCode.ENTER },
|
||||
filterModel: {
|
||||
on: true,
|
||||
mode: "AND",
|
||||
header: true,
|
||||
autoSearch: true,
|
||||
type: 'local',
|
||||
minLength: 1
|
||||
},
|
||||
dataModel: {
|
||||
data: this.state.rows,
|
||||
location: "local",
|
||||
sorting: "local",
|
||||
paging: "local"
|
||||
filterModel: {on: true, mode: "AND", header: true, autoSearch: true, type: 'local', minLength: 1},
|
||||
dataModel: {data: this.state.rows, location: "local", sorting: "local", paging: "local"},
|
||||
cellSave: function (evt, ui) {
|
||||
const payload = {
|
||||
id: ui.rowData.id,
|
||||
field: ui.dataIndx,
|
||||
value: ui.newVal
|
||||
};
|
||||
updateData(payload);
|
||||
},
|
||||
menuIcon: true,
|
||||
menuUI:{
|
||||
tabs: ['hideCols']
|
||||
},
|
||||
menuUI:{tabs: ['hideCols']},
|
||||
colModel: columns,
|
||||
postRenderInterval: -1,
|
||||
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
|
||||
$(this.gridRef.el)
|
||||
|
|
@ -428,7 +438,47 @@ export class ConsolidatedPayslipGrid extends Component {
|
|||
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)
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import models, controllers
|
||||
|
|
@ -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'],
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import controller
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import feed
|
||||
|
|
@ -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 = ""
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -66,6 +66,7 @@
|
|||
'web.assets_backend': [
|
||||
'hr_payroll/static/src/components/add_payslips/**',
|
||||
'hr_payroll/static/src/views/add_payslips_hook.js',
|
||||
'hr_payroll/static/src/js/payslip_line_one2many.js'
|
||||
# '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_pivot_*'),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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);
|
||||
|
||||
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
<t t-call="web.external_layout_boxed">
|
||||
<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>
|
||||
<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;">
|
||||
|
|
@ -73,24 +73,24 @@
|
|||
<tr>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;">
|
||||
<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 > 0)" t-as="l">
|
||||
<t t-esc="l.name"/><br/>
|
||||
</div>
|
||||
</td>
|
||||
<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 > 0)" t-as="l">
|
||||
<t t-esc="l.amount"/><br/>
|
||||
<t t-set="income" t-value="income + l.amount"/>
|
||||
</div>
|
||||
</td>
|
||||
<td style="border: 1px solid #ccc; padding: 6px;">
|
||||
<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 > 0)" t-as="l">
|
||||
<t t-esc="l.name"/><br/>
|
||||
</div>
|
||||
</td>
|
||||
<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 > 0)" t-as="l">
|
||||
<t t-esc="l.amount"/><br/>
|
||||
<t t-set="contribution" t-value="contribution + l.amount"/>
|
||||
</div>
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
</div>
|
||||
</td>
|
||||
<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">
|
||||
<t t-esc="l.amount"/><br/>
|
||||
<t t-set="ded" t-value="ded + l.amount"/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue