Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions academic_hr/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from . import models
from . import controllers
6 changes: 5 additions & 1 deletion academic_hr/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Academic HR",
"version": "18.0.1.0.0",
"version": "18.0.1.1.0",
"category": "Human Resources",
"summary": "HR extensions for academic institutions with multiple employee support",
"author": "ADHOC SA",
Expand All @@ -11,12 +11,16 @@
"hr_holidays",
"hr_timesheet",
"timesheet_grid",
"portal",
],
"data": [
"security/academic_hr_security.xml",
"security/ir.model.access.csv",
"views/hr_leave_views.xml",
"views/hr_timesheet_views.xml",
"views/hr_employee_views.xml",
"views/hr_leave_allocation_views.xml",
"views/portal_templates.xml",
],
"installable": True,
"auto_install": False,
Expand Down
1 change: 1 addition & 0 deletions academic_hr/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import portal
67 changes: 67 additions & 0 deletions academic_hr/controllers/portal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from odoo import http
from odoo.addons.portal.controllers.portal import CustomerPortal
from odoo.http import request


class CustomerPortal(CustomerPortal):
@http.route(["/my/personal_data"], type="http", auth="user", website=True)
def portal_my_personal_data(self, **kw):
user = request.env.user
employee = request.env["hr.employee"].search(
[("user_id", "=", user.id), ("main_employee_id", "=", False)], limit=1
)

if not employee:
return request.redirect("/my")

values = {
"employee": employee,
"page_name": "personal_data",
"countries": request.env["res.country"].search([]),
"states": request.env["res.country.state"].search([]),
}

if kw.get("error"):
values["error_message"] = "There was an error updating your information. Please try again."

if kw.get("success"):
values["success_message"] = "Your personal data has been updated successfully."

return request.render("academic_hr.portal_my_personal_data", values)

@http.route(["/my/personal_data/update"], type="http", auth="user", website=True, methods=["POST"])
def portal_update_personal_data(self, **kw):
user = request.env.user
employee = request.env["hr.employee"].search(
[("user_id", "=", user.id), ("main_employee_id", "=", False)], limit=1
)

if not employee:
return request.redirect("/my")

try:
state_id = int(kw.get("state_id")) if kw.get("state_id") else False
except (ValueError, TypeError):
state_id = False

try:
country_id = int(kw.get("country_id")) if kw.get("country_id") else False
except (ValueError, TypeError):
country_id = False

employee_vals = {
"private_phone": kw.get("phone") or False,
"private_email": kw.get("email") or False,
"private_street": kw.get("street") or False,
"private_city": kw.get("city") or False,
"private_zip": kw.get("zip") or False,
"private_state_id": state_id,
"private_country_id": country_id,
}

try:
employee.write(employee_vals)
except Exception:
return request.redirect("/my/personal_data?error=1")

return request.redirect("/my/personal_data?success=1")
15 changes: 15 additions & 0 deletions academic_hr/security/academic_hr_security.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<odoo>
<record id="hr_employee_rule_portal" model="ir.rule">
<field name="name">Portal: See own employee record</field>
<field name="model_id" ref="hr.model_hr_employee"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>

