payroll and recruitment multi company bug issues

This commit is contained in:
pranaysaidurga 2026-07-06 15:01:57 +05:30
parent 1696e0ded1
commit d913750402
21 changed files with 800 additions and 11 deletions

View File

@ -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) {

View File

@ -1 +1,2 @@
from . import models
from . import models
from . import wizard

View File

@ -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',
],
},
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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},
}

View File

@ -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)

View File

@ -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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_hr_salary_advance_user hr.salary.advance.user model_hr_salary_advance hr_payroll.group_hr_payroll_user 1 1 1 0
3 access_hr_salary_advance_manager hr.salary.advance.manager model_hr_salary_advance hr_payroll.group_hr_payroll_manager 1 1 1 1
4 access_hr_payslip_refund_wizard hr.payslip.refund.wizard model_hr_payslip_refund_wizard hr_payroll.group_hr_payroll_user 1 1 1 1

View File

@ -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");

View File

@ -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;
},
});

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,2 @@
from . import hr_payroll_edit_payslip_lines_wizard
from . import hr_payslip_refund_wizard

View File

@ -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()

View File

@ -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",
}

View File

@ -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>

View File

@ -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"

View File

@ -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': [

View File

@ -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 &amp; 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"