payroll and recruitment multi company bug issues
This commit is contained in:
parent
1696e0ded1
commit
d913750402
|
|
@ -1,6 +1,6 @@
|
|||
/** @odoo-module **/
|
||||
import {standardWidgetProps} from "@web/views/widgets/standard_widget_props";
|
||||
import {Component, onMounted, useRef, useState, onWillStart, onWillUpdateProps} from "@odoo/owl";
|
||||
import {Component, onMounted, useRef, useState, onWillStart, onWillUpdateProps, useEffect} from "@odoo/owl";
|
||||
import {registry} from "@web/core/registry";
|
||||
import {useService} from "@web/core/utils/hooks";
|
||||
import {loadJS, loadCSS} from "@web/core/assets";
|
||||
|
|
@ -35,11 +35,22 @@ export class ConsolidatedPayslipGrid extends Component {
|
|||
}
|
||||
});
|
||||
|
||||
this.currentState = this.props.record.data.state;
|
||||
|
||||
onWillUpdateProps(async (nextProps) => {
|
||||
if (nextProps.record.data.state !== this.props.record.data.state) {
|
||||
if (nextProps.record.data.state !== this.currentState) {
|
||||
this.currentState = nextProps.record.data.state;
|
||||
await this.loadGrid();
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
this.loadGrid();
|
||||
},
|
||||
() => [this.props.record.data.state]
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
async loadDependencies() {
|
||||
|
|
@ -498,7 +509,7 @@ export class ConsolidatedPayslipGrid extends Component {
|
|||
|
||||
async refreshGrid() {
|
||||
try {
|
||||
await this.loadGridData();
|
||||
await this.loadGrid();
|
||||
$(this.gridRef.el).pqGrid("refreshDataAndView");
|
||||
this.notification.add("Data refreshed successfully", {type: 'success'});
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1 +1,2 @@
|
|||
from . import models
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
|
|
|||
|
|
@ -14,12 +14,16 @@
|
|||
'security/ir.model.access.csv',
|
||||
'views/hr_payslip_employees_views.xml',
|
||||
'views/hr_salary_advance_views.xml',
|
||||
'views/hr_payroll_edit_payslip_lines_wizard_views.xml',
|
||||
'views/menus.xml',
|
||||
'views/hr_work_entry_views.xml',
|
||||
'views/hr_payslip_refund_views.xml',
|
||||
'wizard/hr_payslip_refund_wizard_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
'hr_payroll_extended/static/src/js/consolidated_payslip_grid_patch.js',
|
||||
'hr_payroll_extended/static/src/js/payslip_line_one2many_patch.js',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
from . import hr_work_entry
|
||||
from . import hr_salary_advance
|
||||
from . import hr_payslip_employees
|
||||
from . import hr_payslip
|
||||
from . import hr_payslip_run
|
||||
from . import hr_leave
|
||||
from . import hr_payslip_refund
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
from odoo import models
|
||||
|
||||
|
||||
class HrLeave(models.Model):
|
||||
_inherit = "hr.leave"
|
||||
|
||||
def _refresh_payroll_extended_payslips(self):
|
||||
payslips = self.env["hr.payslip"].sudo().search([
|
||||
("employee_id", "in", self.mapped("employee_id").ids),
|
||||
("state", "in", ("draft", "verify")),
|
||||
])
|
||||
for leave in self:
|
||||
impacted = payslips.filtered(
|
||||
lambda slip: slip.employee_id == leave.employee_id
|
||||
and leave.date_from
|
||||
and leave.date_to
|
||||
and slip.date_from <= leave.date_to.date()
|
||||
and slip.date_to >= leave.date_from.date()
|
||||
)
|
||||
for payslip in impacted:
|
||||
payslip.mapped("worked_days_line_ids").unlink()
|
||||
payslip.mapped("line_ids").unlink()
|
||||
payslip.write({"edited": False})
|
||||
payslip._compute_worked_days_line_ids()
|
||||
payslip.compute_sheet()
|
||||
|
||||
def action_validate(self, check_state=True):
|
||||
res = super().action_validate(check_state=check_state)
|
||||
self._refresh_payroll_extended_payslips()
|
||||
return res
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.misc import format_date
|
||||
|
||||
|
||||
class HrPayslip(models.Model):
|
||||
_inherit = "hr.payslip"
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
payslips = super().create(vals_list)
|
||||
payslips._check_duplicate_payslip_periods()
|
||||
return payslips
|
||||
|
||||
def write(self, vals):
|
||||
res = super().write(vals)
|
||||
if {"employee_id", "date_from", "date_to", "state"} & vals.keys():
|
||||
self._check_duplicate_payslip_periods()
|
||||
return res
|
||||
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class HrPayslipEmployees(models.TransientModel):
|
||||
|
|
@ -83,3 +84,25 @@ class HrPayslipEmployees(models.TransientModel):
|
|||
"view_mode": "form",
|
||||
"target": "new",
|
||||
}
|
||||
|
||||
def compute_sheet(self):
|
||||
self.ensure_one()
|
||||
date_start, date_end = self._get_generation_dates()
|
||||
if date_start and date_end:
|
||||
employees = self.with_context(active_test=False).employee_ids
|
||||
payslip_run = self.env["hr.payslip.run"].browse(self.env.context.get("active_id")).exists()
|
||||
if payslip_run:
|
||||
employees -= payslip_run.slip_ids.employee_id
|
||||
duplicate_payslips = self.env["hr.payslip"].search([
|
||||
("employee_id", "in", employees.ids),
|
||||
("state", "not in", ("draft", "cancel")),
|
||||
("date_from", "<=", date_end),
|
||||
("date_to", ">=", date_start),
|
||||
])
|
||||
if duplicate_payslips:
|
||||
employees_with_duplicates = ", ".join(sorted(set(duplicate_payslips.mapped("employee_id.name"))))
|
||||
raise UserError(_(
|
||||
"Payslips already exist for the selected period for: %s. Only draft or canceled payslips can be duplicated.",
|
||||
employees_with_duplicates,
|
||||
))
|
||||
return super().compute_sheet()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.misc import format_date
|
||||
|
||||
|
||||
class HrPayslip(models.Model):
|
||||
_inherit = "hr.payslip"
|
||||
|
||||
state = fields.Selection(
|
||||
selection_add=[
|
||||
("partial_refund", "Partial Refund"),
|
||||
("full_refund", "Full Refund"),
|
||||
],
|
||||
ondelete={
|
||||
"partial_refund": "set default",
|
||||
"full_refund": "set default",
|
||||
},
|
||||
)
|
||||
original_refund_payslip_id = fields.Many2one(
|
||||
"hr.payslip",
|
||||
string="Original Payslip",
|
||||
copy=False,
|
||||
index=True,
|
||||
help="Payslip that this refund belongs to.",
|
||||
)
|
||||
refund_payslip_ids = fields.One2many(
|
||||
"hr.payslip",
|
||||
"original_refund_payslip_id",
|
||||
string="Refund Payslips",
|
||||
copy=False,
|
||||
)
|
||||
refund_payslip_count = fields.Integer(compute="_compute_refund_details")
|
||||
partial_refund_amount = fields.Monetary(
|
||||
string="Refund Amount",
|
||||
currency_field="currency_id",
|
||||
copy=False,
|
||||
help="Amount returned by the employee for this partial refund adjustment.",
|
||||
)
|
||||
refund_history_payslip_ids = fields.Many2many(
|
||||
"hr.payslip",
|
||||
compute="_compute_refund_history_payslip_ids",
|
||||
string="Refund History",
|
||||
)
|
||||
refunded_amount = fields.Monetary(
|
||||
string="Refunded Amount",
|
||||
compute="_compute_refund_details",
|
||||
currency_field="currency_id",
|
||||
help="Total amount refunded against this payslip.",
|
||||
)
|
||||
refundable_amount = fields.Monetary(
|
||||
string="Refundable Amount",
|
||||
compute="_compute_refund_details",
|
||||
currency_field="currency_id",
|
||||
help="Remaining amount available for refund.",
|
||||
)
|
||||
|
||||
@api.depends(
|
||||
"net_wage",
|
||||
"state",
|
||||
"partial_refund_amount",
|
||||
"refund_payslip_ids.state",
|
||||
"refund_payslip_ids.partial_refund_amount",
|
||||
)
|
||||
def _compute_refund_details(self):
|
||||
for payslip in self:
|
||||
reference = payslip.original_refund_payslip_id or payslip
|
||||
refund_slips = reference.refund_payslip_ids.filtered(lambda slip: slip.state != "cancel")
|
||||
refunded_amount = sum(refund_slips.mapped("partial_refund_amount"))
|
||||
total_amount = abs(payslip.net_wage)
|
||||
if reference.state == "full_refund":
|
||||
refunded_amount = max(refunded_amount, abs(reference.net_wage))
|
||||
if payslip.original_refund_payslip_id:
|
||||
total_amount = abs(reference.net_wage)
|
||||
payslip.refund_payslip_count = len(refund_slips) if payslip == reference else 0
|
||||
payslip.refunded_amount = refunded_amount
|
||||
payslip.refundable_amount = max(total_amount - refunded_amount, 0.0)
|
||||
|
||||
@api.depends("employee_id", "date_from", "date_to", "original_refund_payslip_id")
|
||||
def _compute_refund_history_payslip_ids(self):
|
||||
for payslip in self:
|
||||
reference = payslip.original_refund_payslip_id or payslip
|
||||
if not reference.employee_id or not reference.date_from or not reference.date_to:
|
||||
payslip.refund_history_payslip_ids = False
|
||||
continue
|
||||
payslip.refund_history_payslip_ids = self.search([
|
||||
("id", "!=", payslip.id),
|
||||
("employee_id", "=", reference.employee_id.id),
|
||||
("state", "!=", "cancel"),
|
||||
("date_from", "<=", reference.date_to),
|
||||
("date_to", ">=", reference.date_from),
|
||||
"|",
|
||||
("credit_note", "=", True),
|
||||
("original_refund_payslip_id", "!=", False),
|
||||
])
|
||||
|
||||
def _check_duplicate_payslip_periods(self):
|
||||
protected_slips = self.filtered(
|
||||
lambda slip: (
|
||||
slip.employee_id
|
||||
and slip.date_from
|
||||
and slip.date_to
|
||||
and slip.state not in ("draft", "cancel", "full_refund", "partial_refund")
|
||||
and not slip.credit_note
|
||||
and not slip.original_refund_payslip_id
|
||||
)
|
||||
)
|
||||
for slip in protected_slips:
|
||||
duplicate = self.search([
|
||||
("id", "!=", slip.id),
|
||||
("employee_id", "=", slip.employee_id.id),
|
||||
("state", "not in", ("draft", "cancel", "full_refund", "partial_refund")),
|
||||
("credit_note", "=", False),
|
||||
("date_from", "<=", slip.date_to),
|
||||
("date_to", ">=", slip.date_from),
|
||||
], limit=1)
|
||||
if duplicate:
|
||||
raise ValidationError(_(
|
||||
"A non-draft payslip already exists for %(employee)s in the selected period (%(date_from)s - %(date_to)s). "
|
||||
"Cancel or reset the duplicate payslip to draft before confirming another one.",
|
||||
employee=slip.employee_id.name,
|
||||
date_from=format_date(self.env, slip.date_from),
|
||||
date_to=format_date(self.env, slip.date_to),
|
||||
))
|
||||
|
||||
def refund_sheet(self):
|
||||
self.ensure_one()
|
||||
if self.credit_note or self.original_refund_payslip_id:
|
||||
raise ValidationError(_("Refunds can only be created from the original payslip."))
|
||||
if self.state not in ("done", "paid", "partial_refund"):
|
||||
raise ValidationError(_("Only done, paid, or partially refunded payslips can be refunded."))
|
||||
return {
|
||||
"name": _("Refund Payslip"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "hr.payslip.refund.wizard",
|
||||
"view_mode": "form",
|
||||
"target": "new",
|
||||
"context": {
|
||||
"default_payslip_id": self.id,
|
||||
"default_refund_amount": self.refundable_amount or abs(self.net_wage),
|
||||
},
|
||||
}
|
||||
|
||||
def _is_invalid(self):
|
||||
self.ensure_one()
|
||||
if self.state in ("partial_refund", "full_refund"):
|
||||
return False
|
||||
return super()._is_invalid()
|
||||
|
||||
def action_view_refund_payslips(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"name": _("Refund Payslips"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "hr.payslip",
|
||||
"view_mode": "list,form",
|
||||
"domain": [("id", "in", self.refund_payslip_ids.ids)],
|
||||
"context": {"default_original_refund_payslip_id": self.id},
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
|
||||
class HrPayslipRun(models.Model):
|
||||
_inherit = "hr.payslip.run"
|
||||
|
||||
@api.onchange("date_start")
|
||||
def _onchange_date_start_extended(self):
|
||||
for payslip_run in self:
|
||||
if not payslip_run.date_start:
|
||||
continue
|
||||
if payslip_run.date_start.day == 1:
|
||||
payslip_run.date_end = payslip_run.date_start + relativedelta(day=31)
|
||||
else:
|
||||
payslip_run.date_end = payslip_run.date_start + relativedelta(months=1)
|
||||
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_hr_salary_advance_user,hr.salary.advance.user,model_hr_salary_advance,hr_payroll.group_hr_payroll_user,1,1,1,0
|
||||
access_hr_salary_advance_manager,hr.salary.advance.manager,model_hr_salary_advance,hr_payroll.group_hr_payroll_manager,1,1,1,1
|
||||
access_hr_payslip_refund_wizard,hr.payslip.refund.wizard,model_hr_payslip_refund_wizard,hr_payroll.group_hr_payroll_user,1,1,1,1
|
||||
|
|
|
|||
|
|
|
@ -77,6 +77,37 @@ patch(ConsolidatedPayslipGrid.prototype, {
|
|||
this.showNotification("Data refreshed successfully");
|
||||
},
|
||||
|
||||
async getColumns() {
|
||||
const columns = await super.getColumns(...arguments);
|
||||
const component = this;
|
||||
const editColumn = columns.find((column) => column.title === "Edit");
|
||||
if (editColumn) {
|
||||
editColumn.postRender = function (ui) {
|
||||
const grid = this;
|
||||
const $cell = grid.getCell(ui);
|
||||
$cell.find(".row-btn-edit")
|
||||
.button({ icons: { primary: "ui-icon-pencil" } })
|
||||
.off("click.hr_payroll_extended")
|
||||
.on("click.hr_payroll_extended", async function (evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopImmediatePropagation();
|
||||
const action = await component.orm.call(
|
||||
"hr.payslip",
|
||||
"action_edit_payslip_lines",
|
||||
[ui.rowData.id]
|
||||
);
|
||||
action.views = [[false, "form"]];
|
||||
await component.action.doAction(action, {
|
||||
onClose: async () => {
|
||||
await component.refreshGrid();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
return columns;
|
||||
},
|
||||
|
||||
async saveChanges() {
|
||||
const grid = this._getGridInstance();
|
||||
const updatedData = grid.option("dataModel.data");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { WorkedDaysField } from "@hr_payroll/js/payslip_line_one2many";
|
||||
|
||||
const fieldGetter = Object.getOwnPropertyDescriptor(
|
||||
Object.getPrototypeOf(WorkedDaysField.prototype),
|
||||
"fieldComponentProps"
|
||||
).get;
|
||||
|
||||
Object.defineProperty(WorkedDaysField.prototype, "fieldComponentProps", {
|
||||
get() {
|
||||
const props = fieldGetter.call(this);
|
||||
const record = this.props.record;
|
||||
if (!record.isExtendedWorkedDaysField) {
|
||||
record.isExtendedWorkedDaysField = true;
|
||||
const oldUpdate = record.update.bind(record);
|
||||
record.update = async (changes) => {
|
||||
if ("amount" in changes || "quantity" in changes || "number_of_days" in changes || "number_of_hours" in changes) {
|
||||
await oldUpdate(changes);
|
||||
await record._parentRecord.save();
|
||||
const wizardId = record.model.config.resId;
|
||||
if (wizardId) {
|
||||
const action = await this.orm.call(
|
||||
"hr.payroll.edit.payslip.lines.wizard",
|
||||
"recompute_worked_days_lines",
|
||||
[wizardId]
|
||||
);
|
||||
if (action) {
|
||||
await this.actionService.doAction(action);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await oldUpdate(changes);
|
||||
}
|
||||
};
|
||||
}
|
||||
return props;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="hr_payroll_edit_payslip_lines_form_wizard_inherit_extended" model="ir.ui.view">
|
||||
<field name="name">hr.payroll.edit.payslip.lines.wizard.form.inherit.extended</field>
|
||||
<field name="model">hr.payroll.edit.payslip.lines.wizard</field>
|
||||
<field name="inherit_id" ref="hr_payroll.hr_payroll_edit_payslip_lines_form_wizard"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='worked_days_line_ids']" position="replace">
|
||||
<notebook>
|
||||
<page string="Worked Days" name="worked_days">
|
||||
<field name="worked_days_line_ids" widget="worked_days_line_one2many" nolabel="1" context="{'default_slip_id': payslip_id}">
|
||||
<list string="Worked Days" editable="bottom" delete="0">
|
||||
<field name="work_entry_type_id"/>
|
||||
<field name="name" required="1"/>
|
||||
<field name="number_of_days"/>
|
||||
<field name="number_of_hours"/>
|
||||
<field name="code" optional="hide"/>
|
||||
<field name="amount"/>
|
||||
<field name="ytd" optional="show" readonly="0" column_invisible="not parent.ytd_computation"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Leaves" name="leaves">
|
||||
<button name="action_open_leave_application" string="Apply Leave" type="object" class="oe_highlight"/>
|
||||
<field name="period_leave_ids" nolabel="1" readonly="1">
|
||||
<list string="Leaves in Payslip Period">
|
||||
<field name="holiday_status_id"/>
|
||||
<field name="request_date_from"/>
|
||||
<field name="request_date_to"/>
|
||||
<field name="number_of_days"/>
|
||||
<field name="state"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
<page string="Salary Structure" name="salary_structure">
|
||||
<field name="line_ids" widget="payslip_line_one2many" nolabel="1" context="{'default_slip_id': payslip_id}">
|
||||
<list string="Salary Structure" editable="bottom" decoration-info="total == 0" delete="0">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="slip_id" column_invisible="True"/>
|
||||
<field name="struct_id" column_invisible="True"/>
|
||||
<field name="name" required="True"/>
|
||||
<field name="code" readonly="1" force_save="1" optional="hide"/>
|
||||
<field name="category_id" readonly="1" column_invisible="True" force_save="1"/>
|
||||
<field name="quantity"/>
|
||||
<field name="rate" readonly="1" force_save="1"/>
|
||||
<field name="salary_rule_id" options="{'no_create_edit': True}" required="1"/>
|
||||
<field name="amount"/>
|
||||
<field name="total" readonly="1" force_save="1"/>
|
||||
<field name="ytd" optional="show" readonly="0" force_save="1" column_invisible="not parent.ytd_computation"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='line_ids']" position="replace"/>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_hr_payslip_tree_refund_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.payslip.list.refund.inherit</field>
|
||||
<field name="model">hr.payslip</field>
|
||||
<field name="inherit_id" ref="hr_payroll.view_hr_payslip_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='state']" position="attributes">
|
||||
<attribute name="decoration-muted">state == 'partial_refund'</attribute>
|
||||
<attribute name="decoration-danger">state == 'full_refund'</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='net_wage']" position="after">
|
||||
<field name="partial_refund_amount" widget="monetary" options="{'currency_field': 'currency_id'}" optional="hide"/>
|
||||
<field name="refunded_amount" widget="monetary" options="{'currency_field': 'currency_id'}" optional="hide"/>
|
||||
<field name="refundable_amount" widget="monetary" options="{'currency_field': 'currency_id'}" optional="hide"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_hr_payslip_form_refund_inherit" model="ir.ui.view">
|
||||
<field name="name">hr.payslip.form.refund.inherit</field>
|
||||
<field name="model">hr.payslip</field>
|
||||
<field name="inherit_id" ref="hr_payroll.view_hr_payslip_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='refund_sheet']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@name='action_payslip_payment_report']" position="after">
|
||||
<button string="Refund" name="refund_sheet" type="object" class="oe_highlight"
|
||||
invisible="credit_note or original_refund_payslip_id or state not in ('paid', 'partial_refund')"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='state']" position="attributes">
|
||||
<attribute name="statusbar_visible">draft,verify,done,paid,partial_refund,full_refund</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button type="object" class="oe_stat_button" name="action_view_refund_payslips"
|
||||
icon="fa-undo" invisible="refund_payslip_count == 0">
|
||||
<field name="refund_payslip_count" widget="statinfo" string="Refunds"/>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='has_refund_slip']" position="after">
|
||||
<field name="original_refund_payslip_id" invisible="not original_refund_payslip_id" readonly="1"/>
|
||||
<field name="partial_refund_amount" readonly="1" invisible="not partial_refund_amount"/>
|
||||
<field name="refund_payslip_count" invisible="1"/>
|
||||
<field name="refunded_amount" readonly="1" invisible="not refunded_amount"/>
|
||||
<field name="refundable_amount" readonly="1" invisible="not refunded_amount"/>
|
||||
</xpath>
|
||||
<xpath expr="//page[@name='account_info']" position="before">
|
||||
<page string="Refund Details" name="refund_details"
|
||||
invisible="not refund_payslip_ids and not original_refund_payslip_id and not refund_history_payslip_ids and not refunded_amount">
|
||||
<group>
|
||||
<group>
|
||||
<field name="original_refund_payslip_id" readonly="1"/>
|
||||
<field name="refunded_amount" readonly="1"/>
|
||||
<field name="refundable_amount" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Refund Payslips" invisible="not refund_payslip_ids"/>
|
||||
<field name="refund_payslip_ids" readonly="1" invisible="not refund_payslip_ids">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="date_from"/>
|
||||
<field name="date_to"/>
|
||||
<field name="partial_refund_amount"/>
|
||||
<field name="net_wage"/>
|
||||
<field name="state"/>
|
||||
</list>
|
||||
</field>
|
||||
<separator string="Refund History For This Period" invisible="not refund_history_payslip_ids"/>
|
||||
<field name="refund_history_payslip_ids" readonly="1" invisible="not refund_history_payslip_ids">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="date_from"/>
|
||||
<field name="date_to"/>
|
||||
<field name="partial_refund_amount"/>
|
||||
<field name="net_wage"/>
|
||||
<field name="state"/>
|
||||
</list>
|
||||
</field>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import hr_payroll_edit_payslip_lines_wizard
|
||||
from . import hr_payslip_refund_wizard
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
from odoo import Command, api, fields, models, _
|
||||
|
||||
|
||||
class HrPayrollEditPayslipLinesWizard(models.TransientModel):
|
||||
_inherit = "hr.payroll.edit.payslip.lines.wizard"
|
||||
|
||||
period_leave_ids = fields.Many2many("hr.leave", compute="_compute_period_leave_ids", string="Leaves in Period")
|
||||
|
||||
def _reload_wizard_action(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": self._name,
|
||||
"view_mode": "form",
|
||||
"res_id": self.id,
|
||||
"views": [(False, "form")],
|
||||
"target": "new",
|
||||
}
|
||||
|
||||
@api.depends("payslip_id.employee_id", "payslip_id.date_from", "payslip_id.date_to")
|
||||
def _compute_period_leave_ids(self):
|
||||
for wizard in self:
|
||||
if not wizard.payslip_id.employee_id or not wizard.payslip_id.date_from or not wizard.payslip_id.date_to:
|
||||
wizard.period_leave_ids = False
|
||||
continue
|
||||
date_from = fields.Datetime.to_datetime(wizard.payslip_id.date_from)
|
||||
date_to = fields.Datetime.to_datetime(wizard.payslip_id.date_to).replace(hour=23, minute=59, second=59)
|
||||
wizard.period_leave_ids = self.env["hr.leave"].sudo().search([
|
||||
("employee_id", "=", wizard.payslip_id.employee_id.id),
|
||||
("state", "in", ("confirm", "validate1", "validate")),
|
||||
("date_from", "<=", date_to),
|
||||
("date_to", ">=", date_from),
|
||||
])
|
||||
|
||||
def action_open_leave_application(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"name": _("Apply Leave"),
|
||||
"res_model": "hr.leave",
|
||||
"view_mode": "form",
|
||||
"target": "new",
|
||||
"context": {
|
||||
"default_employee_id": self.payslip_id.employee_id.id,
|
||||
"default_request_date_from": self.payslip_id.date_from,
|
||||
"default_request_date_to": self.payslip_id.date_to,
|
||||
"default_private_name": _("Added from payslip %s", self.payslip_id.display_name),
|
||||
"hr_payroll_extended_refresh_payslip_id": self.payslip_id.id,
|
||||
},
|
||||
}
|
||||
|
||||
def _compute_worked_days_amounts(self):
|
||||
self.ensure_one()
|
||||
payslip = self.payslip_id
|
||||
hours_per_day = payslip._get_worked_day_lines_hours_per_day()
|
||||
total_hours = sum(self.worked_days_line_ids.mapped("number_of_hours"))
|
||||
days_count = float((payslip.date_to - payslip.date_from).days + 1)
|
||||
unpaid_type_ids = set(payslip.struct_id.unpaid_work_entry_type_ids.ids)
|
||||
|
||||
for line in self.worked_days_line_ids:
|
||||
if hours_per_day and line.number_of_days and not line.number_of_hours:
|
||||
line.number_of_hours = line.number_of_days * hours_per_day
|
||||
elif hours_per_day and line.number_of_days:
|
||||
line.number_of_hours = line.number_of_days * hours_per_day
|
||||
|
||||
is_paid = line.work_entry_type_id.id not in unpaid_type_ids
|
||||
if not payslip.contract_id or line.code == "OUT":
|
||||
line.amount = 0
|
||||
elif payslip.wage_type == "hourly":
|
||||
line.amount = payslip.contract_id.hourly_wage * line.number_of_hours if is_paid else 0
|
||||
elif line.work_entry_type_id.is_leave:
|
||||
daily_wage = payslip.contract_id.contract_wage / days_count if days_count else 0
|
||||
days = round(line.number_of_hours / hours_per_day, 5) if hours_per_day else line.number_of_days
|
||||
line.amount = daily_wage * days if is_paid else -(daily_wage * days)
|
||||
else:
|
||||
days_to_remove = round((total_hours - line.number_of_hours) / hours_per_day, 5) if hours_per_day else 0
|
||||
daily_wage = payslip.contract_id.contract_wage / days_count if days_count else 0
|
||||
line.amount = payslip.contract_id.contract_wage - (daily_wage * (days_to_remove if days_to_remove > 0 else 1)) if is_paid else 0
|
||||
|
||||
def _recompute_salary_lines_from_worked_days(self):
|
||||
self.ensure_one()
|
||||
localdict = self.payslip_id._get_localdict()
|
||||
localdict["worked_days"] = {line.code: line for line in self.worked_days_line_ids if line.code}
|
||||
payslip = self.payslip_id.with_context(force_payslip_localdict=localdict)
|
||||
self.line_ids = [Command.clear()] + [Command.create(line) for line in payslip._get_payslip_lines()]
|
||||
|
||||
def recompute_worked_days_lines(self):
|
||||
self.ensure_one()
|
||||
self._compute_worked_days_amounts()
|
||||
self._recompute_salary_lines_from_worked_days()
|
||||
return self._reload_wizard_action()
|
||||
|
||||
|
||||
class HrPayrollEditPayslipWorkedDaysLine(models.TransientModel):
|
||||
_inherit = "hr.payroll.edit.payslip.worked.days.line"
|
||||
|
||||
@api.onchange("number_of_days")
|
||||
def _onchange_number_of_days_extended(self):
|
||||
for line in self:
|
||||
payslip = line.edit_payslip_lines_wizard_id.payslip_id
|
||||
if payslip and line.number_of_days:
|
||||
line.number_of_hours = line.number_of_days * payslip._get_worked_day_lines_hours_per_day()
|
||||
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.float_utils import float_compare, float_is_zero
|
||||
|
||||
|
||||
class HrPayslipRefundWizard(models.TransientModel):
|
||||
_name = "hr.payslip.refund.wizard"
|
||||
_description = "Payslip Refund"
|
||||
|
||||
payslip_id = fields.Many2one("hr.payslip", string="Payslip", required=True, readonly=True)
|
||||
refund_type = fields.Selection(
|
||||
[
|
||||
("full", "Full Refund"),
|
||||
("partial", "Partial Refund"),
|
||||
],
|
||||
string="Refund Type",
|
||||
required=True,
|
||||
default="full",
|
||||
)
|
||||
refund_amount = fields.Monetary(
|
||||
string="Refund Amount",
|
||||
required=True,
|
||||
currency_field="currency_id",
|
||||
)
|
||||
currency_id = fields.Many2one(related="payslip_id.currency_id")
|
||||
original_amount = fields.Monetary(
|
||||
string="Original Net Amount",
|
||||
related="payslip_id.net_wage",
|
||||
currency_field="currency_id",
|
||||
readonly=True,
|
||||
)
|
||||
already_refunded_amount = fields.Monetary(
|
||||
string="Already Refunded",
|
||||
related="payslip_id.refunded_amount",
|
||||
currency_field="currency_id",
|
||||
readonly=True,
|
||||
)
|
||||
refundable_amount = fields.Monetary(
|
||||
string="Available to Refund",
|
||||
related="payslip_id.refundable_amount",
|
||||
currency_field="currency_id",
|
||||
readonly=True,
|
||||
)
|
||||
previous_refund_ids = fields.Many2many(
|
||||
"hr.payslip",
|
||||
compute="_compute_previous_refund_ids",
|
||||
string="Previous Refunds",
|
||||
)
|
||||
reason = fields.Text(string="Reason")
|
||||
|
||||
@api.depends("payslip_id", "payslip_id.date_from", "payslip_id.date_to", "payslip_id.employee_id")
|
||||
def _compute_previous_refund_ids(self):
|
||||
for wizard in self:
|
||||
if not wizard.payslip_id:
|
||||
wizard.previous_refund_ids = False
|
||||
continue
|
||||
linked_refunds = self.env["hr.payslip"].search([
|
||||
("id", "!=", wizard.payslip_id.id),
|
||||
("employee_id", "=", wizard.payslip_id.employee_id.id),
|
||||
("state", "!=", "cancel"),
|
||||
("original_refund_payslip_id", "=", wizard.payslip_id.id),
|
||||
])
|
||||
legacy_refunds = self.env["hr.payslip"].search([
|
||||
("id", "!=", wizard.payslip_id.id),
|
||||
("employee_id", "=", wizard.payslip_id.employee_id.id),
|
||||
("state", "!=", "cancel"),
|
||||
("credit_note", "=", True),
|
||||
("date_from", "<=", wizard.payslip_id.date_to),
|
||||
("date_to", ">=", wizard.payslip_id.date_from),
|
||||
])
|
||||
wizard.previous_refund_ids = linked_refunds | legacy_refunds
|
||||
|
||||
@api.onchange("refund_type", "payslip_id")
|
||||
def _onchange_refund_type(self):
|
||||
for wizard in self:
|
||||
if not wizard.payslip_id:
|
||||
continue
|
||||
if wizard.refund_type == "full":
|
||||
wizard.refund_amount = wizard.payslip_id.refundable_amount or abs(wizard.payslip_id.net_wage)
|
||||
elif not wizard.refund_amount:
|
||||
wizard.refund_amount = wizard.payslip_id.refundable_amount
|
||||
|
||||
def _validate_refund(self):
|
||||
self.ensure_one()
|
||||
payslip = self.payslip_id
|
||||
if payslip.credit_note or payslip.original_refund_payslip_id:
|
||||
raise ValidationError(_("Refunds can only be created from the original payslip."))
|
||||
if payslip.state not in ("done", "paid", "partial_refund"):
|
||||
raise ValidationError(_("Only done, paid, or partially refunded payslips can be refunded."))
|
||||
if self.refund_amount <= 0:
|
||||
raise ValidationError(_("Refund amount must be greater than zero."))
|
||||
|
||||
precision = payslip.currency_id.rounding if payslip.currency_id else 0.01
|
||||
if self.refund_type == "full" and not float_is_zero(
|
||||
self.refund_amount - payslip.refundable_amount,
|
||||
precision_rounding=precision,
|
||||
):
|
||||
raise ValidationError(_("A full refund must refund the complete remaining amount."))
|
||||
if float_compare(self.refund_amount, payslip.refundable_amount, precision_rounding=precision) > 0:
|
||||
raise ValidationError(_("Refund amount cannot be greater than the available refundable amount."))
|
||||
|
||||
def _scale_adjusted_payslip_lines(self, adjusted_payslip, ratio):
|
||||
adjusted_payslip.ensure_one()
|
||||
for worked_day in adjusted_payslip.worked_days_line_ids:
|
||||
worked_day.write({
|
||||
"amount": worked_day.amount * ratio,
|
||||
})
|
||||
for line in adjusted_payslip.input_line_ids:
|
||||
line.write({"amount": line.amount * ratio})
|
||||
for line in adjusted_payslip.line_ids:
|
||||
line.write({
|
||||
"amount": line.amount * ratio,
|
||||
"total": line.total * ratio,
|
||||
})
|
||||
|
||||
def action_confirm_refund(self):
|
||||
self.ensure_one()
|
||||
self._validate_refund()
|
||||
|
||||
payslip = self.payslip_id
|
||||
if self.refund_type == "full":
|
||||
note = self.reason or _("Full refund created.")
|
||||
payslip.refund_payslip_ids.filtered(lambda slip: slip.state == "paid").write({"state": "partial_refund"})
|
||||
payslip.write({
|
||||
"state": "full_refund",
|
||||
"note": "%s\n%s" % (payslip.note or "", note),
|
||||
})
|
||||
return {"type": "ir.actions.act_window_close"}
|
||||
|
||||
original_net = abs(payslip.net_wage)
|
||||
if not original_net:
|
||||
raise ValidationError(_("Cannot create a partial refund for a payslip with zero net amount."))
|
||||
|
||||
adjusted_net = payslip.refundable_amount - self.refund_amount
|
||||
precision = payslip.currency_id.rounding if payslip.currency_id else 0.01
|
||||
if float_compare(adjusted_net, 0.0, precision_rounding=precision) <= 0:
|
||||
raise ValidationError(_("Use Full Refund when no amount should remain on the payslip."))
|
||||
ratio = adjusted_net / original_net
|
||||
payslip.refund_payslip_ids.filtered(lambda slip: slip.state == "paid").write({"state": "partial_refund"})
|
||||
adjusted_payslip = payslip.copy({
|
||||
"credit_note": False,
|
||||
"original_refund_payslip_id": payslip.id,
|
||||
"partial_refund_amount": self.refund_amount,
|
||||
"name": _("Adjusted Payslip: %(payslip)s", payslip=payslip.name),
|
||||
"edited": True,
|
||||
"state": "paid",
|
||||
"paid_date": fields.Date.today(),
|
||||
"note": self.reason or _("Partial refund of %(amount)s for %(payslip)s", amount=self.refund_amount, payslip=payslip.name),
|
||||
})
|
||||
self._scale_adjusted_payslip_lines(adjusted_payslip, ratio)
|
||||
payslip.write({"state": "partial_refund"})
|
||||
|
||||
return {
|
||||
"name": _("Adjusted Payslip"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "hr.payslip",
|
||||
"view_mode": "form",
|
||||
"res_id": adjusted_payslip.id,
|
||||
"target": "current",
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_hr_payslip_refund_wizard_form" model="ir.ui.view">
|
||||
<field name="name">hr.payslip.refund.wizard.form</field>
|
||||
<field name="model">hr.payslip.refund.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Refund Payslip">
|
||||
<group>
|
||||
<field name="payslip_id"/>
|
||||
<field name="refund_type" widget="radio"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="original_amount"/>
|
||||
<field name="already_refunded_amount"/>
|
||||
<field name="refundable_amount"/>
|
||||
<field name="refund_amount" readonly="refund_type == 'full'"/>
|
||||
<field name="reason" placeholder="Reason for refund"/>
|
||||
</group>
|
||||
<group string="Previous Refunds" invisible="not previous_refund_ids">
|
||||
<field name="previous_refund_ids" nolabel="1" readonly="1">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="date_from"/>
|
||||
<field name="date_to"/>
|
||||
<field name="net_wage"/>
|
||||
<field name="state"/>
|
||||
</list>
|
||||
</field>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Confirm Refund" name="action_confirm_refund" type="object" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -583,6 +583,7 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
"notice_period": parsed_data.get("notice_period"),
|
||||
"applicant_comments": parsed_data.get("summary"),
|
||||
"hr_job_recruitment": self.job_recruitment_id.id,
|
||||
"company_id": self.env.company.id,
|
||||
}
|
||||
if parsed_data.get("total_experience_years"):
|
||||
applicant_vals["exp_type"] = "experienced"
|
||||
|
|
@ -612,9 +613,12 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
|
||||
candidate = False
|
||||
if email_value:
|
||||
candidate = search_model.search([('id','!=',self.env.context.get('candidate_id')),'|',('email_from','=', email_value),("email_normalized", "=", email_value)], limit=1)
|
||||
candidate = search_model.search(['|',('company_id','=',False),('company_id','in',[self.env.company.id]),('id','!=',self.env.context.get('candidate_id')),'|',('email_from','=', email_value),("email_normalized", "=", email_value)], limit=1)
|
||||
if not candidate and phone_values:
|
||||
candidate = search_model.search([
|
||||
'|',
|
||||
('company_id','=',False),
|
||||
('company_id','in',[self.env.company.id]),
|
||||
"|","|",('partner_phone',"in",phone_values),
|
||||
("partner_phone_sanitized", "in", phone_values),
|
||||
("alternate_phone", "in", phone_values),
|
||||
|
|
@ -1794,8 +1798,8 @@ class HrRecruitmentAutoDocWizard(models.TransientModel):
|
|||
job_request = False
|
||||
if request_id:
|
||||
job_request = job_request_model.search([("recruitment_sequence", "=", request_id)], limit=1)
|
||||
if not job_request and job_title:
|
||||
job_request = job_request_model.search([("name", "=ilike", job_title)], limit=1)
|
||||
# if not job_request and job_title:
|
||||
# job_request = job_request_model.search([("name", "=ilike", job_title)], limit=1)
|
||||
if job_request:
|
||||
return job_request, "updated"
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
'version': '0.1',
|
||||
|
||||
# any module necessary for this one to work correctly
|
||||
'depends': ['base','hr_recruitment','hr','hr_recruitment_skills','website_hr_recruitment','requisitions','search_view_extension'],
|
||||
'depends': ['base','hr_recruitment','hr','hr_recruitment_skills','website_hr_recruitment','requisitions','search_view_extension','hr_employee_extended','website_payment'],
|
||||
|
||||
# always loaded
|
||||
'data': [
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
name="action_validate_personal_details"
|
||||
type="object"
|
||||
class="btn btn-success"
|
||||
invisible="not employee_id">
|
||||
invisible="personal_details_status == 'pending' or not employee_id">
|
||||
<div>
|
||||
Click here to save & Update Employee Data
|
||||
<field name="contact_details_status"
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
name="action_validate_personal_details"
|
||||
type="object"
|
||||
class="btn btn-danger"
|
||||
invisible="employee_id">
|
||||
invisible="personal_details_status == 'validated' or not employee_id">
|
||||
<div>
|
||||
Click here to save, Validate and Update into Employee Data
|
||||
<field name="personal_details_status"
|
||||
|
|
|
|||
Loading…
Reference in New Issue