<record id="hr_employee_public_rule_portal" model="ir.rule">
<field name="name">Portal: See own public employee record</field>
<field name="model_id" ref="hr.model_hr_employee_public"/>
<field name="domain_force">[('user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
</odoo>
3 changes: 3 additions & 0 deletions academic_hr/security/ir.model.access.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_employee_portal,hr.employee.portal,hr.model_hr_employee,base.group_portal,1,0,0,0
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Los permisos de acceso definen solo lectura (perm_write=0) para usuarios portal sobre hr.employee, pero el controlador en controllers/portal.py usa sudo() para realizar el write. Esto crea una inconsistencia: el modelo está configurado sin permisos de escritura para portal, pero el controlador los omite con sudo(). La solución correcta sería: (1) otorgar perm_write=1 para permitir que usuarios portal actualicen sus propios registros (protegidos por la regla de dominio que restringe a user_id = user.id), y (2) eliminar el sudo() del controlador para respetar estas reglas de acceso. Esto mantiene un modelo de seguridad coherente y auditable.

Suggested change
access_hr_employee_portal,hr.employee.portal,hr.model_hr_employee,base.group_portal,1,0,0,0
access_hr_employee_portal,hr.employee.portal,hr.model_hr_employee,base.group_portal,1,1,0,0

Copilot uses AI. Check for mistakes.
access_hr_employee_public_portal,hr.employee.public.portal,hr.model_hr_employee_public,base.group_portal,1,0,0,0
134 changes: 134 additions & 0 deletions academic_hr/views/portal_templates.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<odoo>
<template id="portal_my_home_menu_personal_data" name="Portal layout : Personal Data menu entry" inherit_id="portal.portal_breadcrumbs" priority="20">
<xpath expr="//ol[hasclass('o_portal_submenu')]" position="inside">
<li t-if="page_name == 'personal_data'" class="breadcrumb-item active">
Personal Data
</li>
</xpath>
</template>

<template id="portal_side_content_academic_hr" inherit_id="portal.side_content">
<xpath expr="//div[@name='portal_contact']" position="after">
<t t-set="current_employee" t-value="request.env['hr.employee'].search([('user_id', '=', user_id.id), ('main_employee_id', '=', False)], limit=1)"/>
<div class="o_portal_my_details mt-5" t-if="current_employee">
<h5 class="mb-3 pb-3 border-bottom">My Personal Data</h5>
<div t-if="current_employee.private_email" class="d-flex align-items-baseline mb-1">
<i class="fa fa-envelope fa-fw me-1 text-600"/>
<span class="text-break w-100" t-esc="current_employee.private_email"/>
</div>
<div t-if="current_employee.private_phone" class="d-flex flex-nowrap align-items-center mb-1">
<i class="fa fa-phone fa-fw me-1 text-600"/>
<span t-esc="current_employee.private_phone"/>
</div>
<div t-if="current_employee.private_street" class="d-flex align-items-baseline mb-1">
<i class="fa fa-map-marker fa-fw me-1 text-600"/>
<span class="w-100 lh-sm text-break d-block" t-esc="current_employee.private_street"/>
</div>
<div t-if="current_employee.private_city or current_employee.private_zip" class="d-flex align-items-baseline mb-1">
<i class="fa fa-fw me-1 text-600"/>
<span>
<t t-if="current_employee.private_zip" t-esc="current_employee.private_zip"/>
<t t-if="current_employee.private_zip and current_employee.private_city"> </t>
<t t-if="current_employee.private_city" t-esc="current_employee.private_city"/>
<t t-if="current_employee.private_country_id">, <t t-esc="current_employee.private_country_id.name"/></t>
</span>
</div>
<a role="button" href="/my/personal_data" class="btn btn-link p-0 mt-3"><i class="fa fa-pencil"/> Edit information</a>
</div>
</xpath>
</template>

<template id="portal_my_personal_data" name="My Personal Data">
<t t-call="portal.portal_layout">
<form action="/my/personal_data/update" method="post">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="row o_portal_details">
<div class="col-lg-8">
<div class="row">
<div class="col-lg-12">
<t t-if="success_message">
<div class="alert alert-success" role="alert">
<t t-esc="success_message"/>
</div>
</t>
<t t-if="error_message">
<div class="alert alert-danger" role="alert">
<t t-esc="error_message"/>
</div>
</t>
</div>
<div class="col-lg-6">
<div class="mb-3">
<label class="form-label" for="email">Email</label>
<input type="email" name="email" id="email" class="form-control" t-att-value="employee.private_email"/>
</div>
</div>
<div class="col-lg-6">
<div class="mb-3">
<label class="form-label" for="phone">Phone</label>
<input type="text" name="phone" id="phone" class="form-control" t-att-value="employee.private_phone"/>
</div>
</div>
<div class="col-lg-12">
<hr/>
<h6>Address</h6>
</div>
<div class="col-lg-12">
<div class="mb-3">
<label class="form-label" for="street">Street</label>
<input type="text" name="street" id="street" class="form-control" t-att-value="employee.private_street or ''"/>
</div>
</div>
<div class="col-lg-6">
<div class="mb-3">
<label class="form-label" for="city">City</label>
<input type="text" name="city" id="city" class="form-control" t-att-value="employee.private_city or ''"/>
</div>
</div>
<div class="col-lg-6">
<div class="mb-3">
<label class="form-label" for="zip">Zip</label>
<input type="text" name="zip" id="zip" class="form-control" t-att-value="employee.private_zip or ''"/>
</div>
</div>
<div class="col-lg-6">
<div class="mb-3">
<label class="form-label" for="country_id">Country</label>
<select name="country_id" id="country_id" class="form-control">
<option value="">Country...</option>
<t t-foreach="countries" t-as="c">
<option t-att-value="c.id" t-att-selected="c.id == employee.private_country_id.id">
<t t-esc="c.name"/>
</option>
</t>
</select>
</div>
</div>
<div class="col-lg-6">
<div class="mb-3">
<label class="form-label" for="state_id">State</label>
<select name="state_id" id="state_id" class="form-control">
<option value="">State...</option>
<t t-foreach="states" t-as="s">
<option t-att-value="s.id" t-att-selected="s.id == employee.private_state_id.id">
<t t-esc="s.name"/>
</option>
</t>
</select>
</div>
</div>
</div>
<div class="clearfix"/>
<div class="row">
<div class="col-lg-12">
<button type="submit" class="btn btn-primary float-end">
Confirm
</button>
</div>
</div>
</div>
</div>
</form>
</t>
</template>
</odoo>