Compare commits

...

86 Commits

Author SHA1 Message Date
pranaysaidurga 5e617d3ff8 payroll changes 2026-07-01 17:22:14 +05:30
seshikanth 625bd67064 #fix:HRMSdashboard 2026-06-30 16:17:45 +05:30
seshikanth 3de90ee7ce #fix:Payrollstatement 2026-06-30 12:24:02 +05:30
seshikanth 257e05cc34 #fix:payrolland leave 2026-06-29 12:48:07 +05:30
Bhagya-K 80e90ca88a Merge remote-tracking branch 'origin/feature/share_module' into feature/share_module 2026-06-29 11:50:43 +05:30
Bhagya-K a1320afd5b DEV: Probation Tracking 2026-06-29 11:50:02 +05:30
pranaysaidurga cf0b469b21 recruitment mail issue fix 2026-06-26 17:52:45 +05:30
pranaysaidurga 1b34ae2c5c recruitment bug fixes 2026-06-26 17:26:33 +05:30
pranaysaidurga 08569e0ae0 recruitment fixes 2026-06-25 17:05:21 +05:30
pranaysaidurga 77827a188f payroll fixes 2026-06-25 13:22:45 +05:30
pranaysaidurga aecdbab7c2 helpdesk issue 2026-06-25 13:13:45 +05:30
seshikanth f40d78f5dc #fix:Discplinary few HRMS bugs 2026-06-24 15:33:37 +05:30
seshikanth 3ff97c7c8a #fix: Employee Performance Management Module and few HRMS bugs 2026-06-24 15:23:48 +05:30
seshikanth adc4733e15 #fix: Employee Performance Management Module and few HRMS bugs 2026-06-24 12:20:06 +05:30
Bhagya-K a8570dea30 DEV: Probation Tracking 2026-06-22 15:04:56 +05:30
Bhagya-K 7f8a627a8c DEV: Probation Tracking 2026-06-22 15:02:55 +05:30
Bhagya-K 19669a7a3a Merge remote-tracking branch 'origin/feature/share_module' into feature/share_module 2026-06-22 15:01:22 +05:30
Bhagya-K d448713449 DEV: Probation Tracking 2026-06-22 15:01:12 +05:30
pranaysaidurga a9a9f08ff9 recritment bug fixes 2026-06-19 14:05:09 +05:30
pranaysaidurga 7960d1926b recruitment bug fixes 2026-06-17 15:51:56 +05:30
Bhagya-K e2c8a25c7b DEV: Probation Tracking 2026-06-16 17:05:42 +05:30
Bhagya-K 9ce8c135d0 Merge remote-tracking branch 'origin/feature/share_module' into feature/share_module 2026-06-15 10:59:53 +05:30
Bhagya-K 8877a4ebc4 DEV: Sequence genartion for Employee Code 2026-06-15 10:59:42 +05:30
pranaysaidurga 12ec001b38 business travel expenses 2026-06-12 15:00:28 +05:30
pranaysaidurga 064bd90c58 ALL updates regrading the hrms, ats and pmt 2026-06-12 12:17:49 +05:30
raman 29af1ebf29 Merge pull request 'docker files added' (#3) from feature/odoo18_docker into feature/share_module
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/3
2026-06-10 17:35:22 +05:30
Deepak 41f493840b docker files added 2026-06-10 16:53:14 +05:30
Bhagya-K 54541998c5 Merge remote-tracking branch 'origin/feature/share_module' into feature/share_module 2026-06-10 12:00:05 +05:30
Bhagya-K 68e62956b7 resolved installation issue of grace period module 2026-06-10 11:59:42 +05:30
seshikanth 14e725be4f #fix: Employee Performance Management Module 2026-06-09 13:31:02 +05:30
Bhagya-K adfe801d8e Roster management and grace period changes 2026-06-08 17:01:09 +05:30
Bhagya-K 5c6341d8b7 Merge remote-tracking branch 'origin/feature/share_module' into feature/share_module
# Conflicts:
#	addons_extensions/grace_period/models/__init__.py
2026-06-08 16:33:01 +05:30
Bhagya-K 71f416e8e8 grace Period 2026-06-08 16:30:00 +05:30
pranaysaidurga b5f643fb18 attendance, leaves, weekly timesheets enhancements 2026-06-05 16:42:23 +05:30
karuna 3092032fee enhancements of PMT 2026-06-05 15:16:49 +05:30
karuna f118600ab6 enhancements of PMT 2026-06-04 15:36:32 +05:30
karuna 05bdddc472 enhancements of PMT 2026-06-04 11:06:43 +05:30
pranaysaidurga 582225e11e employee issue 2026-06-04 10:51:17 +05:30
pranaysaidurga 478c1708fb Auto doc get default recruitment 2026-06-04 10:43:54 +05:30
pranaysaidurga 945aedc0b4 Recruitement Dashboards 2026-06-03 14:23:00 +05:30
pranaysaidurga eb17d717dd Project Changes 2026-06-03 10:55:50 +05:30
pranaysaidurga 604d556501 Recruitment changes 2026-06-03 10:48:17 +05:30
karuna 90211776a1 small enhancements 2026-05-28 12:20:45 +05:30
karuna 923304f759 project enhancements 2026-05-26 15:03:48 +05:30
pranaysaidurga 96493be796 Leaves Timesheets Management 2026-05-25 16:38:40 +05:30
pranaysaidurga 5460f6c207 Employee it declaration 2026-05-25 16:10:45 +05:30
pranaysaidurga 4db7e5ade2 Document parser upload 2026-05-20 18:59:16 +05:30
pranaysaidurga f2788e025d bench management system changes 2026-05-19 16:10:05 +05:30
pranaysaidurga 0e51ac85e9 user_ids in project tasks bug 2026-05-19 13:48:45 +05:30
pranaysaidurga 19e5f1db80 Project, recruitment, payroll, bench management changes and updates 2026-05-19 13:36:24 +05:30
karuna c2e33753bb project modifications 2026-05-19 12:08:41 +05:30
pranaysaidurga f6bfd46f2c my tasks issue fix 2026-05-14 12:23:28 +05:30
pranaysaidurga c9746456b8 equipment extended 2026-05-13 11:28:50 +05:30
pranaysaidurga b0ec5ee508 Menu Control Center Functionality change 2026-05-12 11:37:01 +05:30
pranaysaidurga e2d6a8c417 user own estimations 2026-05-07 14:14:41 +05:30
pranaysaidurga 9c33507a45 multi company changes 2026-05-06 18:17:04 +05:30
pranaysaidurga a9a6c683d7 new timeline feature and bug fixes 2026-05-06 17:26:15 +05:30
pranaysaidurga 723dcbe225 portfolio company rules 2026-05-06 11:51:28 +05:30
pranaysaidurga 4139e5fa33 company_id constrain fix 2026-05-06 10:56:13 +05:30
pranaysaidurga ce93d9601c project updates and changes 2026-05-05 11:55:32 +05:30
pranaysaidurga 1b21175e75 accounting kit 2026-05-04 10:56:10 +05:30
pranaysaidurga 74526cc1a2 project task issue fix and changes 2026-04-27 16:14:25 +05:30
pranaysaidurga 6a32ac3f37 project task creation issue 2026-04-27 15:27:27 +05:30
pranaysaidurga fa3833bac3 Project Employee view 2026-04-27 12:37:57 +05:30
pranay 73a27d8921 master selector fix 2026-04-14 11:45:11 +05:30
pranay 26923e20b9 bug fix 2026-04-13 18:21:55 +05:30
karuna ff806505e2 PMT bug fix 2026-04-13 17:56:48 +05:30
karuna b5b276f552 Module Master Selector 2026-03-10 10:45:00 +05:30
Pranay 5d6c2c09aa list view scrolling issue 2026-02-05 12:23:55 +05:30
Pranay 4b85cc0f59 Recruitment changes, Menu Contral Center xml change, attachment preview changes 2026-02-04 11:57:31 +05:30
Pranay ad5967d420 ica web responsive theme 2025-12-24 17:50:22 +05:30
Pranay 53f90a7834 packages list 2025-12-24 17:29:23 +05:30
Pranay 87824199d0 project management system Commit 2025-12-24 12:50:57 +05:30
karuna 92543295d6 adding page visibility based on stages 2025-12-15 18:07:11 +05:30
Pranay 7b6d108ace one2many search widget and project task module updates 2025-12-15 11:25:18 +05:30
Pranay 6f77059f85 PMT, UNIVERSAL ATTACHMENT PREVIEW, MENU CONTROL CENTER INTEGRATION 2025-12-10 10:09:23 +05:30
Pranay c93d208990 Document Preview 2025-12-03 10:19:21 +05:30
Pranay bfd7890cbc Documents preview, binary field widget, menu control center changes 2025-12-03 10:16:19 +05:30
karuna d362ef87aa kudos changes commit 2025-12-03 10:11:32 +05:30
Pranay 20d22c1f04 Project timesheet updates 2025-11-25 16:45:09 +05:30
pranay 44e5ee7e2f ALL MODULE CHANGES 2025-11-20 10:11:59 +05:30
pranay 0fa84c6d43 PMS Updates 2025-11-20 10:06:10 +05:30
karuna 4ce02e58fa Rewards Module for PMS 2025-11-13 12:43:31 +05:30
pranay 66077d1819 Project Task Extended Changes 2025-11-13 12:36:54 +05:30
karuna 2dbdb58127 internal team model update 2025-11-10 10:14:09 +05:30
pranay 5f267e96da PROJECT MODULE AND THEME ADDED IN SHARED MODULES 2025-11-04 11:57:04 +05:30
3394 changed files with 1437168 additions and 1264924 deletions

54
Dockerfile Normal file
View File

@ -0,0 +1,54 @@
FROM python:3.12-bookworm
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# System dependencies
RUN apt-get update && apt-get install -y \
build-essential \
gcc \
g++ \
git \
curl \
npm \
libpq-dev \
libldap2-dev \
libsasl2-dev \
libxml2-dev \
libxslt1-dev \
libjpeg62-turbo-dev \
zlib1g-dev \
libffi-dev \
libssl-dev \
liblcms2-dev \
wkhtmltopdf \
&& rm -rf /var/lib/apt/lists/*
# Less compiler used by Odoo
RUN npm install -g less less-plugin-clean-css
# Create odoo user
RUN useradd -m -d /opt/odoo -U -r -s /bin/bash odoo
WORKDIR /opt/odoo/odoo18
# Copy project
COPY . .
# Upgrade pip tools
RUN pip install --upgrade pip setuptools wheel
# Install requirements
RUN pip install --no-cache-dir -r requirements.txt
# PostgreSQL driver
RUN pip install psycopg2-binary
# Permissions
RUN chown -R odoo:odoo /opt/odoo
USER odoo
EXPOSE 8069
CMD ["python3", "odoo-bin", "-c", "odoo.conf"]

View File

@ -103,7 +103,7 @@ class ImBus(models.Model):
"""Low-level method to send ``notification_type`` and ``message`` to ``target``.
Using ``_bus_send()`` from ``bus.listener.mixin`` is recommended for simplicity and
security.
security.
When using ``_sendone`` directly, ``target`` (if str) should not be guessable by an
attacker.

View File

@ -6,7 +6,7 @@
'version': '1.0',
'category': 'Accounting/Localizations/Point of Sale',
'description': """
This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability, security, storage and archiving of data related to sales to private individuals (B2C).
This add-on brings the technical requirements of the French regulation CGI art. 286, I. 3° bis that stipulates certain criteria concerning the inalterability,security, storage and archiving of data related to sales to private individuals (B2C).
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Install it if you use the Point of Sale app to sell to individuals.

View File

@ -41,7 +41,7 @@ class Survey(http.Controller):
def _check_validity(self, survey_token, answer_token, ensure_token=True, check_partner=True):
""" Check survey is open and can be taken. This does not checks for
security rules, only functional / business rules. It returns a string key
security rules, only functional / business rules. It returns a string key
allowing further manipulation of validity issues
* survey_wrong: survey does not exist;

View File

@ -24,7 +24,7 @@
},
'installable': True,
'data': [
# security.xml first, data.xml need the group to exist (checking it)
#security.xml first, data.xml need the group to exist (checking it)
'security/website_security.xml',
'security/ir.model.access.csv',
'data/image_library.xml',

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from odoo import api, SUPERUSER_ID
from . import models
from . import report
from . import wizard
# TODO: Generate Sequence For each company for Equipment
def pre_init_hook(env):
company_ids = env['res.company'].search([])
for company_id in company_ids:
sequence_id = env['ir.sequence'].search(
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', company_id.id)])
if not sequence_id:
env['ir.sequence'].create({
'name': 'Equipment Company Sequence',
'prefix': company_id.id,
'padding': 5,
'number_increment': 1,
'company_id': company_id.id
})

View File

@ -0,0 +1,25 @@
{
"name": "QR Code on Equipment",
'category': '',
"summary": "Add QR Code on equipment .",
'license': 'LGPL-3',
"price": 00.00,
'description': """
The Equipment Management Module generates unique QR codes for each asset, offering instant details and direct Odoo profile access for seamless management.
""",
"author": "Raman Marikanti",
"depends": ['account','maintenance'],
"external_dependencies": {
'python': ['qrcode']
},
"data": [
'views/maintenance_equipment.xml',
'security/ir.model.access.csv',
'report/custom_qrcode.xml',
'wizard/equipment_label_layout_views.xml',
],
'pre_init_hook': 'pre_init_hook',
"application": True,
"installable": True,
}

View File

@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import maintenance_equipment
from . import res_company

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class MaintenanceEquipment(models.Model):
_inherit = 'maintenance.equipment'
qr_code = fields.Binary("QR Code")
comp_serial_no = fields.Char("Inventory Serial No", tracking=True)
serial_no = fields.Char('Mfg. Serial Number', copy=False)
def action_print_qrcode_layout(self):
action = self.env['ir.actions.act_window']._for_xml_id('aspl_equipment_qrcode_generator.action_open_label_layout_equipment')
action['context'] = {'default_equipment_ids': self.ids}
return action
def generate_serial_no(self):
for equipment_id in self:
if not equipment_id.comp_serial_no:
company_id = equipment_id.company_id.id
sequence_id = self.env['ir.sequence'].search(
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', company_id)])
if sequence_id:
data = sequence_id._next()
equipment_id.write({
'comp_serial_no': data
})

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
from odoo import models, api
class ResCompany(models.Model):
_inherit = 'res.company'
@api.model_create_multi
def create(self, vals):
result = super(ResCompany, self).create(vals)
sequence_id = self.env['ir.sequence'].search(
[('name', '=', 'Equipment Company Sequence'), ('company_id', '=', result.id)])
if not sequence_id:
self.env['ir.sequence'].create({
'name': 'Equipment Company Sequence',
'prefix': result.id,
'padding': 5,
'number_increment': 1,
'company_id': result.id
})
return result

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import custom_qrcode_generator

View File

@ -0,0 +1,188 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="report_equipment_simple_label2x7">
<t t-set="qrcode_size" t-value="'height:14mm'"/>
<t t-set="table_style" t-value="'width:97mm;height:37.1mm;margin:inherit;' + table_style"/>
<td t-att-style="make_invisible and 'visibility:hidden;'" >
<div class="o_label_full" t-att-style="table_style">
<div class="o_label_name">
<strong t-field="equipment.name"/>
</div>
<div class="o_label_data">
<div class="text-center o_label_right_column">
<t t-if="equipment.qr_code">
<div t-field="equipment.qr_code" t-options="{'widget': 'image'}" style="width: 130px;margin-top: -40px;"/>
</t>
</div>
<div class="text-left" style="line-height:normal;word-wrap: break-word;">
<span class="text-nowrap" t-field="equipment.serial_no"/>
<div class="o_label_extra_data">
<span t-field="equipment.comp_serial_no"/>
</div>
<t t-if="equipment.warranty_date">
<strong t-field="equipment.warranty_date"/>
</t>
</div>
<div class="o_label_clear"></div>
</div>
</div>
</td>
</template>
<template id="report_equipment_simple_label4x7">
<t t-set="barcode_size" t-value="'width:80px;'"/>
<t t-set="table_style" t-value="'width:47mm;height:37.1mm;margin:inherit;' + table_style"/>
<td t-att-style="make_invisible and 'visibility:hidden;'" >
<div class="o_label_full" t-att-style="table_style">
<div class="o_label_name">
<strong t-field="equipment.name"/>
</div>
<div class= "text-center o_label_right_column">
<t t-if="equipment.qr_code">
<div t-field="equipment.qr_code" t-options="{'widget': 'image'}" style="width:95px;padding:0px;margin-top:-10px"/>
</t>
</div>
<div class="text-left o_label_left_column" style="line-height:normal;word-wrap: break-word;">
<div class="o_label_data">
<strong t-field="equipment.serial_no"/>
<span t-field="equipment.comp_serial_no"/>
<t t-if="equipment.warranty_date">
<strong t-field="equipment.warranty_date"/>
</t>
</div>
</div>
</div>
</td>
</template>
<template id="report_equipment_simple_label2x5">
<div class="d-flex flex-column" style="
width: 87mm;
height:40mm;
border: 1.5px solid black;
border-radius: 6px;
padding: 6px;
font-family: Arial, Roboto, sans-serif;
font-size: 11px;
margin-bottom: 15px;
">
<!-- Header -->
<div class="text-center fw-bold" style="
font-size: 15px;
border-bottom: 1px solid #ccc;
padding-bottom: 4px;
white-space: nowrap;
">
FTPROTECH - Asset Management Team
</div>
<!-- Main Content -->
<div class="d-flex" style="flex: 1; padding-top: 6px;">
<!-- Left Column -->
<div class="flex-grow-1 pe-2" style="white-space: nowrap;">
<div class="d-flex">
<div class="fw-bold" style="width: 40%;">Asset Tag:</div>
<div style="width: 60%; overflow: visible;"><t style="padding-left:5px;" t-esc="equipment.name"/></div>
</div>
<div class="d-flex">
<div class="fw-bold" style="width: 40%;">Serial Number:</div>
<div style="width: 60%; overflow: visible;"><t style="padding-left:5px;" t-esc="equipment.serial_no"/></div>
</div>
<div class="d-flex">
<div class="fw-bold" style="width: 40%;">Model Number:</div>
<div style="width: 60%; overflow: visible;"><t style="padding-left:5px;" t-esc="equipment.model"/></div>
</div>
</div>
<!-- Right Column - QR Code -->
<div class="d-flex justify-content-end align-items-center" style="width: 30%;">
<t t-if="equipment.name">
<div t-field="equipment.qr_code"
t-options="{'widget': 'image'}"
style="width: 90px; height: 90px; display: inline-block;">
</div>
</t>
</div>
</div>
</div>
</template>
<template id="report_quipmentlabel">
<t t-call="web.html_container">
<t t-if="columns and rows">
<t t-if="columns == 2 and rows == 7">
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label2x7'"/>
</t>
<t t-if="columns == 4 and rows == 7">
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label4x7'"/>
</t>
<t t-if="columns == 2 and rows == 5">
<t t-set="padding_page" t-value="'padding: 14mm 3mm'"/>
<t t-set="report_to_call" t-value="'aspl_equipment_qrcode_generator.report_equipment_simple_label2x5'"/>
</t>
<t t-foreach="range(page_numbers)" t-as="page">
<div class="o_label_sheet" t-att-style="padding_page">
<table class="my-0 table table-sm table-borderless" style="border-spacing: 5mm 3mm;">
<t t-foreach="range(rows)" t-as="row">
<tr>
<t t-foreach="range(columns)" t-as="column">
<t t-if="equipment_data">
<t t-set="current_data" t-value="equipment_data.popitem()"/>
<t t-set="equipment" t-value="current_data[0]"/>
<t t-set="table_style" t-value="'border: 1px solid black;'"/>
<t t-call="{{report_to_call}}"/>
</t>
</t>
</tr>
</t>
</table>
</div>
</t>
</t>
</t>
</template>
<template id="maintenance_quip">
<t t-call="web.basic_layout">
<div class="page">
<t t-call="aspl_equipment_qrcode_generator.report_quipmentlabel">
<t t-set="products" t-value="products"/>
</t>
</div>
</t>
</template>
<record id="paperformat_label_sheet_qrcode" model="report.paperformat">
<field name="name">A4 Label Sheet</field>
<field name="default" eval="True"/>
<field name="format">A4</field>
<field name="page_height">0</field>
<field name="page_width">0</field>
<field name="orientation">Portrait</field>
<field name="margin_top">0</field>
<field name="margin_bottom">0</field>
<field name="margin_left">0</field>
<field name="margin_right">0</field>
<field name="disable_shrinking" eval="True"/>
<field name="dpi">96</field>
</record>
<record id="report_equipment_label" model="ir.actions.report">
<field name="name">Equipment QR-code (PDF)</field>
<field name="model">maintenance.equipment</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">aspl_equipment_qrcode_generator.maintenance_quip</field>
<field name="report_file">aspl_equipment_qrcode_generator.maintenance_quip</field>
<field name="paperformat_id" ref="aspl_equipment_qrcode_generator.paperformat_label_sheet_qrcode"/>
<field name="print_report_name">'Products Labels - %s' % (object.name)</field>
<field name="binding_model_id" eval="False"/>
<field name="binding_type">report</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import math
from io import BytesIO
import qrcode
from odoo import models
def generate_qr_code(value):
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=20,
border=4,
)
qr.add_data(value)
qr.make(fit=True)
img = qr.make_image()
temp = BytesIO()
img.save(temp, format="PNG")
qr_img = base64.b64encode(temp.getvalue())
return qr_img
def _prepare_data(env, data):
equipment_label_layout_id = env['equipment.label.layout'].browse(data['equipment_label_layout_id'])
equipment_dict = {}
equipment_ids = equipment_label_layout_id.equipment_ids
for equipment in equipment_ids:
if not equipment.name:
continue
equipment_dict[equipment] = 1
combine_equipment_detail = ""
# Generate Equipment Redirect LInk
url = env['ir.config_parameter'].sudo().get_param('web.base.url')
menuId = env.ref('maintenance.menu_equipment_form').sudo().id
actionId = env.ref('maintenance.hr_equipment_action').sudo().id
equipment_link = url + '/web#id=' + str(equipment.id) + '&menu_id=' + str(menuId) + '&action=' + str(
actionId) + '&model=maintenance.equipment&view_type=form'
# Prepare main Equipment Detail
main_equipment_detail = ""
main_equipment_detail = main_equipment_detail.join(
"Name: " + str(equipment.name) + "\n" +
"Model: " + str(equipment.model) + "\n" +
"Mfg serial no: " + str(equipment.serial_no) + "\n"
"Warranty Exp. Date: " +str(equipment.warranty_date) + "\n"
"Category: " +str(equipment.category_id.name)+ "\n"
"Contact No: "+str(equipment.technician_user_id.phone) +"\n"
"Contact Email: "+str(equipment.technician_user_id.login)
)
# main_equipment_detail = equipment_link + '\n' + '\n' + main_equipment_detail
# Prepare Child Equipment Detail
combine_equipment_detail = main_equipment_detail
combine_equipment_detail += '\n' + '\n' + equipment_link
# Generate Qr Code depends on Details
qr_image = generate_qr_code(combine_equipment_detail)
equipment.write({
'qr_code': qr_image
})
env.cr.commit()
page_numbers = (len(equipment_ids) - 1) // (equipment_label_layout_id.rows * equipment_label_layout_id.columns) + 1
dict_equipment = {
'rows': equipment_label_layout_id.rows,
'columns': equipment_label_layout_id.columns,
'page_numbers': page_numbers,
'equipment_data': equipment_dict
}
return dict_equipment
class ReportProductTemplateLabel(models.AbstractModel):
_name = 'report.aspl_equipment_qrcode_generator.maintenance_quip'
_description = 'Equipment QR-code Report'
def _get_report_values(self, docids, data):
return _prepare_data(self.env, data)

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_equipment_label_layout,access.equipment_label_layout,model_equipment_label_layout,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_equipment_label_layout access.equipment_label_layout model_equipment_label_layout 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_extend_equipment_tree" model="ir.ui.view">
<field name="name">maintenance.equipment.tree.inherit</field>
<field name="model">maintenance.equipment</field>
<field name="priority" eval="16"/>
<field name="inherit_id" ref="maintenance.hr_equipment_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//list" position="inside">
<header>
<button string="Print QR-code" type="object" name="action_print_qrcode_layout"/>
</header>
</xpath>
</field>
</record>
<record id="view_extend_equipment_form" model="ir.ui.view">
<field name="name">maintenance.equipment.form.inherit</field>
<field name="model">maintenance.equipment</field>
<field name="priority" eval="16"/>
<field name="inherit_id" ref="maintenance.hr_equipment_view_form"/>
<field name="arch" type="xml">
<xpath expr="//page[@name='product_information']" position="inside">
<group>
<field name="comp_serial_no"/>
</group>
</xpath>
<xpath expr="//sheet" position="before">
<header>
<button string="Generate Serial Number" type="object" name="generate_serial_no" class="oe_highlight"/>
</header>
</xpath>
</field>
</record>
<record id="generate_serial_no_action" model="ir.actions.server">
<field name="name">Generate Serial Number</field>
<field name="model_id" ref="model_maintenance_equipment"/>
<field name="binding_model_id" ref="model_maintenance_equipment"/>
<field name="binding_view_types">list</field>
<field name="state">code</field>
<field name="code">action = records.generate_serial_no()</field>
</record>
<record id="generate_qrcode_no_action" model="ir.actions.server">
<field name="name">Print QR-Code</field>
<field name="model_id" ref="model_maintenance_equipment"/>
<field name="binding_model_id" ref="model_maintenance_equipment"/>
<field name="binding_view_types">form</field>
<field name="state">code</field>
<field name="code">action = records.action_print_qrcode_layout()</field>
</record>
</odoo>

View File

@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import equipment_label_layout

View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
class EquipmentLabelLayout(models.TransientModel):
_name = 'equipment.label.layout'
_description = 'Choose the sheet layout to print the labels'
print_format = fields.Selection([
('2x5', '2 x 5'),
('2x7', '2 x 7'),
('4x7', '4 x 7')], string="Format", default='2x5', required=True)
equipment_ids = fields.Many2many('maintenance.equipment')
rows = fields.Integer(compute='_compute_dimensions')
columns = fields.Integer(compute='_compute_dimensions')
@api.depends('print_format')
def _compute_dimensions(self):
for wizard in self:
if 'x' in wizard.print_format:
columns, rows = wizard.print_format.split('x')[:2]
wizard.columns = int(columns)
wizard.rows = int(rows)
else:
wizard.columns, wizard.rows = 1, 1
def process_label(self):
xml_id = 'aspl_equipment_qrcode_generator.report_equipment_label'
data = {
'equipment_label_layout_id':self.id
}
return self.env.ref(xml_id).report_action(None, data=data)

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="equipment_label_layout_form" model="ir.ui.view">
<field name="name">equipment.label.layout.form</field>
<field name="model">equipment.label.layout</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<form>
<group>
<group>
<field name="print_format" widget="radio"/>
</group>
</group>
<footer>
<button name="process_label" string="Confirm" type="object" class="btn-primary"/>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_open_label_layout_equipment" model="ir.actions.act_window">
<field name="name">Choose Labels Layout</field>
<field name="res_model">equipment.label.layout</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'form', 'view_id': ref('equipment_label_layout_form')})]" />
<field name="target">new</field>
</record>
</odoo>

View File

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

View File

@ -0,0 +1,24 @@
{
'name': 'Bench Management',
'version': '1.0',
'category': 'Human Resources',
'summary': 'Bench Management System',
'author': 'Team Srivyn',
'depends': [
'hr',
'project',
'hr_timesheet',
'project_task_timesheet_extended',
'hr_employee_extended'
],
'data': [
'security/ir.model.access.csv',
'data/sync_team_lines.xml',
'views/project.xml',
'views/bench_management_view.xml',
],
'installable': True,
'application': True,
'auto_install': False,
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<function model="project.project" name="_sync_all_team_lines_from_members"/>
</odoo>

View File

@ -0,0 +1,2 @@
from . import project
from . import bench_management

View File

@ -0,0 +1,174 @@
from odoo import models, fields, tools, api
class BenchManagementLine(models.Model):
_name = "bench.management.line"
_description = "Employee Availability (Bench)"
_auto = False
_rec_name = 'employee_id'
employee_id = fields.Many2one("hr.employee", readonly=True)
job_id = fields.Many2one("hr.job", readonly=True)
company_id = fields.Many2one("res.company", related="employee_id.company_id")
project_line_ids = fields.Many2many(
'project.team.line',
compute='_compute_bench_details',
string='Project Assignments',
readonly=True,
)
limited_project_line_ids = fields.Many2many(
compute='_compute_bench_details',
comodel_name='project.team.line',
string='Kanban Projects',
readonly=True,
)
project_names_tooltip = fields.Text(
string="Project Names",
compute='_compute_bench_details',
readonly=True,
)
project_count = fields.Integer(
string="Project Count",
compute='_compute_bench_details',
readonly=True,
)
active_project_count = fields.Integer(
string="Active Projects",
compute='_compute_bench_details',
readonly=True,
)
future_project_count = fields.Integer(
string="Upcoming Projects",
compute='_compute_bench_details',
readonly=True,
)
completed_project_count = fields.Integer(
string="Completed Projects",
compute='_compute_bench_details',
readonly=True,
)
status = fields.Selection([
("bench", "Bench"),
("partial", "Partial"),
("full", "Full"),
], readonly=True)
def _get_line_availability_status(self, line, today):
return line.status or 'not_started'
def _compute_bench_details(self):
project_team_line = self.env['project.team.line'].sudo()
today = fields.Date.context_today(self)
for rec in self:
project_lines = project_team_line.search(
[('employee_id', '=', rec.employee_id.id)],
order='start_date desc, id desc'
)
active_lines = project_lines.filtered(
lambda line: rec._get_line_availability_status(line, today) == 'in_progress'
)
future_lines = project_lines.filtered(
lambda line: rec._get_line_availability_status(line, today) == 'not_started'
)
completed_lines = project_lines.filtered(
lambda line: rec._get_line_availability_status(line, today) == 'done'
)
project_records = project_lines.mapped('project_id')
if active_lines:
bench_status = 'full'
elif future_lines:
bench_status = 'partial'
else:
bench_status = 'bench'
rec.project_line_ids = project_lines
rec.limited_project_line_ids = project_lines[:3]
rec.project_count = len(project_records)
rec.active_project_count = len(active_lines)
rec.future_project_count = len(future_lines)
rec.completed_project_count = len(completed_lines)
rec.project_names_tooltip = '\n'.join(
f"{line.project_id.display_name or 'No Project'} - {dict(line._fields['status'].selection).get(rec._get_line_availability_status(line, today), 'N/A')}"
for line in project_lines
) or ''
def init(self):
tools.drop_view_if_exists(self.env.cr, self._table)
self.env.cr.execute("""
CREATE OR REPLACE VIEW bench_management_line AS (
SELECT
he.id AS id,
he.id AS employee_id,
he.job_id AS job_id,
CASE
WHEN EXISTS (
SELECT 1
FROM project_team_line tpl
WHERE tpl.employee_id = he.id
AND tpl.status = 'in_progress'
) THEN 'full'
WHEN EXISTS (
SELECT 1
FROM project_team_line tpl
WHERE tpl.employee_id = he.id
AND tpl.status = 'not_started'
) THEN 'partial'
ELSE 'bench'
END AS status
FROM hr_employee he
)
""")
class ProjectTeamLine(models.Model):
_inherit = 'project.team.line'
line_status_color = fields.Integer(
compute='_compute_line_status_color',
string='Status Color',
readonly=True,
)
@api.depends('status')
def _compute_line_status_color(self):
color_map = {
'not_started': 8,
'in_progress': 2,
'done': 10,
}
for line in self:
line.line_status_color = color_map.get(line.status, 0)
def name_get(self):
result = []
for rec in self:
name = rec.project_id.display_name or 'No Project'
result.append((rec.id, name))
return result
def _sync_project_members(self):
if self.env.context.get('skip_project_team_member_sync'):
return True
self.mapped('project_id')._sync_members_from_team_lines()
return True
@api.model_create_multi
def create(self, vals_list):
records = super().create(vals_list)
records._sync_project_members()
return records
def write(self, vals):
projects = self.mapped('project_id')
res = super().write(vals)
(projects | self.mapped('project_id'))._sync_members_from_team_lines()
return res
def unlink(self):
projects = self.mapped('project_id')
res = super().unlink()
projects._sync_members_from_team_lines()
return res

View File

@ -0,0 +1,223 @@
from odoo import Command, api, fields, models, _
from odoo.exceptions import ValidationError
class ProjectProject(models.Model):
_inherit = 'project.project'
team_line_ids = fields.One2many(
'project.team.line',
'project_id',
string="Team Details"
)
can_manage_team_lines = fields.Boolean(
compute='_compute_can_manage_team_lines',
string='Can Manage Team Lines'
)
@api.depends('user_id', 'project_lead')
def _compute_can_manage_team_lines(self):
current_user = self.env.user
for project in self:
project.can_manage_team_lines = bool(
self.env.is_superuser()
or project.user_id == current_user
or ('project_lead' in project._fields and project.project_lead == current_user) or (current_user.has_group("project.group_project_manager"))
)
@api.onchange('team_line_ids')
def _onchange_team_line_ids(self):
for project in self:
users = project.team_line_ids.mapped('user_id')
project.members_ids = [(6, 0, users.ids)]
def _sync_members_from_team_lines(self):
if self.env.context.get('skip_project_team_member_sync'):
return
for project in self:
users = project.team_line_ids.mapped('user_id')
if set(project.members_ids.ids) != set(users.ids):
project.with_context(skip_project_team_member_sync=True).sudo().write({
'members_ids': [Command.set(users.ids)],
})
def _sync_team_lines_from_members(self):
if self.env.context.get('skip_project_team_member_sync'):
return
TeamLine = self.env['project.team.line'].sudo().with_context(skip_project_team_member_sync=True)
for project in self.sudo():
member_ids = set(project.members_ids.ids)
kept_user_ids = set()
lines_to_remove = self.env['project.team.line'].sudo()
for line in project.team_line_ids.sorted('id'):
user_id = line.user_id.id
if not user_id or user_id not in member_ids or user_id in kept_user_ids:
lines_to_remove |= line
else:
kept_user_ids.add(user_id)
if lines_to_remove:
lines_to_remove.with_context(skip_project_team_member_sync=True).unlink()
for user_id in member_ids - kept_user_ids:
TeamLine.create({
'project_id': project.id,
'user_id': user_id,
})
@api.model
def _sync_all_team_lines_from_members(self):
self.search([])._sync_team_lines_from_members()
return True
@api.model_create_multi
def create(self, vals_list):
projects = super().create(vals_list)
for project, vals in zip(projects, vals_list):
if 'team_line_ids' in vals:
project._sync_members_from_team_lines()
elif 'members_ids' in vals:
project._sync_team_lines_from_members()
return projects
def write(self, vals):
res = super().write(vals)
if 'team_line_ids' in vals:
self._sync_members_from_team_lines()
elif 'members_ids' in vals:
self._sync_team_lines_from_members()
return res
class ProjectTeamLine(models.Model):
_name = 'project.team.line'
_description = 'Project Team Line'
_rec_name = 'project_id'
project_id = fields.Many2one('project.project', ondelete='cascade')
user_id = fields.Many2one('res.users')
employee_id = fields.Many2one(
'hr.employee',
compute="_compute_employee",
store=True
)
job_id = fields.Many2one(
'hr.job',
related='employee_id.job_id',
store=True
)
start_date = fields.Date()
end_date = fields.Date()
status = fields.Selection([
('not_started', 'Not Started'),
('in_progress', 'In Progress'),
('done', 'Completed')
], compute='_compute_status', inverse='_inverse_status', store=True, readonly=False)
can_edit_assignment = fields.Boolean(
compute='_compute_can_edit_assignment',
string='Can Edit Assignment'
)
# ------------------------
# COMPUTE EMPLOYEE
# ------------------------
@api.depends('user_id')
def _compute_employee(self):
for rec in self:
rec.employee_id = self.env['hr.employee'].search([
('user_id', '=', rec.user_id.id)
], limit=1)
@api.depends('start_date', 'end_date')
def _compute_status(self):
today = fields.Date.context_today(self)
for rec in self:
if rec.end_date and rec.end_date < today:
rec.status = 'done'
elif rec.start_date and rec.start_date > today:
rec.status = 'not_started'
else:
rec.status = 'in_progress'
@api.depends('project_id.user_id', 'project_id.project_lead')
def _compute_can_edit_assignment(self):
current_user = self.env.user
for rec in self:
project = rec.project_id
rec.can_edit_assignment = bool(
self.env.is_superuser()
or (project and project.user_id == current_user)
or (project and 'project_lead' in project._fields and project.project_lead == current_user) or (current_user.has_group("project.group_project_manager"))
)
def _inverse_status(self):
# Allow manual edits to the stored computed field.
# When start/end dates change later, compute will refresh it again.
return True
def _check_manager_access(self):
if self.env.is_superuser():
return
unauthorized = self.filtered(lambda rec: not rec.can_edit_assignment)
if unauthorized:
raise ValidationError(_("Only the related project manager can update team assignment dates or status."))
# ------------------------
# SYNC BENCH
# ------------------------
def _sync_bench(self):
# Bench data is read live from SQL view / computed fields,
# so there is no separate sync model to refresh here.
return True
# ------------------------
# CREATE
# ------------------------
@api.model_create_multi
def create(self, vals_list):
if not self.env.is_superuser():
for vals in vals_list:
project_id = vals.get('project_id')
if project_id:
project = self.env['project.project'].browse(project_id)
if not (
project.user_id == self.env.user
or ('project_lead' in project._fields and project.project_lead == self.env.user)
):
raise ValidationError(_("Only the related project manager can add team assignments."))
records = super().create(vals_list)
records._sync_bench()
records.mapped('project_id')._sync_members_from_team_lines()
return records
# ------------------------
# WRITE
# ------------------------
def write(self, vals):
if any(key in vals for key in ('status', 'start_date', 'end_date', 'user_id', 'project_id')):
self._check_manager_access()
projects = self.mapped('project_id')
res = super().write(vals)
self._sync_bench()
if any(key in vals for key in ('user_id', 'project_id')):
(projects | self.mapped('project_id'))._sync_members_from_team_lines()
return res
# ------------------------
# UNLINK
# ------------------------
def unlink(self):
projects = self.mapped('project_id')
self._check_manager_access()
res = super().unlink()
self._sync_bench()
projects._sync_members_from_team_lines()
return res

View File

@ -0,0 +1,3 @@
id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink
access_project_team_line,project.team.line,model_project_team_line,,1,1,1,1
access_bench_management_line,bench.management.line,model_bench_management_line,,1,1,1,1
1 id name model_id:id group_id perm_read perm_write perm_create perm_unlink
2 access_project_team_line project.team.line model_project_team_line 1 1 1 1
3 access_bench_management_line bench.management.line model_bench_management_line 1 1 1 1

View File

@ -0,0 +1,245 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_bench_management_tree" model="ir.ui.view">
<field name="name">bench.management.line.list</field>
<field name="model">bench.management.line</field>
<field name="arch" type="xml">
<list create="0" string="Bench Management">
<field name="employee_id"/>
<field name="job_id"/>
<field name="status"/>
<field name="limited_project_line_ids" widget="many2many_tags" options="{'color_field': 'line_status_color'}"/>
<field name="active_project_count"/>
<field name="future_project_count"/>
<field name="completed_project_count"/>
</list>
</field>
</record>
<record id="view_bench_management_search" model="ir.ui.view">
<field name="name">bench.management.line.search</field>
<field name="model">bench.management.line</field>
<field name="arch" type="xml">
<search string="Bench Management">
<field name="employee_id"/>
<field name="job_id"/>
<field name="status"/>
<filter name="filter_bench" string="Bench" domain="[('status', '=', 'bench')]"/>
<filter name="filter_partial" string="Partially Available" domain="[('status', '=', 'partial')]"/>
<filter name="filter_full" string="Fully Allocated" domain="[('status', '=', 'full')]"/>
<group expand="0" string="Group By">
<filter name="group_by_status" string="Status" context="{'group_by': 'status'}"/>
<filter name="group_by_job" string="Job Position" context="{'group_by': 'job_id'}"/>
</group>
</search>
</field>
</record>
<record id="view_bench_management_form" model="ir.ui.view">
<field name="name">bench.management.line.form</field>
<field name="model">bench.management.line</field>
<field name="arch" type="xml">
<form create="0" string="Bench Management">
<sheet>
<group>
<field name="employee_id"/>
<field name="job_id"/>
<field name="status"/>
</group>
<group string="Project Information">
<field name="project_line_ids" nolabel="1" readonly="0">
<list create="0" delete="0" editable="bottom">
<field name="project_id" readonly="1"/>
<field name="can_edit_assignment" column_invisible="1"/>
<field name="status" readonly="not can_edit_assignment"/>
<field name="start_date" readonly="not can_edit_assignment"/>
<field name="end_date" readonly="not can_edit_assignment"/>
<field name="job_id" optional="hide" readonly="1"/>
</list>
</field>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_bench_management_kanban" model="ir.ui.view">
<field name="name">bench.management.line.kanban</field>
<field name="model">bench.management.line</field>
<field name="arch" type="xml">
<kanban create="0" class="o_kanban_mobile">
<field name="employee_id"/>
<field name="job_id"/>
<field name="status"/>
<field name="limited_project_line_ids"/>
<field name="project_count"/>
<field name="active_project_count"/>
<field name="future_project_count"/>
<field name="completed_project_count"/>
<field name="project_names_tooltip"/>
<templates>
<t t-name="kanban-box">
<div class="oe_kanban_card oe_kanban_global_click"
style="border-radius:16px;border:1px solid #dbe4ee;background:linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);padding:16px;min-height:260px;box-shadow:0 8px 24px rgba(15, 23, 42, 0.06);">
<!-- Header -->
<div class="d-flex align-items-center mb-3">
<img t-att-src="'/web/image/hr.employee/' + record.employee_id.raw_value + '/avatar_128'"
style="
width:42px;
height:42px;
border-radius:50%;
object-fit:cover;
margin-right:10px;
"/>
<div>
<div style="
font-size:16px;
font-weight:600;
color:#1f2937;
">
<field name="employee_id"/>
</div>
<div style="
font-size:12px;
color:#6b7280;
">
<field name="job_id"/>
</div>
</div>
</div>
<!-- Status -->
<div class="mb-3 d-flex align-items-center justify-content-between">
<t t-if="record.status.raw_value == 'bench'">
<span style="
background:#e2e8f0;
color:#334155;
padding:5px 10px;
border-radius:20px;
font-size:11px;
font-weight:600;
">
Bench
</span>
</t>
<t t-if="record.status.raw_value == 'partial'">
<span style="
background:#fef3c7;
color:#92400e;
padding:5px 10px;
border-radius:20px;
font-size:11px;
font-weight:600;
">
Partially Available
</span>
</t>
<t t-if="record.status.raw_value == 'full'">
<span style="
background:#dcfce7;
color:#166534;
padding:5px 10px;
border-radius:20px;
font-size:11px;
font-weight:600;
">
Fully Allocated
</span>
</t>
<div style="font-size:11px;color:#64748b;">
<t t-out="record.active_project_count.raw_value"/> Active
</div>
</div>
<div class="d-flex gap-2 mb-3" style="gap:8px;">
<div style="flex:1;border:1px solid #e2e8f0;border-radius:12px;padding:8px 10px;background:#ffffff;">
<div style="font-size:10px;color:#64748b;text-transform:uppercase;">Current</div>
<div style="font-size:18px;font-weight:700;color:#0f172a;">
<t t-out="record.active_project_count.raw_value"/>
</div>
</div>
<div style="flex:1;border:1px solid #e2e8f0;border-radius:12px;padding:8px 10px;background:#ffffff;">
<div style="font-size:10px;color:#64748b;text-transform:uppercase;">Upcoming</div>
<div style="font-size:18px;font-weight:700;color:#0f172a;">
<t t-out="record.future_project_count.raw_value"/>
</div>
</div>
<div style="flex:1;border:1px solid #e2e8f0;border-radius:12px;padding:8px 10px;background:#ffffff;">
<div style="font-size:10px;color:#64748b;text-transform:uppercase;">Completed</div>
<div style="font-size:18px;font-weight:700;color:#0f172a;">
<t t-out="record.completed_project_count.raw_value"/>
</div>
</div>
</div>
<!-- Project Info -->
<div t-if="record.project_count.raw_value"
style="border-top:1px solid #f3f4f6;padding-top:12px;">
<div style="
font-size:13px;
font-weight:600;
color:#374151;
margin-bottom:8px;
">
Projects
</div>
<div t-att-title="record.project_names_tooltip.raw_value"
style="max-height:48px;overflow:hidden;">
<field name="limited_project_line_ids"
widget="many2many_tags"
options="{'no_create': True, 'no_create_edit': True, 'no_open': True, 'color_field': 'line_status_color'}"/>
</div>
<t t-if="record.project_count.raw_value &gt; 3">
<div t-att-title="record.project_names_tooltip.raw_value"
style="margin-top:8px;font-size:11px;color:#2563eb;font-weight:600;">
+ <t t-out="record.project_count.raw_value - 3"/> more
</div>
</t>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="action_bench_management" model="ir.actions.act_window">
<field name="name">Bench Management</field>
<field name="res_model">bench.management.line</field>
<field name="view_mode">kanban,list,form</field>
<field name="domain">[('company_id', 'in', allowed_company_ids)]</field>
<field name="search_view_id" ref="view_bench_management_search"/>
</record>
<menuitem id="menu_bench_management"
name="Employee Bench"
parent="hr.menu_hr_root"
groups="hr.group_hr_manager"
action="action_bench_management"
sequence="3"/>
</odoo>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="project_project_inherit_form_view2_inherit" model="ir.ui.view">
<field name="name">project.project.inherit.form.view.inherit</field>
<field name="model">project.project</field>
<field name="inherit_id" ref="project_task_timesheet_extended.project_project_inherit_form_view2"/>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<field name="can_manage_team_lines" invisible="1"/>
</xpath>
<xpath expr="//page[@name='team']/group[field[@name='members_ids']]" position="after">
<group string="Project Team Details">
<field name="team_line_ids" nolabel="1" readonly="not can_manage_team_lines">
<list editable="bottom">
<field name="user_id" string="Employee Name"/>
<field name="employee_id" string="Employee" readonly="1" column_invisible="1" invisible="1"/>
<field name="job_id" string="Job Position"/>
<field name="project_id" column_invisible="1"/>
<field name="can_edit_assignment" column_invisible="1"/>
<field name="start_date" string="Start Date" readonly="not can_edit_assignment"/>
<field name="end_date" string="End date" readonly="not can_edit_assignment"/>
<field name="status" string="Status" readonly="not can_edit_assignment"/>
</list>
</field>
</group>
</xpath>
</field>
</record>
</odoo>

View File

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

View File

@ -0,0 +1,38 @@
{
'name': 'Business Travel & Expense Management',
'version': '1.0',
'summary': 'Enterprise Business Travel & Expense Management',
'description': """
Business Travel (Trips) & Expense Management Module.
- Pre-approved Trips
- Trip lifecycle management
- Expense tracking per Trip
- Manager & Finance approvals
- Reimbursement workflow
""",
'category': 'Human Resources',
'author': 'Karuna',
'depends': ['base', 'hr'],
'data': [
'security/travel_groups.xml',
'security/travel_trip_rules.xml',
'security/ir.model.access.csv',
# 'data/users.xml',
'data/trip_sequence.xml',
'wizard/trip_reject_wizard_view.xml',
'views/hr_job_view.xml', # 👈 hr extension BEFORE menus
'views/travel_trip_views.xml',
'views/travel_city_category_views.xml',
'views/travel_group_view.xml',
'views/travel_stay_policy_view.xml', # 👈 ADD HERE
'views/travel_daily_allowance_view.xml',
'views/travel_mode_policy_view.xml',
'views/travel_expense_views.xml',
'views/travel_activity_views.xml',
'views/travel_menu.xml',
],
'images': ['static/description/banner.png'],
'installable': True,
'application': True,
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="seq_travel_trip" model="ir.sequence">
<field name="name">Travel Trip</field>
<field name="code">travel.trip</field>
<field name="prefix">TRIP/%(year)s/</field>
<field name="padding">4</field>
</record>
</odoo>

View File

@ -0,0 +1,9 @@
<!--<odoo>-->
<!-- <record id="user_travel_finance" model="res.users">-->
<!-- <field name="name">Finance User</field>-->
<!-- <field name="login">finance@test.com</field>-->
<!-- <field name="email">finance@test.com</field>-->
<!-- &lt;!&ndash; Attach Travel - Finance group &ndash;&gt;-->
<!-- <field name="groups_id" eval="[(4, ref('business_travel_expense_management.group_travel_finance'))]"/>-->
<!-- </record>-->

View File

@ -0,0 +1,9 @@
from . import travel_trip
from . import travel_expense
from . import travel_activity
from . import travel_city_category
from . import travel_group
from . import hr_job
from . import travel_stay_policy
from . import travel_daily_allowance
from . import travel_mode_policy

View File

@ -0,0 +1,28 @@
from odoo import models, fields, api
class HrJob(models.Model):
_inherit = 'hr.job'
designation_level = fields.Selection([
('a', 'Level A'),
('b', 'Level B'),
('c', 'Level C'),
], string="Designation Level")
travel_group_id = fields.Many2one(
'travel.group',
compute='_compute_travel_group',
store=True,
readonly=True
)
@api.depends('designation_level')
def _compute_travel_group(self):
for rec in self:
if rec.designation_level:
group = self.env['travel.group'].search([
('level_code', '=', rec.designation_level)
], limit=1)
rec.travel_group_id = group.id
else:
rec.travel_group_id = False

View File

@ -0,0 +1,114 @@
from odoo import models, fields, api
class TravelActivity(models.Model):
_name = 'travel.activity'
_description = 'Travel Activity'
_order = 'sequence, id'
# --------------------
# BASIC
# --------------------
sequence = fields.Integer(default=10)
name = fields.Char(string="Activity Title", required=True)
trip_id = fields.Many2one(
'travel.trip',
required=True,
ondelete='cascade'
)
activity_type = fields.Selection([
('travel', 'Travel'),
('stay', 'Stay'),
('meeting', 'Meeting'),
('local', 'Local Travel'),
], required=True)
start_datetime = fields.Datetime("Start Time")
end_datetime = fields.Datetime("End Time")
# --------------------
# COMPUTED GROUP (VERY IMPORTANT)
# --------------------
travel_group_id = fields.Many2one(
'travel.group',
string="Travel Group",
compute="_compute_travel_group",
store=True
)
@api.depends('trip_id')
def _compute_travel_group(self):
for rec in self:
rec.travel_group_id = rec.trip_id.travel_group_id
# --------------------
# TRAVEL MODE (FILTERED BY GROUP)
# --------------------
travel_mode_policy_id = fields.Many2one(
'travel.mode.policy',
string="Travel Mode",
domain="""
[
('travel_group_id', '=', travel_group_id),
('mode_type', '=', activity_type),
('active', '=', True)
]
"""
)
from_location = fields.Char()
to_location = fields.Char()
travel_details = fields.Char()
# --------------------
# OTHER FIELDS
# --------------------
stay_type = fields.Selection([
('hotel', 'Hotel'),
('guest', 'Guest House'),
])
hotel_name = fields.Char()
city = fields.Char()
# city_category_id = fields.Many2one(
# 'travel.city.category',
# string="City Category"
# )
checkin = fields.Datetime()
checkout = fields.Datetime()
meeting_title = fields.Char()
meeting_location = fields.Char()
notes = fields.Text()
local_travel_mode = fields.Selection([
('cab', 'Cab'),
('own', 'Own Vehicle'),
], string="Local Travel Mode")
attachment_ids = fields.Many2many(
'ir.attachment',
'travel_activity_attachment_rel',
'activity_id',
'attachment_id',
string="Documents"
)
expense_ids = fields.One2many(
'travel.expense',
'activity_id',
string="Expenses"
)
total_amount = fields.Float(
string='Activity Total',
compute='_compute_total_amount',
store=True
)
@api.depends('expense_ids.amount')
def _compute_total_amount(self):
for rec in self:
rec.total_amount = sum(rec.expense_ids.mapped('amount'))

View File

@ -0,0 +1,17 @@
from odoo import models, fields
class TravelCityCategory(models.Model):
_name = 'travel.city.category'
_description = 'Travel City Category'
name = fields.Char(
string="City Category",
required=True
)
code = fields.Char(
string="Code",
help="Short code like AP_TG_HYD, AP_TG_OTHER, ROI"
)
active = fields.Boolean(default=True)

View File

@ -0,0 +1,25 @@
from odoo import models, fields
class TravelDailyAllowance(models.Model):
_name = 'travel.daily.allowance'
_description = 'Daily Allowance Policy'
_rec_name = 'travel_group_id'
travel_group_id = fields.Many2one(
'travel.group',
string="Travel Group",
required=True
)
city_category_id = fields.Many2one(
'travel.city.category',
string="City Category",
required=True
)
amount = fields.Float(string="Allowance Amount")
actuals_allowed = fields.Boolean(string="Actuals Allowed")
active = fields.Boolean(default=True)

View File

@ -0,0 +1,112 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
from odoo.exceptions import ValidationError
class TravelExpense(models.Model):
_name = 'travel.expense'
_description = 'Travel Expense'
_order = 'expense_date desc, id desc'
name = fields.Char(string="Expense Description", required=True)
expense_date = fields.Date(default=fields.Date.today)
amount = fields.Monetary(required=True)
activity_id = fields.Many2one(
'travel.activity',
string="Activity",
required=True,
ondelete='cascade'
)
receipt = fields.Binary()
currency_id = fields.Many2one(
'res.currency',
default=lambda self: self.env.company.currency_id
)
state = fields.Selection([
('draft', 'Draft'),
('submitted', 'Submitted'),
('approved', 'Approved'),
('rejected', 'Rejected'),
], default='draft')
# ---------------- Actions ----------------
@api.depends('expense_ids.amount')
def _compute_total_amount(self):
for rec in self:
rec.total_amount = sum(rec.expense_ids.mapped('amount'))
@api.onchange('expense_ids')
def _onchange_expense_ids(self):
self.total_amount = sum(self.expense_ids.mapped('amount'))
def action_submit(self):
for rec in self:
if rec.state != 'draft':
raise UserError("Only Draft expenses can be submitted.")
rec.state = 'submitted'
rec.message_post(body="🟡 Expense submitted.")
def action_approve(self):
for rec in self:
if rec.state != 'submitted':
raise UserError("Only Submitted expenses can be approved.")
manager_user = rec.activity_id.trip_id.manager_id.sudo().user_id
if manager_user != self.env.user:
raise UserError("Only the reporting manager can approve.")
rec.state = 'approved'
rec.message_post(body="🟢 Expense approved.")
def action_mark_reimbursed(self):
for rec in self:
if rec.state != 'approved':
raise UserError("Only Approved expenses can be reimbursed.")
if not self.env.user.has_group(
'business_travel_expense_management.group_travel_finance'
):
raise UserError("Only Finance can reimburse.")
rec.state = 'reimbursed'
rec.message_post(body="💰 Expense reimbursed.")
@api.constrains('amount', 'activity_id')
def _check_stay_policy(self):
for record in self:
activity = record.activity_id
if not activity or activity.activity_type != 'stay':
continue
trip = activity.trip_id
group = trip.travel_group_id
city_category = trip.city_category_id # 👈 NOW FROM TRIP
if not group:
raise ValidationError("Trip must have a Travel Group.")
if not city_category:
raise ValidationError("Trip must have a City Category selected.")
policy = self.env['travel.stay.policy'].search([
('travel_group_id', '=', group.id),
('city_category_id', '=', city_category.id),
('active', '=', True)
], limit=1)
if not policy:
raise ValidationError(
f"No Stay Policy configured for Group '{group.name}' and City '{city_category.name}'."
)
if record.amount > policy.max_amount:
raise ValidationError(
f"Stay expense exceeds allowed limit of {policy.max_amount}."
)

View File

@ -0,0 +1,25 @@
from odoo import models, fields
class TravelGroup(models.Model):
_name = 'travel.group'
_description = 'Travel Group'
name = fields.Char(string='Travel Group', required=True)
active = fields.Boolean(default=True)
job_ids = fields.One2many(
'hr.job',
'travel_group_id',
string="Designations"
)
level_code = fields.Selection([
('a', 'Level A'),
('b', 'Level B'),
('c', 'Level C'),
], required=True)
allowed_travel_mode_ids = fields.Many2many(
'travel.mode',
string="Allowed Travel Modes"
)

View File

@ -0,0 +1,31 @@
from odoo import models, fields
class TravelModePolicy(models.Model):
_name = 'travel.mode.policy'
_description = 'Travel Mode Policy'
_rec_name = 'travel_mode' # This makes dropdown show Flight/2AC etc
travel_group_id = fields.Many2one(
'travel.group',
string="Travel Group",
required=True,
ondelete='cascade'
)
mode_type = fields.Selection([
('travel', 'Travel'),
('local', 'Local Travel'),
], string="Mode Type", required=True)
travel_mode = fields.Selection([
('flight', 'Flight'),
('2ac', 'II AC'),
('3ac', 'III AC'),
('1st_class', '1st Class'),
('car', 'Car'),
('taxi', 'Taxi'),
('auto', 'Auto'),
], string="Travel Mode", required=True)
active = fields.Boolean(default=True)

View File

@ -0,0 +1,26 @@
from odoo import models, fields
class TravelStayPolicy(models.Model):
_name = 'travel.stay.policy'
_description = 'Travel Stay Policy'
travel_group_id = fields.Many2one(
'travel.group',
string='Travel Group',
required=True
)
city_category_id = fields.Many2one(
'travel.city.category',
string='City Category',
required=True
)
min_amount = fields.Float(string='Min Amount')
max_amount = fields.Float(string='Max Amount')
is_actuals = fields.Boolean(
string='Actuals Allowed',
help='If checked, actual hotel cost is allowed'
)
active = fields.Boolean(default=True)

View File

@ -0,0 +1,179 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
class TravelTrip(models.Model):
_name = 'travel.trip'
_description = 'Business Travel Trip'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'id desc'
name = fields.Char(
string='Trip Reference',
required=True,
copy=False,
readonly=True,
default='New',
tracking=True
)
employee_id = fields.Many2one(
'hr.employee',
string='Employee',
required=True,
tracking=True
)
department_id = fields.Many2one(
'hr.department',
compute='_compute_emp_details',
store=True,
readonly=True,
tracking=True,
compute_sudo=True, # IMPORTANT
)
manager_id = fields.Many2one(
'hr.employee',
compute='_compute_emp_details',
store=True,
readonly=True,
tracking=True,
compute_sudo=True, # IMPORTANT
)
purpose = fields.Text(tracking=True)
from_location = fields.Char(tracking=True)
to_location = fields.Char(tracking=True)
start_date = fields.Date(tracking=True)
end_date = fields.Date(tracking=True)
estimated_cost = fields.Float(tracking=True)
reject_reason = fields.Text(string="Reject Reason", tracking=True)
state = fields.Selection([
('draft', 'Draft'),
('submitted', 'Submitted'),
('approved', 'Approved'),
('completed', 'Completed'),
('reimbursed', 'Reimbursed'),
], default='draft', tracking=True)
# expense_ids = fields.One2many(
# 'travel.expense',
# 'trip_id',
# string='Expenses'
# )
trave_activity_ids = fields.One2many(
'travel.activity', # child model
'trip_id', # inverse field in travel.activity
string="Activities"
)
total_expense = fields.Float(
string='Activity Total',
compute='_compute_total_expense',
store=True
)
travel_group_id = fields.Many2one(
'travel.group',
string='Travel Group',
related='employee_id.job_id.travel_group_id',
store=True,
readonly=True
)
city_category_id = fields.Many2one(
'travel.city.category',
string="City Category",
required=True
)
@api.depends('trave_activity_ids.total_amount')
def _compute_total_expense(self):
for rec in self:
rec.total_expense = sum(rec.trave_activity_ids.mapped('total_amount'))
# ---------------- COMPUTE ----------------
@api.depends('employee_id')
def _compute_emp_details(self):
for rec in self:
if rec.employee_id:
emp = rec.employee_id.sudo()
rec.department_id = emp.department_id
rec.manager_id = emp.parent_id
else:
rec.department_id = False
rec.manager_id = False
# ---------------- CREATE ----------------
@api.model
def create(self, vals):
if vals.get('name', 'New') == 'New':
vals['name'] = self.env['ir.sequence'].next_by_code('travel.trip') or 'New'
return super().create(vals)
# ---------------- ACTIONS ----------------
def action_submit(self):
for rec in self:
# Submit all DRAFT expenses inside ALL activities
activities = rec.trave_activity_ids
expenses = activities.mapped('expense_ids').filtered(
lambda e: e.state == 'draft'
)
expenses.write({'state': 'submitted'})
rec.state = 'submitted'
def action_approve(self):
for rec in self:
if rec.state != 'submitted':
raise UserError('Only Submitted trips can be approved.')
# Only reporting manager or admin can approve
manager_user = rec.manager_id.sudo().user_id
if not self.env.is_admin() and (not manager_user or manager_user != self.env.user):
raise UserError("Only the reporting manager can approve this trip.")
rec.state = 'approved'
rec.message_post(
body=f"Trip <b>{rec.name}</b> has been approved by {self.env.user.name}.",
subtype_xmlid="mail.mt_comment"
)
def action_mark_completed(self):
for rec in self:
if rec.state != 'approved':
raise UserError('Only Approved trips can be marked as Completed.')
rec.state = 'completed'
rec.message_post(
body=f"Trip <b>{rec.name}</b> has been marked as Completed.",
subtype_xmlid="mail.mt_comment"
)
is_current_user_manager = fields.Boolean(
compute="_compute_is_current_user_manager",
store=False
)
def _compute_is_current_user_manager(self):
for rec in self:
rec.is_current_user_manager = (
rec.manager_id
and rec.manager_id.sudo().user_id
and rec.manager_id.sudo().user_id.id == self.env.user.id
)

View File

@ -0,0 +1,11 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_travel_trip,travel.trip,model_travel_trip,base.group_user,1,1,1,1
access_travel_expense,travel.expense,model_travel_expense,base.group_user,1,1,1,1
access_trip_reject_wizard,trip.reject.wizard,model_trip_reject_wizard,base.group_user,1,1,1,1
access_travel_activity_employee,travel.activity employee,model_travel_activity,base.group_user,1,1,1,1
access_travel_activity_user,travel.activity user,model_travel_activity,base.group_user,1,1,1,1
access_travel_city_category,travel.city.category,model_travel_city_category,base.group_user,1,1,1,1
access_travel_group_user,travel.group user,model_travel_group,base.group_user,1,1,1,1
access_travel_stay_policy,travel.stay.policy,model_travel_stay_policy,base.group_user,1,1,1,1
access_travel_daily_allowance_user,access_travel_daily_allowance_user,model_travel_daily_allowance,base.group_user,1,1,1,1
access_travel_mode_policy_user,access_travel_mode_policy_user,model_travel_mode_policy,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_travel_trip travel.trip model_travel_trip base.group_user 1 1 1 1
3 access_travel_expense travel.expense model_travel_expense base.group_user 1 1 1 1
4 access_trip_reject_wizard trip.reject.wizard model_trip_reject_wizard base.group_user 1 1 1 1
5 access_travel_activity_employee travel.activity employee model_travel_activity base.group_user 1 1 1 1
6 access_travel_activity_user travel.activity user model_travel_activity base.group_user 1 1 1 1
7 access_travel_city_category travel.city.category model_travel_city_category base.group_user 1 1 1 1
8 access_travel_group_user travel.group user model_travel_group base.group_user 1 1 1 1
9 access_travel_stay_policy travel.stay.policy model_travel_stay_policy base.group_user 1 1 1 1
10 access_travel_daily_allowance_user access_travel_daily_allowance_user model_travel_daily_allowance base.group_user 1 1 1 1
11 access_travel_mode_policy_user access_travel_mode_policy_user model_travel_mode_policy base.group_user 1 1 1 1

View File

@ -0,0 +1,18 @@
<odoo>
<record id="group_travel_employee" model="res.groups">
<field name="name">Travel - Employee</field>
<field name="category_id" ref="base.module_category_human_resources"/>
</record>
<record id="group_travel_manager" model="res.groups">
<field name="name">Travel - Manager</field>
<field name="category_id" ref="base.module_category_human_resources"/>
<field name="implied_ids" eval="[(4, ref('group_travel_employee'))]"/>
</record>
<record id="group_travel_finance" model="res.groups">
<field name="name">Travel - Finance</field>
<field name="category_id" ref="base.module_category_human_resources"/>
<field name="implied_ids" eval="[(4, ref('group_travel_manager'))]"/>
</record>
</odoo>

View File

@ -0,0 +1,27 @@
<odoo>
<!-- Employees: only their own trips -->
<record id="travel_trip_rule_employee_own" model="ir.rule">
<field name="name">Travel Trip: Employee Own</field>
<field name="model_id" ref="model_travel_trip"/>
<field name="domain_force">[('employee_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('business_travel_expense_management.group_travel_employee'))]"/>
</record>
<!-- Managers: trips of their team -->
<record id="travel_trip_rule_manager_team" model="ir.rule">
<field name="name">Travel Trip: Manager Team</field>
<field name="model_id" ref="model_travel_trip"/>
<field name="domain_force">[('manager_id.user_id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('business_travel_expense_management.group_travel_manager'))]"/>
</record>
<!-- System / Admin: everything -->
<record id="travel_trip_rule_admin_all" model="ir.rule">
<field name="name">Travel Trip: Admin All</field>
<field name="model_id" ref="model_travel_trip"/>
<field name="domain_force">[(1,'=',1)]</field>
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
</record>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,16 @@
<odoo>
<record id="view_hr_job_form_inherit_travel_group" model="ir.ui.view">
<field name="name">hr.job.form.inherit.travel.group</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr.view_hr_job_form"/>
<field name="arch" type="xml">
<!-- Insert Travel Group below Department -->
<xpath expr="//field[@name='department_id']" position="after">
<field name="designation_level"/>
<field name="travel_group_id" readonly="1"/>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_travel_activity_form" model="ir.ui.view">
<field name="name">travel.activity.form</field>
<field name="model">travel.activity</field>
<field name="arch" type="xml">
<form string="Activity">
<sheet>
<!-- TITLE -->
<div class="oe_title">
<h1>
<field name="name" placeholder="Activity title"/>
</h1>
</div>
<!-- ACTIVITY TYPE -->
<group>
<field name="activity_type"/>
</group>
<!-- TRAVEL DETAILS -->
<group string="Travel Details"
invisible="activity_type != 'travel'">
<group>
<field name="travel_mode_policy_id"/>
<field name="from_location"/>
<field name="to_location"/>
</group>
<group>
<field name="start_datetime"/>
<field name="end_datetime"/>
</group>
<!-- <field name="travel_details" colspan="2"/>-->
</group>
<!-- STAY DETAILS -->
<group string="Accommodation Details"
invisible="activity_type != 'stay'">
<group>
<field name="stay_type"/>
<field name="hotel_name"/>
<field name="city"/>
</group>
<group>
<field name="checkin"/>
<field name="checkout"/>
</group>
</group>
<!-- MEETING DETAILS -->
<group string="Meeting Details"
invisible="activity_type != 'meeting'">
<group>
<field name="meeting_title"/>
<field name="meeting_location"/>
</group>
<group>
<field name="start_datetime"/>
<field name="end_datetime"/>
</group>
<!-- <field name="notes" colspan="2"/>-->
</group>
<!-- LOCAL TRAVEL -->
<group string="Local Commute Details"
invisible="activity_type != 'local'">
<group>
<field name="local_travel_mode"/>
<field name="from_location"/>
<field name="to_location"/>
</group>
<group>
<field name="start_datetime"/>
<field name="end_datetime"/>
</group>
</group>
<!-- DOCUMENTS -->
<separator string="Documents"/>
<field name="attachment_ids">
<list editable="bottom">
<field name="name"/>
<field name="datas"/>
</list>
</field>
<!-- EXPENSES -->
<separator string="Expenses"/>
<field name="expense_ids"
context="{'default_activity_id': id}">
<list editable="bottom">
<field name="name"/>
<field name="expense_date"/>
<field name="amount"/>
<field name="state"/>
</list>
</field>
<!-- TOTAL -->
<group>
<field name="total_amount" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_travel_activity" model="ir.actions.act_window">
<field name="name">Activity</field>
<field name="res_model">travel.activity</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_travel_activity_form"/>
</record>
</odoo>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- List View -->
<record id="view_travel_city_category_tree" model="ir.ui.view">
<field name="name">travel.city.category.tree</field>
<field name="model">travel.city.category</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="code"/>
<field name="active"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_travel_city_category_form" model="ir.ui.view">
<field name="name">travel.city.category.form</field>
<field name="model">travel.city.category</field>
<field name="arch" type="xml">
<form string="City Category">
<sheet>
<group>
<field name="name"/>
<field name="code"/>
<field name="active"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- Action -->
<record id="action_travel_city_category" model="ir.actions.act_window">
<field name="name">City Categories</field>
<field name="res_model">travel.city.category</field>
<field name="view_mode">list,form</field>
</record>
<!-- Menu -->
<!-- <menuitem id="menu_travel_config_root"-->
<!-- name="Travel Configuration"-->
<!-- parent="menu_travel_root"-->
<!-- sequence="50"/>-->
<!-- <menuitem id="menu_travel_city_category"-->
<!-- name="City Categories"-->
<!-- parent="menu_travel_config_root"-->
<!-- action="action_travel_city_category"-->
<!-- sequence="10"/>-->
</odoo>

View File

@ -0,0 +1,44 @@
<odoo>
<!-- TREE VIEW -->
<record id="view_travel_daily_allowance_tree" model="ir.ui.view">
<field name="name">travel.daily.allowance.tree</field>
<field name="model">travel.daily.allowance</field>
<field name="arch" type="xml">
<list>
<field name="travel_group_id"/>
<field name="city_category_id"/>
<field name="amount"/>
<field name="actuals_allowed"/>
<field name="active"/>
</list>
</field>
</record>
<!-- FORM VIEW -->
<record id="view_travel_daily_allowance_form" model="ir.ui.view">
<field name="name">travel.daily.allowance.form</field>
<field name="model">travel.daily.allowance</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="travel_group_id"/>
<field name="city_category_id"/>
<field name="amount"/>
<field name="actuals_allowed"/>
<field name="active"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- ACTION -->
<record id="action_travel_daily_allowance" model="ir.actions.act_window">
<field name="name">Daily Allowance Policies</field>
<field name="res_model">travel.daily.allowance</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_travel_expense_form" model="ir.ui.view">
<field name="name">travel.expense.form</field>
<field name="model">travel.expense</field>
<field name="arch" type="xml">
<form string="Expense">
<header>
<button name="action_submit"
string="Submit"
type="object"
class="btn-primary"
invisible="state != 'draft'"/>
<button name="action_approve"
string="Approve"
type="object"
class="btn-success"
invisible="state != 'submitted'"/>
<button name="action_mark_reimbursed"
string="Reimburse"
type="object"
class="btn-success"
invisible="state != 'approved'"/>
</header>
<sheet>
<div class="oe_title"
style="display:flex; justify-content:space-between;">
<h1>
<field name="name" placeholder="Expense Description"/>
</h1>
<field name="state"
widget="statusbar"
statusbar_visible="draft,submitted,approved,reimbursed"/>
</div>
<group>
<group>
<field name="activity_id"/>
<field name="expense_date"/>
</group>
<group>
<field name="amount"/>
</group>
</group>
<group string="Documents">
<field name="receipt" widget="binary"/>
</group>
</sheet>
<!-- <chatter>-->
<!-- <field name="message_ids"/>-->
<!-- </chatter>-->
</form>
</field>
</record>
</odoo>

View File

@ -0,0 +1,43 @@
<odoo>
<!-- Tree View -->
<record id="view_travel_group_tree" model="ir.ui.view">
<field name="name">travel.group.tree</field>
<field name="model">travel.group</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="active"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_travel_group_form" model="ir.ui.view">
<field name="name">travel.group.form</field>
<field name="model">travel.group</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
<field name="level_code"/> <!-- ADD THIS -->
<field name="active"/>
</group>
<group string="Designations">
<field name="job_ids" widget="many2many_tags" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- Action -->
<record id="action_travel_group" model="ir.actions.act_window">
<field name="name">Travel Groups</field>
<field name="res_model">travel.group</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- ROOT MENU (MUST HAVE ACTION) -->
<menuitem id="menu_travel_root"
name="Business Travel"
action="action_travel_trip"
sequence="50"
web_icon="business_travel_expense_management,static/decription/icon.png"
groups="base.group_user"/>
<!-- TRIPS -->
<menuitem id="menu_travel_trip"
name="Trips"
parent="menu_travel_root"
action="action_travel_trip"
sequence="10"
groups="base.group_user"/>
<!-- CONFIG ROOT -->
<menuitem id="menu_travel_config_root"
name="Travel Configuration"
parent="menu_travel_root"
sequence="50"
groups="base.group_user"/>
<!-- CITY CATEGORY -->
<menuitem id="menu_travel_city_category"
name="City Categories"
parent="menu_travel_config_root"
action="action_travel_city_category"
sequence="10"
groups="base.group_user"/>
<!-- Menu -->
<menuitem id="menu_travel_group"
name="Travel Groups"
parent="menu_travel_config_root"
action="action_travel_group"
sequence="20"
groups="base.group_user"/>
<menuitem id="menu_travel_stay_policy"
name="Stay Policies"
parent="menu_travel_config_root"
action="action_travel_stay_policy"
sequence="30"/>
<menuitem id="menu_travel_daily_allowance"
name="Daily Allowance Policies"
parent="menu_travel_config_root"
action="action_travel_daily_allowance"
sequence="20"/>
<menuitem id="menu_travel_mode_policy"
name="Travel Mode Policies"
parent="menu_travel_config_root"
action="action_travel_mode_policy"
sequence="30"/>
</odoo>

View File

@ -0,0 +1,41 @@
<odoo>
<!-- TREE -->
<record id="view_travel_mode_policy_tree" model="ir.ui.view">
<field name="name">travel.mode.policy.tree</field>
<field name="model">travel.mode.policy</field>
<field name="arch" type="xml">
<list>
<field name="travel_group_id"/>
<field name="mode_type"/>
<field name="travel_mode"/>
<field name="active"/>
</list>
</field>
</record>
<!-- FORM -->
<record id="view_travel_mode_policy_form" model="ir.ui.view">
<field name="name">travel.mode.policy.form</field>
<field name="model">travel.mode.policy</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="travel_group_id"/>
<field name="mode_type"/>
<field name="travel_mode"/>
<field name="active"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_travel_mode_policy" model="ir.actions.act_window">
<field name="name">Travel Mode Policies</field>
<field name="res_model">travel.mode.policy</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -0,0 +1,42 @@
<odoo>
<record id="view_travel_stay_policy_tree" model="ir.ui.view">
<field name="name">travel.stay.policy.tree</field>
<field name="model">travel.stay.policy</field>
<field name="arch" type="xml">
<list>
<field name="travel_group_id"/>
<field name="city_category_id"/>
<field name="min_amount"/>
<field name="max_amount"/>
<field name="is_actuals"/>
<field name="active"/>
</list>
</field>
</record>
<record id="view_travel_stay_policy_form" model="ir.ui.view">
<field name="name">travel.stay.policy.form</field>
<field name="model">travel.stay.policy</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="travel_group_id"/>
<field name="city_category_id"/>
<field name="is_actuals"/>
<field name="min_amount"/>
<field name="max_amount"/>
<field name="active"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_travel_stay_policy" model="ir.actions.act_window">
<field name="name">Stay Policies</field>
<field name="res_model">travel.stay.policy</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Trip Form View -->
<record id="view_travel_trip_form" model="ir.ui.view">
<field name="name">travel.trip.form</field>
<field name="model">travel.trip</field>
<field name="arch" type="xml">
<form string="Trip">
<header>
<!-- Employee -->
<button name="action_submit"
string="Submit Trip"
type="object"
class="btn-primary"
invisible="state != 'draft'"/>
<!-- Manager only -->
<button name="action_approve"
string="Approve"
type="object"
class="btn-success"
invisible="state != 'submitted' or not is_current_user_manager"/>
<button name="%(action_trip_reject_wizard)d"
string="Reject"
type="action"
class="btn-danger"
invisible="state != 'submitted' or not is_current_user_manager"/>
<!-- Employee after approval -->
<button name="action_mark_completed"
string="Mark Completed"
type="object"
class="btn-secondary"
invisible="state != 'approved'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,submitted,approved,completed,reimbursed,rejected"/>
</header>
<sheet>
<group>
<group>
<field name="name" readonly="1"/>
<field name="employee_id" readonly="state != 'draft'"/>
<xpath expr="//field[@name='employee_id']" position="after">
<field name="travel_group_id" readonly="1"/>
</xpath>
<field name="department_id" readonly="1"/>
<field name="manager_id" readonly="1"/>
</group>
<group>
<field name="from_location" readonly="state != 'draft'"/>
<field name="to_location" readonly="state != 'draft'"/>
<field name="start_date" readonly="state != 'draft'"/>
<field name="end_date" readonly="state != 'draft'"/>
<field name="estimated_cost" readonly="state != 'draft'"/>
<field name="total_expense" string="Actual Cost"/>
<field name="city_category_id"/>
</group>
</group>
<group>
<field name="purpose" readonly="state != 'draft'"/>
</group>
<!-- Show reject reason only when present -->
<group invisible="not reject_reason">
<field name="reject_reason" readonly="1"/>
</group>
<notebook>
<!-- <page string="Expenses">-->
<!-- <field name="expense_ids"-->
<!-- readonly="state != 'draft'"-->
<!-- context="{'form_view_ref': 'business_travel_expense_management.view_travel_expense_form'}">-->
<!-- &lt;!&ndash; Only LIST here &ndash;&gt;-->
<!-- <list>-->
<!-- <field name="name"/>-->
<!-- <field name="category"/>-->
<!-- <field name="transport_detail"/>-->
<!-- <field name="distance_km"/>-->
<!-- <field name="amount"/>-->
<!-- <field name="expense_date"/>-->
<!-- <field name="state"/>-->
<!-- </list>-->
<!-- </field>-->
<page string="Activities">
<field name="trave_activity_ids"
context="{'default_trip_id': id}">
<list>
<field name="sequence" widget="handle"/>
<field name="name" string="Activity"/>
<field name="activity_type"/>
<field name="start_datetime"/>
<field name="end_datetime"/>
<!-- <field name="currency_id" invisible="1"/>-->
<field name="total_amount"
string="Activity Total"
widget="monetary"/>
<!-- options="{'currency_field': 'currency_id'}"/>-->
</list>
</field>
</page>
</notebook>
<!-- <group>-->
<!-- <field name="total_amount"-->
<!-- widget="monetary"-->
<!-- options="{'currency_field': 'currency_id'}"-->
<!-- readonly="1"/>-->
<!-- </group>-->
<chatter>
<field name="message_follower_ids"/>
<!-- <field name="activity_ids"/>-->
<field name="message_ids"/>
</chatter>
</sheet>
</form>
</field>
</record>
<!-- Tree View -->
<record id="view_travel_trip_tree" model="ir.ui.view">
<field name="name">travel.trip.tree</field>
<field name="model">travel.trip</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="employee_id"/>
<field name="from_location"/>
<field name="to_location"/>
<field name="start_date"/>
<field name="end_date"/>
<field name="estimated_cost"/>
<field name="state"/>
</list>
</field>
</record>
<!-- Action -->
<record id="action_travel_trip" model="ir.actions.act_window">
<field name="name">Trips</field>
<field name="res_model">travel.trip</field>
<field name="view_mode">list,form</field>
</record>
<!-- &lt;!&ndash; Menu &ndash;&gt;-->
<!-- <menuitem id="menu_travel_root"-->
<!-- name="Business Travel"-->
<!-- sequence="50"/>-->
<!-- <menuitem id="menu_travel_trip"-->
<!-- name="Trips"-->
<!-- parent="menu_travel_root"-->
<!-- action="action_travel_trip"-->
<!-- sequence="10"/>-->
</odoo>

View File

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

View File

@ -0,0 +1,19 @@
from odoo import models, fields
from odoo.exceptions import UserError
class TripRejectWizard(models.TransientModel):
_name = 'trip.reject.wizard'
_description = 'Reject Trip Wizard'
reason = fields.Text(string="Reason for Rejection", required=True)
def action_confirm_reject(self):
trip = self.env['travel.trip'].browse(self.env.context.get('active_id'))
if not trip:
raise UserError("No Trip found.")
trip.write({
'state': 'draft',
'reject_reason': self.reason
})

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_trip_reject_wizard" model="ir.ui.view">
<field name="name">trip.reject.wizard.form</field>
<field name="model">trip.reject.wizard</field>
<field name="arch" type="xml">
<form string="Reject Trip">
<group>
<field name="reason"/>
</group>
<footer>
<button string="Confirm Reject"
type="object"
name="action_confirm_reject"
class="btn-danger"/>
<button string="Cancel" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_trip_reject_wizard" model="ir.actions.act_window">
<field name="name">Reject Trip</field>
<field name="res_model">trip.reject.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
</odoo>

View File

@ -1 +1 @@
from . import models
from . import models

View File

@ -1,21 +1,21 @@
{
'name': 'Consolidated Payslip Grid (OWL)',
'version': '1.0',
'category': 'Human Resources',
'summary': 'Editable Consolidated Payslip Grid (OWL + pqGrid)',
'author': 'Raman Marikanti',
'depends': ['hr_payroll', 'web'],
'data': [
'security/ir.model.access.csv',
'views/batch_payslip_view.xml',
],
'assets': {
'web.assets_backend': [
# Internal module JS and XML files (ensure correct paths within 'static/src')
'consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.js',
'consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.xml',
],
},
'installable': True,
}
{
'name': 'Consolidated Payslip Grid (OWL)',
'version': '1.0',
'category': 'Human Resources',
'summary': 'Editable Consolidated Payslip Grid (OWL + pqGrid)',
'author': 'Raman Marikanti',
'depends': ['hr_payroll', 'web'],
'data': [
'security/ir.model.access.csv',
'views/batch_payslip_view.xml',
],
'assets': {
'web.assets_backend': [
# Internal module JS and XML files (ensure correct paths within 'static/src')
'consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.js',
'consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.xml',
],
},
'installable': True,
}

View File

@ -1,312 +1,312 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
from collections import defaultdict
from functools import reduce
class HrPayslipRun(models.Model):
_inherit = 'hr.payslip.run'
@api.model
def get_consolidated_attendance_data(self, payslip_run_id):
"""
Returns consolidated attendance and leave data for all employees in the payslip run
"""
# Get all payslips in this batch
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
if not payslips:
return []
result = []
for slip in payslips:
employee = slip.employee_id
contract = slip.contract_id
# Get attendance data
attendance_days = self._get_attendance_days(slip)
worked_days = self._get_worked_days(slip)
leave_days = self._get_leave_days(slip)
lop_days = self._get_lop_days(slip)
# Get leave balances
leave_balances = self._get_leave_balances(employee,slip.date_from,slip.date_to)
leave_taken = self._get_leave_taken(slip)
result.append({
'id': slip.id,
'employee_id': (employee.id, employee.name),
'employee_code': employee.employee_id or '',
'department_id': (employee.department_id.id,
employee.department_id.name) if employee.department_id else False,
'total_days': (slip.date_to - slip.date_from).days + 1,
'worked_days': worked_days,
'attendance_days': attendance_days,
'leave_days': leave_days,
'lop_days': lop_days,
'doj':contract.date_start,
'birthday':employee.birthday,
'bank': employee.bank_account_id.display_name if employee.bank_account_id else '-',
'sick_leave_balance': leave_balances.get('LEAVE110', 0),
'casual_leave_balance': leave_balances.get('LEAVE120', 0),
'privilege_leave_balance': leave_balances.get('LEAVE100', 0),
'sick_leave_taken': leave_taken.get('sick', 0),
'casual_leave_taken': leave_taken.get('casual', 0),
'privilege_leave_taken': leave_taken.get('privilege', 0),
'state': slip.state,
'lines':self.get_payslip_lines_data(slip),
})
return result
@api.model
def sub_columns(self,payslip_run_id):
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
names = payslips.line_ids.filtered(lambda x:x.amount != 0)
code_name_dict = {line.code+line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code : line.name for line in names}
columns = []
for code, name in code_name_dict.items():
columns.append({
'title': name,
'dataIndx': code,
'width': 150,
'editable': False,
'summary': {'type': "sum_"},
})
return columns
def save_consolidated_attendance_data(self, payslip_run_id, data):
"""
Saves the edited attendance and leave data from the grid
"""
self.ensure_one()
for item in data:
slip = self.env['hr.payslip'].browse(item['id'])
if slip.state != 'draft':
raise UserError(_("Cannot edit payslip in %s state") % slip.state)
# Update LOP days
if 'lop_days' in item:
self._update_lop_days(slip, float(item['lop_days']))
# Update leave days taken
leave_updates = {}
if 'sick_leave_taken' in item:
leave_updates['sick'] = float(item['sick_leave_taken'])
if 'casual_leave_taken' in item:
leave_updates['casual'] = float(item['casual_leave_taken'])
if 'privilege_leave_taken' in item:
leave_updates['privilege'] = float(item['privilege_leave_taken'])
if leave_updates:
self._update_leave_taken(slip, leave_updates)
return True
def recalculate_lop_days(self, payslip_run_id):
"""
Recalculates LOP days for all payslips in the batch based on attendance
"""
self.ensure_one()
payslips = self.env['hr.payslip'].search([
('payslip_run_id', '=', payslip_run_id),
('state', '=', 'draft')
])
for slip in payslips:
attendance_days = self._get_attendance_days(slip)
expected_days = (slip.date_to - slip.date_from).days + 1
lop_days = expected_days - attendance_days
self._update_lop_days(slip, lop_days)
return True
def validate_all_attendance_data(self, payslip_run_id):
"""
Marks all payslips in the batch as validated
"""
self.ensure_one()
payslips = self.env['hr.payslip'].search([
('payslip_run_id', '=', payslip_run_id),
('state', '=', 'draft')
])
if not payslips:
raise UserError(_("No draft payslips found in this batch"))
payslips.write({'state': 'verify'})
return True
# Helper methods
def _get_attendance_days(self, payslip):
"""
Returns number of days employee was present (based on attendance records)
"""
attendance_records = self.env['hr.attendance'].search([
('employee_id', '=', payslip.employee_id.id),
('check_in', '>=', payslip.date_from),
('check_in', '<=', payslip.date_to)
])
# Group by day
unique_days = set()
for att in attendance_records:
unique_days.add(att.check_in.date())
return len(unique_days)
def _get_worked_days(self, payslip):
"""
Returns number of working days (excluding weekends and holidays)
"""
return payslip._get_worked_days_line_number_of_days('WORK100') # Assuming WORK100 is your work code
def get_payslip_lines_data(self, payslip_id):
list = []
for line in payslip_id.line_ids:
list.append({
'name': line.name,
'code': line.code + line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code,
'category_id': line.category_id.name if line.category_id else False,
'amount': line.amount,
'quantity': line.quantity,
'rate': line.rate
})
return list
def _get_leave_days(self, payslip):
"""
Returns total leave days taken in this period
"""
leave_lines = payslip.worked_days_line_ids.filtered(
lambda l: l.code in ['LEAVE110', 'LEAVE90', 'LEAVE100', 'LEAVE120'] # Your leave codes
)
return sum(leave_lines.mapped('number_of_days'))
def _get_lop_days(self, payslip):
"""
Returns LOP days from payslip
"""
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LEAVE90')
return lop_line.number_of_days if lop_line else 0
def _get_leave_taken(self, payslip):
"""
Returns leave days taken in this payslip period
"""
leave_lines = payslip.worked_days_line_ids
return {
'sick': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE110').mapped('number_of_days')),
'casual': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE120').mapped('number_of_days')),
'privilege': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE100').mapped('number_of_days')),
}
def _update_lop_days(self, payslip, days):
"""
Updates LOP days in the payslip
"""
WorkedDays = self.env['hr.payslip.worked_days']
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LOP')
if lop_line:
if days > 0:
lop_line.write({'number_of_days': days})
else:
lop_line.unlink()
elif days > 0:
WorkedDays.create({
'payslip_id': payslip.id,
'name': _('Loss of Pay'),
'code': 'LOP',
'number_of_days': days,
'number_of_hours': days * 8, # Assuming 8-hour work day
'contract_id': payslip.contract_id.id,
})
def _get_leave_balances(self, employee, date_from, date_to):
Leave = self.env['hr.leave']
Allocation = self.env['hr.leave.allocation']
leave_types = self.env['hr.leave.type'].search([])
balances = {}
for leave_type in leave_types:
# Approved allocations within or before payslip period
allocations = Allocation.search([
('employee_id', '=', employee.id),
('state', '=', 'validate'),
('holiday_status_id', '=', leave_type.id),
('date_from', '<=', str(date_to)), # Allocation should be active during payslip
])
allocated = sum(alloc.number_of_days for alloc in allocations)
# Approved leaves within the payslip period
leaves = Leave.search([
('employee_id', '=', employee.id),
('state', '=', 'validate'),
('holiday_status_id', '=', leave_type.id),
('request_date_to', '<=', str(date_to))
])
used = sum(leave.number_of_days for leave in leaves)
# Key: leave code or fallback to name
code = leave_type.work_entry_type_id.code
balances[code] = allocated - used
return balances
def _update_leave_taken(self, payslip, leave_data):
"""
Updates leave days taken in the payslip
"""
WorkedDays = self.env['hr.payslip.worked_days']
for leave_type, days in leave_data.items():
code = leave_type.upper()
line = payslip.worked_days_line_ids.filtered(lambda l: l.code == code)
if line:
if days > 0:
line.write({'number_of_days': days})
else:
line.unlink()
elif days > 0:
WorkedDays.create({
'payslip_id': payslip.id,
'name': _(leave_type.capitalize() + ' Leave'),
'code': code,
'number_of_days': days,
'number_of_hours': days * 8, # Assuming 8-hour work day
'contract_id': payslip.contract_id.id,
})
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
def get_payslip_lines_data(self, payslip_id):
payslip = self.browse(payslip_id)
return [{
'name': line.name,
'code': line.code,
'category_id': (line.category_id.id, line.category_id.name),
'amount': line.amount,
'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
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
from collections import defaultdict
from functools import reduce
class HrPayslipRun(models.Model):
_inherit = 'hr.payslip.run'
@api.model
def get_consolidated_attendance_data(self, payslip_run_id):
"""
Returns consolidated attendance and leave data for all employees in the payslip run
"""
# Get all payslips in this batch
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
if not payslips:
return []
result = []
for slip in payslips:
employee = slip.employee_id
contract = slip.contract_id
# Get attendance data
attendance_days = self._get_attendance_days(slip)
worked_days = self._get_worked_days(slip)
leave_days = self._get_leave_days(slip)
lop_days = self._get_lop_days(slip)
# Get leave balances
leave_balances = self._get_leave_balances(employee,slip.date_from,slip.date_to)
leave_taken = self._get_leave_taken(slip)
result.append({
'id': slip.id,
'employee_id': (employee.id, employee.name),
'employee_code': employee.employee_id or '',
'department_id': (employee.department_id.id,
employee.department_id.name) if employee.department_id else False,
'total_days': (slip.date_to - slip.date_from).days + 1,
'worked_days': worked_days,
'attendance_days': attendance_days,
'leave_days': leave_days,
'lop_days': lop_days,
'doj':contract.date_start,
'birthday':employee.birthday,
'bank': employee.bank_account_id.display_name if employee.bank_account_id else '-',
'sick_leave_balance': leave_balances.get('LEAVE110', 0),
'casual_leave_balance': leave_balances.get('LEAVE120', 0),
'privilege_leave_balance': leave_balances.get('LEAVE100', 0),
'sick_leave_taken': leave_taken.get('sick', 0),
'casual_leave_taken': leave_taken.get('casual', 0),
'privilege_leave_taken': leave_taken.get('privilege', 0),
'state': slip.state,
'lines':self.get_payslip_lines_data(slip),
})
return result
@api.model
def sub_columns(self,payslip_run_id):
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
names = payslips.line_ids.filtered(lambda x:x.amount != 0)
code_name_dict = {line.code+line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code : line.name for line in names}
columns = []
for code, name in code_name_dict.items():
columns.append({
'title': name,
'dataIndx': code,
'width': 150,
'editable': False,
'summary': {'type': "sum_"},
})
return columns
def save_consolidated_attendance_data(self, payslip_run_id, data):
"""
Saves the edited attendance and leave data from the grid
"""
self.ensure_one()
for item in data:
slip = self.env['hr.payslip'].browse(item['id'])
if slip.state != 'draft':
raise UserError(_("Cannot edit payslip in %s state") % slip.state)
# Update LOP days
if 'lop_days' in item:
self._update_lop_days(slip, float(item['lop_days']))
# Update leave days taken
leave_updates = {}
if 'sick_leave_taken' in item:
leave_updates['sick'] = float(item['sick_leave_taken'])
if 'casual_leave_taken' in item:
leave_updates['casual'] = float(item['casual_leave_taken'])
if 'privilege_leave_taken' in item:
leave_updates['privilege'] = float(item['privilege_leave_taken'])
if leave_updates:
self._update_leave_taken(slip, leave_updates)
return True
def recalculate_lop_days(self, payslip_run_id):
"""
Recalculates LOP days for all payslips in the batch based on attendance
"""
self.ensure_one()
payslips = self.env['hr.payslip'].search([
('payslip_run_id', '=', payslip_run_id),
('state', '=', 'draft')
])
for slip in payslips:
attendance_days = self._get_attendance_days(slip)
expected_days = (slip.date_to - slip.date_from).days + 1
lop_days = expected_days - attendance_days
self._update_lop_days(slip, lop_days)
return True
def validate_all_attendance_data(self, payslip_run_id):
"""
Marks all payslips in the batch as validated
"""
self.ensure_one()
payslips = self.env['hr.payslip'].search([
('payslip_run_id', '=', payslip_run_id),
('state', '=', 'draft')
])
if not payslips:
raise UserError(_("No draft payslips found in this batch"))
payslips.write({'state': 'verify'})
return True
# Helper methods
def _get_attendance_days(self, payslip):
"""
Returns number of days employee was present (based on attendance records)
"""
attendance_records = self.env['hr.attendance'].search([
('employee_id', '=', payslip.employee_id.id),
('check_in', '>=', payslip.date_from),
('check_in', '<=', payslip.date_to)
])
# Group by day
unique_days = set()
for att in attendance_records:
unique_days.add(att.check_in.date())
return len(unique_days)
def _get_worked_days(self, payslip):
"""
Returns number of working days (excluding weekends and holidays)
"""
return payslip._get_worked_days_line_number_of_days('WORK100') # Assuming WORK100 is your work code
def get_payslip_lines_data(self, payslip_id):
list = []
for line in payslip_id.line_ids:
list.append({
'name': line.name,
'code': line.code + line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code,
'category_id': line.category_id.name if line.category_id else False,
'amount': line.amount,
'quantity': line.quantity,
'rate': line.rate
})
return list
def _get_leave_days(self, payslip):
"""
Returns total leave days taken in this period
"""
leave_lines = payslip.worked_days_line_ids.filtered(
lambda l: l.code in ['LEAVE110', 'LEAVE90', 'LEAVE100', 'LEAVE120'] # Your leave codes
)
return sum(leave_lines.mapped('number_of_days'))
def _get_lop_days(self, payslip):
"""
Returns LOP days from payslip
"""
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LEAVE90')
return lop_line.number_of_days if lop_line else 0
def _get_leave_taken(self, payslip):
"""
Returns leave days taken in this payslip period
"""
leave_lines = payslip.worked_days_line_ids
return {
'sick': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE110').mapped('number_of_days')),
'casual': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE120').mapped('number_of_days')),
'privilege': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE100').mapped('number_of_days')),
}
def _update_lop_days(self, payslip, days):
"""
Updates LOP days in the payslip
"""
WorkedDays = self.env['hr.payslip.worked_days']
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LOP')
if lop_line:
if days > 0:
lop_line.write({'number_of_days': days})
else:
lop_line.unlink()
elif days > 0:
WorkedDays.create({
'payslip_id': payslip.id,
'name': _('Loss of Pay'),
'code': 'LOP',
'number_of_days': days,
'number_of_hours': days * 8, # Assuming 8-hour work day
'contract_id': payslip.contract_id.id,
})
def _get_leave_balances(self, employee, date_from, date_to):
Leave = self.env['hr.leave']
Allocation = self.env['hr.leave.allocation']
leave_types = self.env['hr.leave.type'].search([])
balances = {}
for leave_type in leave_types:
# Approved allocations within or before payslip period
allocations = Allocation.search([
('employee_id', '=', employee.id),
('state', '=', 'validate'),
('holiday_status_id', '=', leave_type.id),
('date_from', '<=', str(date_to)), # Allocation should be active during payslip
])
allocated = sum(alloc.number_of_days for alloc in allocations)
# Approved leaves within the payslip period
leaves = Leave.search([
('employee_id', '=', employee.id),
('state', '=', 'validate'),
('holiday_status_id', '=', leave_type.id),
('request_date_to', '<=', str(date_to))
])
used = sum(leave.number_of_days for leave in leaves)
# Key: leave code or fallback to name
code = leave_type.work_entry_type_id.code
balances[code] = allocated - used
return balances
def _update_leave_taken(self, payslip, leave_data):
"""
Updates leave days taken in the payslip
"""
WorkedDays = self.env['hr.payslip.worked_days']
for leave_type, days in leave_data.items():
code = leave_type.upper()
line = payslip.worked_days_line_ids.filtered(lambda l: l.code == code)
if line:
if days > 0:
line.write({'number_of_days': days})
else:
line.unlink()
elif days > 0:
WorkedDays.create({
'payslip_id': payslip.id,
'name': _(leave_type.capitalize() + ' Leave'),
'code': code,
'number_of_days': days,
'number_of_hours': days * 8, # Assuming 8-hour work day
'contract_id': payslip.contract_id.id,
})
class HrPayslip(models.Model):
_inherit = 'hr.payslip'
def get_payslip_lines_data(self, payslip_id):
payslip = self.browse(payslip_id)
return [{
'name': line.name,
'code': line.code,
'category_id': (line.category_id.id, line.category_id.name),
'amount': line.amount,
'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

View File

@ -1 +1 @@
id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink
id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink

1 id name model_id:id group_id perm_read perm_write perm_create perm_unlink

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="ConsolidatedPayslipGrid" owl="1">
<div t-ref="gridContainer" style="width: 100%; height: 600px;"></div>
</t>
</templates>
<?xml version="1.0" encoding="utf-8"?>
<templates xml:space="preserve">
<t t-name="ConsolidatedPayslipGrid" owl="1">
<div t-ref="gridContainer" style="width: 100%; height: 600px;"></div>
</t>
</templates>

View File

@ -1,18 +1,18 @@
<odoo>
<record id="view_hr_payslip_run_form_inherit" model="ir.ui.view">
<field name="name">hr.payslip.run.form.inherit.consolidated.pqgrid.owl</field>
<field name="model">hr.payslip.run</field>
<field name="inherit_id" ref="hr_payroll.hr_payslip_run_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<notebook>
<page string="Consolidated Payslip">
<div class="o_consolidated_grid" style="height:600px; margin-top:10px;">
<widget name="ConsolidatedPayslipGrid" batchId="context.active_id"/>
</div>
</page>
</notebook>
</xpath>
</field>
</record>
</odoo>
<odoo>
<record id="view_hr_payslip_run_form_inherit" model="ir.ui.view">
<field name="name">hr.payslip.run.form.inherit.consolidated.pqgrid.owl</field>
<field name="model">hr.payslip.run</field>
<field name="inherit_id" ref="hr_payroll.hr_payslip_run_form"/>
<field name="arch" type="xml">
<xpath expr="//sheet" position="inside">
<notebook>
<page string="Consolidated Payslip">
<div class="o_consolidated_grid" style="height:600px; margin-top:10px;">
<widget name="ConsolidatedPayslipGrid" batchId="context.active_id"/>
</div>
</page>
</notebook>
</xpath>
</field>
</record>
</odoo>

View File

@ -1,22 +1,22 @@
{
'name': 'CWF Timesheet Update',
'version': '1.0',
'category': 'Human Resources',
'summary': 'Manage and update weekly timesheets for CWF department',
'author': 'Your Name or Company',
'depends': ['hr_attendance_extended','web', 'mail', 'base','hr_emp_dashboard','hr_employee_extended'],
'data': [
# 'views/timesheet_form.xml',
'security/security.xml',
'security/ir.model.access.csv',
'views/timesheet_view.xml',
'views/timesheet_weekly_view.xml',
'data/email_template.xml',
],
'assets': {
'web.assets_backend': [
'cwf_timesheet/static/src/js/timesheet_form.js',
],
},
'application': True,
}
{
'name': 'CWF Timesheet Update',
'version': '1.0',
'category': 'Human Resources',
'summary': 'Manage and update weekly timesheets for CWF department',
'author': 'Your Name or Company',
'depends': ['hr_attendance_extended','web', 'mail', 'base','hr_emp_dashboard','hr_employee_extended'],
'data': [
# 'views/timesheet_form.xml',
'security/security.xml',
'security/ir.model.access.csv',
'views/timesheet_view.xml',
'views/timesheet_weekly_view.xml',
'data/email_template.xml',
],
'assets': {
'web.assets_backend': [
'cwf_timesheet/static/src/js/timesheet_form.js',
],
},
'application': True,
}

View File

@ -1,9 +1,9 @@
from odoo import http
from odoo.http import request
class TimesheetController(http.Controller):
@http.route('/timesheet/form', auth='user', website=True)
def timesheet_form(self, **kw):
# This will render the template for the timesheet form
return request.render('timesheet_form', {})
from odoo import http
from odoo.http import request
class TimesheetController(http.Controller):
@http.route('/timesheet/form', auth='user', website=True)
def timesheet_form(self, **kw):
# This will render the template for the timesheet form
return request.render('timesheet_form', {})

View File

@ -1,45 +1,45 @@
<odoo>
<data noupdate="0">
<record id="email_template_timesheet_weekly_update" model="mail.template">
<field name="name">Timesheet Update Reminder</field>
<field name="model_id" ref="cwf_timesheet.model_cwf_weekly_timesheet"/>
<field name="email_from">{{ user.email_formatted }}</field>
<field name="email_to">{{ object.employee_id.user_id.email }}</field>
<field name="subject">Reminder: Update Your Weekly Timesheet</field>
<field name="description">
Reminder to employee to update their weekly timesheet.
</field>
<field name="body_html" type="html">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear <t t-esc="ctx['employee_name']">Employee</t>,
<br/>
<br/>
I hope this message finds you in good spirits. I would like to remind you to please update your weekly timesheet for the period from
<strong>
<t t-esc="ctx['week_from']"/>
</strong>
to
<strong>
<t t-esc="ctx['week_to']"/>
</strong>.
Timely updates are crucial for maintaining accurate records and ensuring smooth processing.
<br/>
<br/>
To make things easier, you can use the link below to update your timesheet:
<br/>
<a href="https://ftprotech.in/odoo/action-261" class="cta-button" target="_blank">Update Timesheet</a>
<br/>
<br/>
Thank you for your attention.
<br/>
Best regards,
<br/>
<strong>Fast Track Project Pvt Ltd.</strong>
<br/>
<br/>
<a href="https://ftprotech.in/" target="_blank">Visit our site</a> for more information.
</p>
</field>
</record>
</data>
</odoo>
<odoo>
<data noupdate="0">
<record id="email_template_timesheet_weekly_update" model="mail.template">
<field name="name">Timesheet Update Reminder</field>
<field name="model_id" ref="cwf_timesheet.model_cwf_weekly_timesheet"/>
<field name="email_from">{{ user.email_formatted }}</field>
<field name="email_to">{{ object.employee_id.user_id.email }}</field>
<field name="subject">Reminder: Update Your Weekly Timesheet</field>
<field name="description">
Reminder to employee to update their weekly timesheet.
</field>
<field name="body_html" type="html">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Dear <t t-esc="ctx['employee_name']">Employee</t>,
<br/>
<br/>
I hope this message finds you in good spirits. I would like to remind you to please update your weekly timesheet for the period from
<strong>
<t t-esc="ctx['week_from']"/>
</strong>
to
<strong>
<t t-esc="ctx['week_to']"/>
</strong>.
Timely updates are crucial for maintaining accurate records and ensuring smooth processing.
<br/>
<br/>
To make things easier, you can use the link below to update your timesheet:
<br/>
<a href="https://ftprotech.in/odoo/action-261" class="cta-button" target="_blank">Update Timesheet</a>
<br/>
<br/>
Thank you for your attention.
<br/>
Best regards,
<br/>
<strong>Fast Track Project Pvt Ltd.</strong>
<br/>
<br/>
<a href="https://ftprotech.in/" target="_blank">Visit our site</a> for more information.
</p>
</field>
</record>
</data>
</odoo>

View File

@ -1,399 +1,398 @@
from odoo import models, fields, api
from odoo.exceptions import ValidationError, UserError
from datetime import datetime, timedelta
import datetime as dt
from odoo import _
from calendar import month_name, month
from datetime import date
class CwfTimesheetYearly(models.Model):
_name = 'cwf.timesheet.calendar'
_description = "CWF Timesheet Calendar"
_rec_name = 'name'
name = fields.Char(string='Year Name', required=True)
week_period = fields.One2many('cwf.timesheet','cwf_calendar_id')
_sql_constraints = [
('unique_year', 'unique(name)', 'The year must be unique!')
]
@api.constrains('name')
def _check_year_format(self):
for record in self:
if not record.name.isdigit() or len(record.name) != 4:
raise ValidationError("Year Name must be a 4-digit number.")
def generate_week_period(self):
for record in self:
record.week_period.unlink()
year = int(record.name)
# Find the first Monday of the year
start_date = datetime(year, 1, 1)
while start_date.weekday() != 0: # Monday is 0 in weekday()
start_date += timedelta(days=1)
# Generate weeks from Monday to Sunday
while start_date.year == year or (start_date - timedelta(days=1)).year == year:
end_date = start_date + timedelta(days=6)
self.env['cwf.timesheet'].create({
'name': f'Week {start_date.strftime("%W")}, {year}',
'week_start_date': start_date.date(),
'week_end_date': end_date.date(),
'cwf_calendar_id': record.id,
})
start_date += timedelta(days=7)
def action_generate_weeks(self):
self.generate_week_period()
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
class CwfTimesheet(models.Model):
_name = 'cwf.timesheet'
_description = 'CWF Weekly Timesheet'
_rec_name = 'name'
name = fields.Char(string='Week Name', required=True)
week_start_date = fields.Date(string='Week Start Date', required=True)
week_end_date = fields.Date(string='Week End Date', required=True)
status = fields.Selection([
('draft', 'Draft'),
('submitted', 'Submitted')
], default='draft', string='Status')
lines = fields.One2many('cwf.timesheet.line','week_id')
cwf_calendar_id = fields.Many2one('cwf.timesheet.calendar')
start_month = fields.Selection(
[(str(i), month_name[i]) for i in range(1, 13)],
string='Start Month',
compute='_compute_months',
store=True
)
end_month = fields.Selection(
[(str(i), month_name[i]) for i in range(1, 13)],
string='End Month',
compute='_compute_months',
store=True
)
@api.depends('week_start_date', 'week_end_date')
def _compute_months(self):
for rec in self:
if rec.week_start_date:
rec.start_month = str(rec.week_start_date.month)
else:
rec.start_month = False
if rec.week_end_date:
rec.end_month = str(rec.week_end_date.month)
else:
rec.end_month = False
@api.depends('name','week_start_date','week_end_date')
def _compute_display_name(self):
for rec in self:
rec.display_name = rec.name if not rec.week_start_date and rec.week_end_date else "%s (%s - %s)"%(rec.name,rec.week_start_date.strftime('%-d %b'), rec.week_end_date.strftime('%-d %b') )
def send_timesheet_update_email(self):
template = self.env.ref('cwf_timesheet.email_template_timesheet_weekly_update')
# Ensure that we have a valid employee email
current_date = fields.Date.from_string(self.week_start_date)
end_date = fields.Date.from_string(self.week_end_date)
if current_date > end_date:
raise UserError('The start date cannot be after the end date.')
# Get all employees in the department
external_group_id = self.env.ref("hr_employee_extended.group_external_user")
users = self.env["res.users"].search([("groups_id", "=", external_group_id.id)])
employees = self.env['hr.employee'].search([('user_id', 'in', users.ids),'|',('doj','=',False),('doj','>=', self.week_start_date)])
print(employees)
# Loop through each day of the week and create timesheet lines for each employee
while current_date <= end_date:
for employee in employees:
existing_record = self.env['cwf.weekly.timesheet'].sudo().search([
('week_id', '=', self.id),
('employee_id', '=', employee.id)
], limit=1)
if not existing_record:
self.env['cwf.timesheet.line'].sudo().create({
'week_id': self.id,
'employee_id': employee.id,
'week_day':current_date,
})
current_date += timedelta(days=1)
self.status = 'submitted'
for employee in employees:
weekly_timesheet_exists = self.env['cwf.weekly.timesheet'].sudo().search([('week_id','=',self.id),('employee_id','=',employee.id)])
if not weekly_timesheet_exists:
weekly_timesheet = self.env['cwf.weekly.timesheet'].sudo().create({
'week_id': self.id,
'employee_id': employee.id,
'status': 'draft'
})
else:
weekly_timesheet = weekly_timesheet_exists
# Generate the URL for the newly created weekly_timesheet
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
record_url = f"{base_url}/web#id={weekly_timesheet.id}&view_type=form&model=cwf.weekly.timesheet"
weekly_timesheet.update_attendance()
if employee.work_email and weekly_timesheet:
email_values = {
'email_to': employee.work_email, # Email body from template
'subject': 'Timesheet Update Notification',
}
body_html = template.body_html.replace(
'https://ftprotech.in/odoo/action-261',
record_url
),
render_ctx = {'employee_name':weekly_timesheet.employee_id.name,'week_from':weekly_timesheet.week_id.week_start_date,'week_to':weekly_timesheet.week_id.week_end_date}
template.with_context(default_body_html=body_html,**render_ctx).send_mail(weekly_timesheet.id, email_values=email_values, force_send=True)
class CwfWeeklyTimesheet(models.Model):
_name = "cwf.weekly.timesheet"
_description = "CWF Weekly Timesheet"
_rec_name = 'employee_id'
def _default_week_id(self):
current_date = fields.Date.today()
timesheet = self.env['cwf.timesheet'].sudo().search([
('week_start_date', '<=', current_date),
('week_end_date', '>=', current_date)
], limit=1)
return timesheet.id if timesheet else False
def _get_week_id_domain(self):
for rec in self:
return [('week_start_date.month_number','=',2)]
pass
month_id = fields.Selection(
[(str(i), month_name[i]) for i in range(1, 13)],
string='Month'
)
week_id = fields.Many2one('cwf.timesheet', 'Week', default=lambda self: self._default_week_id())
employee_id = fields.Many2one('hr.employee', default=lambda self: self.env.user.employee_id.id)
cwf_timesheet_lines = fields.One2many('cwf.timesheet.line' ,'weekly_timesheet')
status = fields.Selection([('draft','Draft'),('submitted','Submitted')], default='draft')
week_start_date = fields.Date(related='week_id.week_start_date')
week_end_date = fields.Date(related='week_id.week_end_date')
@api.onchange('month_id')
def _onchange_month(self):
if self.month_id:
year = self.week_start_date.year if self.week_start_date else fields.Date.today().year
start = date(year, int(self.month_id), 1)
if int(self.month_id) == 12:
end = date(year + 1, 1, 1) - timedelta(days=1)
else:
end = date(year, int(self.month_id) + 1, 1) - timedelta(days=1)
self = self.with_context(month_start=start, month_end=end)
@api.onchange('week_id')
def _onchange_week_id(self):
if self.week_id and self.week_id.week_start_date:
self.month_id = str(self.week_id.week_start_date.month)
@api.constrains('week_id', 'employee_id')
def _check_unique_week_employee(self):
for record in self:
if record.week_id.week_start_date > fields.Date.today():
raise ValidationError(_("You Can't select future week period"))
# Search for existing records with the same week_id and employee_id
existing_record = self.env['cwf.weekly.timesheet'].search([
('week_id', '=', record.week_id.id),
('employee_id', '=', record.employee_id.id)
], limit=1)
# If an existing record is found and it's not the current record (in case of update), raise an error
if existing_record and existing_record.id != record.id:
raise ValidationError("A timesheet for this employee already exists for the selected week.")
def update_attendance(self):
for rec in self:
# Get the week start and end date
week_start_date = rec.week_id.week_start_date
week_end_date = rec.week_id.week_end_date
# Convert start and end dates to datetime objects for proper filtering
week_start_datetime = datetime.combine(week_start_date, datetime.min.time())
week_end_datetime = datetime.combine(week_end_date, datetime.max.time())
# Delete timesheet lines that are outside the week range
rec.cwf_timesheet_lines.filtered(lambda line:
line.week_day < week_start_date or line.week_day > week_end_date
).unlink()
# Search for attendance records that fall within the week period and match the employee
hr_attendance_records = self.env['hr.attendance'].sudo().search([
('check_in', '>=', week_start_datetime),
('check_out', '<=', week_end_datetime),
('employee_id', '=', rec.employee_id.id)
])
# Group the attendance records by date
attendance_by_date = {}
for attendance in hr_attendance_records:
attendance_date = attendance.check_in.date()
if attendance_date not in attendance_by_date:
attendance_by_date[attendance_date] = []
attendance_by_date[attendance_date].append(attendance)
# Get all the dates within the week period
all_week_dates = [week_start_date + timedelta(days=i) for i in
range((week_end_date - week_start_date).days + 1)]
# Create or update timesheet lines for each day in the week
for date in all_week_dates:
# Check if there is attendance for this date
if date in attendance_by_date:
# If there are multiple attendance records, take the earliest check_in and latest check_out
earliest_check_in = min(attendance.check_in for attendance in attendance_by_date[date])
latest_check_out = max(attendance.check_out for attendance in attendance_by_date[date])
if (earliest_check_in + timedelta(hours=5, minutes=30)).date() > date:
earliest_check_in = (datetime.combine(date, datetime.max.time()) - timedelta(hours=5, minutes=30))
if (latest_check_out + timedelta(hours=5, minutes=30)).date() > date:
latest_check_out = (datetime.combine(date, datetime.max.time()) - timedelta(hours=5, minutes=30))
# Check if a timesheet line for this employee, week, and date already exists
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
('week_day', '=', date),
('employee_id', '=', rec.employee_id.id),
('week_id', '=', rec.week_id.id),
('weekly_timesheet', '=', rec.id)
], limit=1)
if existing_timesheet_line:
# If it exists, update the existing record
existing_timesheet_line.write({
'check_in_date': earliest_check_in,
'check_out_date': latest_check_out,
'state_type': 'present',
})
else:
# If it doesn't exist, create a new timesheet line with present state_type
self.env['cwf.timesheet.line'].create({
'weekly_timesheet': rec.id,
'employee_id': rec.employee_id.id,
'week_id': rec.week_id.id,
'week_day': date,
'check_in_date': earliest_check_in,
'check_out_date': latest_check_out,
'state_type': 'present',
})
else:
# If no attendance exists for this date, create a new timesheet line with time_off state_type
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
('week_day', '=', date),
('employee_id', '=', rec.employee_id.id),
('week_id', '=', rec.week_id.id),
('weekly_timesheet', '=', rec.id)
], limit=1)
if not existing_timesheet_line:
if date.weekday() != 5 and date.weekday() != 6:
# If no record exists for this date, create a new timesheet line with time_off state_type
self.env['cwf.timesheet.line'].create({
'weekly_timesheet': rec.id,
'employee_id': rec.employee_id.id,
'week_id': rec.week_id.id,
'week_day': date,
'state_type': 'time_off',
})
def action_submit(self):
for rec in self:
for timesheet in rec.cwf_timesheet_lines:
timesheet.action_submit()
rec.status = 'submitted'
class CwfTimesheetLine(models.Model):
_name = 'cwf.timesheet.line'
_description = 'CWF Weekly Timesheet Lines'
_rec_name = 'employee_id'
weekly_timesheet = fields.Many2one('cwf.weekly.timesheet')
employee_id = fields.Many2one('hr.employee', string='Employee', related='weekly_timesheet.employee_id')
week_id = fields.Many2one('cwf.timesheet', 'Week', related='weekly_timesheet.week_id')
week_day = fields.Date(string='Date')
check_in_date = fields.Datetime(string='Checkin')
check_out_date = fields.Datetime(string='Checkout ')
is_updated = fields.Boolean('Attendance Updated')
state_type = fields.Selection([('draft','Draft'),('holiday', 'Holiday'),('time_off','Time Off'),('half_day','Half Day'),('present','Present')], default='draft', required=True)
@api.constrains('week_day', 'check_in_date', 'check_out_date')
def _check_week_day_and_times(self):
for record in self:
# Ensure week_day is within the week range
if record.week_id:
if record.week_day < record.week_id.week_start_date or record.week_day > record.week_id.week_end_date:
raise ValidationError(
"The selected 'week_day' must be within the range of the week from %s to %s." %
(record.week_id.week_start_date, record.week_id.week_end_date)
)
# Ensure check_in_date and check_out_date are on the selected week_day
if record.check_in_date:
if record.check_in_date.date() != record.week_day:
raise ValidationError(
"The 'check_in_date' must be on the selected Date."
)
if record.check_out_date:
if record.check_out_date.date() != record.week_day:
raise ValidationError(
"The 'check_out_date' must be on the selected Date."
)
def action_submit(self):
if self.state_type == 'draft' or not self.state_type:
raise ValidationError(_('State type should not Draft or Empty'))
if self.state_type not in ['holiday','time_off'] and not (self.check_in_date or self.check_out_date):
raise ValidationError(_('Please enter Check details'))
self._update_attendance()
def _update_attendance(self):
attendance_obj = self.env['hr.attendance']
for record in self:
if record.check_in_date != False and record.check_out_date != False and record.employee_id:
first_check_in = attendance_obj.sudo().search([('check_in', '>=', record.check_in_date.date()),
('check_out', '<=', record.check_out_date.date()),('employee_id','=',record.employee_id.id)],
limit=1, order="check_in")
last_check_out = attendance_obj.sudo().search([('check_in', '>=', record.check_in_date.date()),
('check_out', '<=', record.check_out_date.date()),('employee_id','=',record.employee_id.id)],
limit=1, order="check_out desc")
if first_check_in or last_check_out:
if first_check_in.sudo().check_in != record.check_in_date:
first_check_in.sudo().check_in = record.check_in_date
if last_check_out.sudo().check_out != record.check_out_date:
last_check_out.sudo().check_out = record.check_out_date
else:
attendance_obj.sudo().create({
'employee_id': record.employee_id.id,
'check_in': record.check_in_date - timedelta(hours=5, minutes=30),
'check_out': record.check_out_date - timedelta(hours=5, minutes=30),
})
record.is_updated = True
from odoo import models, fields, api
from odoo.exceptions import ValidationError, UserError
from datetime import datetime, timedelta
import datetime as dt
from odoo import _
from calendar import month_name, month
from datetime import date
class CwfTimesheetYearly(models.Model):
_name = 'cwf.timesheet.calendar'
_description = "CWF Timesheet Calendar"
_rec_name = 'name'
name = fields.Char(string='Year Name', required=True)
week_period = fields.One2many('cwf.timesheet','cwf_calendar_id')
_sql_constraints = [
('unique_year', 'unique(name)', 'The year must be unique!')
]
@api.constrains('name')
def _check_year_format(self):
for record in self:
if not record.name.isdigit() or len(record.name) != 4:
raise ValidationError("Year Name must be a 4-digit number.")
def generate_week_period(self):
for record in self:
record.week_period.unlink()
year = int(record.name)
# Find the first Monday of the year
start_date = datetime(year, 1, 1)
while start_date.weekday() != 0: # Monday is 0 in weekday()
start_date += timedelta(days=1)
# Generate weeks from Monday to Sunday
while start_date.year == year or (start_date - timedelta(days=1)).year == year:
end_date = start_date + timedelta(days=6)
self.env['cwf.timesheet'].create({
'name': f'Week {start_date.strftime("%W")}, {year}',
'week_start_date': start_date.date(),
'week_end_date': end_date.date(),
'cwf_calendar_id': record.id,
})
start_date += timedelta(days=7)
def action_generate_weeks(self):
self.generate_week_period()
return {
'type': 'ir.actions.client',
'tag': 'reload',
}
class CwfTimesheet(models.Model):
_name = 'cwf.timesheet'
_description = 'CWF Weekly Timesheet'
_rec_name = 'name'
name = fields.Char(string='Week Name', required=True)
week_start_date = fields.Date(string='Week Start Date', required=True)
week_end_date = fields.Date(string='Week End Date', required=True)
status = fields.Selection([
('draft', 'Draft'),
('submitted', 'Submitted')
], default='draft', string='Status')
lines = fields.One2many('cwf.timesheet.line','week_id')
cwf_calendar_id = fields.Many2one('cwf.timesheet.calendar')
start_month = fields.Selection(
[(str(i), month_name[i]) for i in range(1, 13)],
string='Start Month',
compute='_compute_months',
store=True
)
end_month = fields.Selection(
[(str(i), month_name[i]) for i in range(1, 13)],
string='End Month',
compute='_compute_months',
store=True
)
@api.depends('week_start_date', 'week_end_date')
def _compute_months(self):
for rec in self:
if rec.week_start_date:
rec.start_month = str(rec.week_start_date.month)
else:
rec.start_month = False
if rec.week_end_date:
rec.end_month = str(rec.week_end_date.month)
else:
rec.end_month = False
@api.depends('name','week_start_date','week_end_date')
def _compute_display_name(self):
for rec in self:
rec.display_name = rec.name if not rec.week_start_date and rec.week_end_date else "%s (%s - %s)"%(rec.name,rec.week_start_date.strftime('%-d %b'), rec.week_end_date.strftime('%-d %b') )
def send_timesheet_update_email(self):
template = self.env.ref('cwf_timesheet.email_template_timesheet_weekly_update')
# Ensure that we have a valid employee email
current_date = fields.Date.from_string(self.week_start_date)
end_date = fields.Date.from_string(self.week_end_date)
if current_date > end_date:
raise UserError('The start date cannot be after the end date.')
# Get all employees in the department
external_group_id = self.env.ref("hr_employee_extended.group_external_user")
users = self.env["res.users"].search([("groups_id", "=", external_group_id.id)])
employees = self.env['hr.employee'].search([('user_id', 'in', users.ids),'|',('doj','=',False),('doj','>=', self.week_start_date)])
# Loop through each day of the week and create timesheet lines for each employee
while current_date <= end_date:
for employee in employees:
existing_record = self.env['cwf.weekly.timesheet'].sudo().search([
('week_id', '=', self.id),
('employee_id', '=', employee.id)
], limit=1)
if not existing_record:
self.env['cwf.timesheet.line'].sudo().create({
'week_id': self.id,
'employee_id': employee.id,
'week_day':current_date,
})
current_date += timedelta(days=1)
self.status = 'submitted'
for employee in employees:
weekly_timesheet_exists = self.env['cwf.weekly.timesheet'].sudo().search([('week_id','=',self.id),('employee_id','=',employee.id)])
if not weekly_timesheet_exists:
weekly_timesheet = self.env['cwf.weekly.timesheet'].sudo().create({
'week_id': self.id,
'employee_id': employee.id,
'status': 'draft'
})
else:
weekly_timesheet = weekly_timesheet_exists
# Generate the URL for the newly created weekly_timesheet
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
record_url = f"{base_url}/web#id={weekly_timesheet.id}&view_type=form&model=cwf.weekly.timesheet"
weekly_timesheet.update_attendance()
if employee.work_email and weekly_timesheet:
email_values = {
'email_to': employee.work_email, # Email body from template
'subject': 'Timesheet Update Notification',
}
body_html = template.body_html.replace(
'https://ftprotech.in/odoo/action-261',
record_url
),
render_ctx = {'employee_name':weekly_timesheet.employee_id.name,'week_from':weekly_timesheet.week_id.week_start_date,'week_to':weekly_timesheet.week_id.week_end_date}
template.with_context(default_body_html=body_html,**render_ctx).send_mail(weekly_timesheet.id, email_values=email_values, force_send=True)
class CwfWeeklyTimesheet(models.Model):
_name = "cwf.weekly.timesheet"
_description = "CWF Weekly Timesheet"
_rec_name = 'employee_id'
def _default_week_id(self):
current_date = fields.Date.today()
timesheet = self.env['cwf.timesheet'].sudo().search([
('week_start_date', '<=', current_date),
('week_end_date', '>=', current_date)
], limit=1)
return timesheet.id if timesheet else False
def _get_week_id_domain(self):
for rec in self:
return [('week_start_date.month_number','=',2)]
pass
month_id = fields.Selection(
[(str(i), month_name[i]) for i in range(1, 13)],
string='Month'
)
week_id = fields.Many2one('cwf.timesheet', 'Week', default=lambda self: self._default_week_id())
employee_id = fields.Many2one('hr.employee', default=lambda self: self.env.user.employee_id.id)
cwf_timesheet_lines = fields.One2many('cwf.timesheet.line' ,'weekly_timesheet')
status = fields.Selection([('draft','Draft'),('submitted','Submitted')], default='draft')
week_start_date = fields.Date(related='week_id.week_start_date')
week_end_date = fields.Date(related='week_id.week_end_date')
@api.onchange('month_id')
def _onchange_month(self):
if self.month_id:
year = self.week_start_date.year if self.week_start_date else fields.Date.today().year
start = date(year, int(self.month_id), 1)
if int(self.month_id) == 12:
end = date(year + 1, 1, 1) - timedelta(days=1)
else:
end = date(year, int(self.month_id) + 1, 1) - timedelta(days=1)
self = self.with_context(month_start=start, month_end=end)
@api.onchange('week_id')
def _onchange_week_id(self):
if self.week_id and self.week_id.week_start_date:
self.month_id = str(self.week_id.week_start_date.month)
@api.constrains('week_id', 'employee_id')
def _check_unique_week_employee(self):
for record in self:
if record.week_id.week_start_date > fields.Date.today():
raise ValidationError(_("You Can't select future week period"))
# Search for existing records with the same week_id and employee_id
existing_record = self.env['cwf.weekly.timesheet'].search([
('week_id', '=', record.week_id.id),
('employee_id', '=', record.employee_id.id)
], limit=1)
# If an existing record is found and it's not the current record (in case of update), raise an error
if existing_record and existing_record.id != record.id:
raise ValidationError("A timesheet for this employee already exists for the selected week.")
def update_attendance(self):
for rec in self:
# Get the week start and end date
week_start_date = rec.week_id.week_start_date
week_end_date = rec.week_id.week_end_date
# Convert start and end dates to datetime objects for proper filtering
week_start_datetime = datetime.combine(week_start_date, datetime.min.time())
week_end_datetime = datetime.combine(week_end_date, datetime.max.time())
# Delete timesheet lines that are outside the week range
rec.cwf_timesheet_lines.filtered(lambda line:
line.week_day < week_start_date or line.week_day > week_end_date
).unlink()
# Search for attendance records that fall within the week period and match the employee
hr_attendance_records = self.env['hr.attendance'].sudo().search([
('check_in', '>=', week_start_datetime),
('check_out', '<=', week_end_datetime),
('employee_id', '=', rec.employee_id.id)
])
# Group the attendance records by date
attendance_by_date = {}
for attendance in hr_attendance_records:
attendance_date = attendance.check_in.date()
if attendance_date not in attendance_by_date:
attendance_by_date[attendance_date] = []
attendance_by_date[attendance_date].append(attendance)
# Get all the dates within the week period
all_week_dates = [week_start_date + timedelta(days=i) for i in
range((week_end_date - week_start_date).days + 1)]
# Create or update timesheet lines for each day in the week
for date in all_week_dates:
# Check if there is attendance for this date
if date in attendance_by_date:
# If there are multiple attendance records, take the earliest check_in and latest check_out
earliest_check_in = min(attendance.check_in for attendance in attendance_by_date[date])
latest_check_out = max(attendance.check_out for attendance in attendance_by_date[date])
if (earliest_check_in + timedelta(hours=5, minutes=30)).date() > date:
earliest_check_in = (datetime.combine(date, datetime.max.time()) - timedelta(hours=5, minutes=30))
if (latest_check_out + timedelta(hours=5, minutes=30)).date() > date:
latest_check_out = (datetime.combine(date, datetime.max.time()) - timedelta(hours=5, minutes=30))
# Check if a timesheet line for this employee, week, and date already exists
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
('week_day', '=', date),
('employee_id', '=', rec.employee_id.id),
('week_id', '=', rec.week_id.id),
('weekly_timesheet', '=', rec.id)
], limit=1)
if existing_timesheet_line:
# If it exists, update the existing record
existing_timesheet_line.write({
'check_in_date': earliest_check_in,
'check_out_date': latest_check_out,
'state_type': 'present',
})
else:
# If it doesn't exist, create a new timesheet line with present state_type
self.env['cwf.timesheet.line'].create({
'weekly_timesheet': rec.id,
'employee_id': rec.employee_id.id,
'week_id': rec.week_id.id,
'week_day': date,
'check_in_date': earliest_check_in,
'check_out_date': latest_check_out,
'state_type': 'present',
})
else:
# If no attendance exists for this date, create a new timesheet line with time_off state_type
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
('week_day', '=', date),
('employee_id', '=', rec.employee_id.id),
('week_id', '=', rec.week_id.id),
('weekly_timesheet', '=', rec.id)
], limit=1)
if not existing_timesheet_line:
if date.weekday() != 5 and date.weekday() != 6:
# If no record exists for this date, create a new timesheet line with time_off state_type
self.env['cwf.timesheet.line'].create({
'weekly_timesheet': rec.id,
'employee_id': rec.employee_id.id,
'week_id': rec.week_id.id,
'week_day': date,
'state_type': 'time_off',
})
def action_submit(self):
for rec in self:
for timesheet in rec.cwf_timesheet_lines:
timesheet.action_submit()
rec.status = 'submitted'
class CwfTimesheetLine(models.Model):
_name = 'cwf.timesheet.line'
_description = 'CWF Weekly Timesheet Lines'
_rec_name = 'employee_id'
weekly_timesheet = fields.Many2one('cwf.weekly.timesheet')
employee_id = fields.Many2one('hr.employee', string='Employee', related='weekly_timesheet.employee_id')
week_id = fields.Many2one('cwf.timesheet', 'Week', related='weekly_timesheet.week_id')
week_day = fields.Date(string='Date')
check_in_date = fields.Datetime(string='Checkin')
check_out_date = fields.Datetime(string='Checkout ')
is_updated = fields.Boolean('Attendance Updated')
state_type = fields.Selection([('draft','Draft'),('holiday', 'Holiday'),('time_off','Time Off'),('half_day','Half Day'),('present','Present')], default='draft', required=True)
@api.constrains('week_day', 'check_in_date', 'check_out_date')
def _check_week_day_and_times(self):
for record in self:
# Ensure week_day is within the week range
if record.week_id:
if record.week_day < record.week_id.week_start_date or record.week_day > record.week_id.week_end_date:
raise ValidationError(
"The selected 'week_day' must be within the range of the week from %s to %s." %
(record.week_id.week_start_date, record.week_id.week_end_date)
)
# Ensure check_in_date and check_out_date are on the selected week_day
if record.check_in_date:
if record.check_in_date.date() != record.week_day:
raise ValidationError(
"The 'check_in_date' must be on the selected Date."
)
if record.check_out_date:
if record.check_out_date.date() != record.week_day:
raise ValidationError(
"The 'check_out_date' must be on the selected Date."
)
def action_submit(self):
if self.state_type == 'draft' or not self.state_type:
raise ValidationError(_('State type should not Draft or Empty'))
if self.state_type not in ['holiday','time_off'] and not (self.check_in_date or self.check_out_date):
raise ValidationError(_('Please enter Check details'))
self._update_attendance()
def _update_attendance(self):
attendance_obj = self.env['hr.attendance']
for record in self:
if record.check_in_date != False and record.check_out_date != False and record.employee_id:
first_check_in = attendance_obj.sudo().search([('check_in', '>=', record.check_in_date.date()),
('check_out', '<=', record.check_out_date.date()),('employee_id','=',record.employee_id.id)],
limit=1, order="check_in")
last_check_out = attendance_obj.sudo().search([('check_in', '>=', record.check_in_date.date()),
('check_out', '<=', record.check_out_date.date()),('employee_id','=',record.employee_id.id)],
limit=1, order="check_out desc")
if first_check_in or last_check_out:
if first_check_in.sudo().check_in != record.check_in_date:
first_check_in.sudo().check_in = record.check_in_date
if last_check_out.sudo().check_out != record.check_out_date:
last_check_out.sudo().check_out = record.check_out_date
else:
attendance_obj.sudo().create({
'employee_id': record.employee_id.id,
'check_in': record.check_in_date - timedelta(hours=5, minutes=30),
'check_out': record.check_out_date - timedelta(hours=5, minutes=30),
})
record.is_updated = True

View File

@ -1,15 +1,15 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_cwf_timesheet_user,access.cwf.timesheet,model_cwf_timesheet,base.group_user,1,0,0,0
access_cwf_timesheet_manager,access.cwf.timesheet,model_cwf_timesheet,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_calendar,cwf_timesheet_calendar,model_cwf_timesheet_calendar,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_calendar_user,cwf_timesheet_calendar_user,model_cwf_timesheet_calendar,base.group_user,1,0,0,0
access_cwf_timesheet_line_manager,access.cwf.timesheet.line.manager,model_cwf_timesheet_line,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_line_user,access.cwf.timesheet.line,model_cwf_timesheet_line,hr_employee_extended.group_external_user,1,1,1,1
access_cwf_weekly_timesheet_manager,cwf.weekly.timesheet.manager access,model_cwf_weekly_timesheet,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_weekly_timesheet_user,cwf.weekly.timesheet access,model_cwf_weekly_timesheet,hr_employee_extended.group_external_user,1,1,1,0
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_cwf_timesheet_user,access.cwf.timesheet,model_cwf_timesheet,base.group_user,1,0,0,0
access_cwf_timesheet_manager,access.cwf.timesheet,model_cwf_timesheet,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_calendar,cwf_timesheet_calendar,model_cwf_timesheet_calendar,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_calendar_user,cwf_timesheet_calendar_user,model_cwf_timesheet_calendar,base.group_user,1,0,0,0
access_cwf_timesheet_line_manager,access.cwf.timesheet.line.manager,model_cwf_timesheet_line,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_timesheet_line_user,access.cwf.timesheet.line,model_cwf_timesheet_line,hr_employee_extended.group_external_user,1,1,1,1
access_cwf_weekly_timesheet_manager,cwf.weekly.timesheet.manager access,model_cwf_weekly_timesheet,hr_attendance.group_hr_attendance_manager,1,1,1,1
access_cwf_weekly_timesheet_user,cwf.weekly.timesheet access,model_cwf_weekly_timesheet,hr_employee_extended.group_external_user,1,1,1,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_cwf_timesheet_user access.cwf.timesheet model_cwf_timesheet base.group_user 1 0 0 0
3 access_cwf_timesheet_manager access.cwf.timesheet model_cwf_timesheet hr_attendance.group_hr_attendance_manager 1 1 1 1
4 access_cwf_timesheet_calendar cwf_timesheet_calendar model_cwf_timesheet_calendar hr_attendance.group_hr_attendance_manager 1 1 1 1
5 access_cwf_timesheet_calendar_user cwf_timesheet_calendar_user model_cwf_timesheet_calendar base.group_user 1 0 0 0
6 access_cwf_timesheet_line_manager access.cwf.timesheet.line.manager model_cwf_timesheet_line hr_attendance.group_hr_attendance_manager 1 1 1 1
7 access_cwf_timesheet_line_user access.cwf.timesheet.line model_cwf_timesheet_line hr_employee_extended.group_external_user 1 1 1 1
8 access_cwf_weekly_timesheet_manager cwf.weekly.timesheet.manager access model_cwf_weekly_timesheet hr_attendance.group_hr_attendance_manager 1 1 1 1
9 access_cwf_weekly_timesheet_user cwf.weekly.timesheet access model_cwf_weekly_timesheet hr_employee_extended.group_external_user 1 1 1 0
10
11
12
13
14
15

View File

@ -1,36 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data noupdate="0">
<record id="cwf_weekly_timesheet_user_rule" model="ir.rule">
<field name="name">CWF Weekly Timesheet User Rule</field>
<field ref="cwf_timesheet.model_cwf_weekly_timesheet" name="model_id"/>
<field name="domain_force">[('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_employee_extended.group_external_user'))]"/>
</record>
<record id="cwf_timesheet_line_user_rule" model="ir.rule">
<field name="name">CWF Timesheet Line User Rule</field>
<field ref="cwf_timesheet.model_cwf_timesheet_line" name="model_id"/>
<field name="domain_force">[('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_employee_extended.group_external_user'))]"/>
</record>
<record id="cwf_weekly_timesheet_manager_rule" model="ir.rule">
<field name="name">CWF Weekly Timesheet manager Rule</field>
<field ref="cwf_timesheet.model_cwf_weekly_timesheet" name="model_id"/>
<field name="domain_force">['|',('employee_id.user_id.id','!=',user.id),('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_attendance.group_hr_attendance_manager'))]"/>
</record>
<record id="cwf_timesheet_line_manager_rule" model="ir.rule">
<field name="name">CWF Timesheet Line manager Rule</field>
<field ref="cwf_timesheet.model_cwf_timesheet_line" name="model_id"/>
<field name="domain_force">['|',('employee_id.user_id.id','!=',user.id),('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_attendance.group_hr_attendance_manager'))]"/>
</record>
</data>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<data noupdate="0">
<record id="cwf_weekly_timesheet_user_rule" model="ir.rule">
<field name="name">CWF Weekly Timesheet User Rule</field>
<field ref="cwf_timesheet.model_cwf_weekly_timesheet" name="model_id"/>
<field name="domain_force">[('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_employee_extended.group_external_user'))]"/>
</record>
<record id="cwf_timesheet_line_user_rule" model="ir.rule">
<field name="name">CWF Timesheet Line User Rule</field>
<field ref="cwf_timesheet.model_cwf_timesheet_line" name="model_id"/>
<field name="domain_force">[('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_employee_extended.group_external_user'))]"/>
</record>
<record id="cwf_weekly_timesheet_manager_rule" model="ir.rule">
<field name="name">CWF Weekly Timesheet manager Rule</field>
<field ref="cwf_timesheet.model_cwf_weekly_timesheet" name="model_id"/>
<field name="domain_force">['|',('employee_id.user_id.id','!=',user.id),('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_attendance.group_hr_attendance_manager'))]"/>
</record>
<record id="cwf_timesheet_line_manager_rule" model="ir.rule">
<field name="name">CWF Timesheet Line manager Rule</field>
<field ref="cwf_timesheet.model_cwf_timesheet_line" name="model_id"/>
<field name="domain_force">['|',('employee_id.user_id.id','!=',user.id),('employee_id.user_id.id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('hr_attendance.group_hr_attendance_manager'))]"/>
</record>
</data>
</odoo>

View File

@ -1,54 +1,54 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { NetflixProfileContainer } from "@hr_emp_dashboard/js/profile_component";
import { user } from "@web/core/user";
// Apply patch to NetflixProfileContainer prototype
patch(NetflixProfileContainer.prototype, {
/**
* @override
*/
setup() {
// Call parent setup method
super.setup(...arguments);
// Log the department of the logged-in employee (check if data is available)
// if (this.state && this.state.login_employee) {
// console.log(this.state.login_employee['department_id']);
// } else {
// console.error('Employee or department data is unavailable.');
// }
},
/**
* Override the hr_timesheets method
*/
async hr_timesheets() {
const isExternalUser = await user.hasGroup("hr_employee_extended.group_external_user");
// Check the department of the logged-in employee
console.log(isExternalUser);
console.log("is external user");
debugger;
if (isExternalUser && this.state.login_employee.department_id) {
console.log("hello external");
// If the department is 'CWF', perform the action to open the timesheets
this.action.doAction({
name: "Timesheets",
type: 'ir.actions.act_window',
res_model: 'cwf.timesheet.line', // Ensure this model exists
view_mode: 'list,form',
views: [[false, 'list'], [false, 'form']],
context: {
'search_default_week_id': true,
},
domain: [['employee_id.user_id','=', this.props.action.context.user_id]],
target: 'current',
});
} else {
// If not 'CWF', call the base functionality
return super.hr_timesheets();
}
},
});
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { NetflixProfileContainer } from "@hr_emp_dashboard/js/profile_component";
import { user } from "@web/core/user";
// Apply patch to NetflixProfileContainer prototype
patch(NetflixProfileContainer.prototype, {
/**
* @override
*/
setup() {
// Call parent setup method
super.setup(...arguments);
// Log the department of the logged-in employee (check if data is available)
// if (this.state && this.state.login_employee) {
// console.log(this.state.login_employee['department_id']);
// } else {
// console.error('Employee or department data is unavailable.');
// }
},
/**
* Override the hr_timesheets method
*/
async hr_timesheets() {
const isExternalUser = await user.hasGroup("hr_employee_extended.group_external_user");
// Check the department of the logged-in employee
console.log(isExternalUser);
console.log("is external user");
debugger;
if (isExternalUser && this.state.login_employee.department_id) {
console.log("hello external");
// If the department is 'CWF', perform the action to open the timesheets
this.action.doAction({
name: "Timesheets",
type: 'ir.actions.act_window',
res_model: 'cwf.timesheet.line', // Ensure this model exists
view_mode: 'list,form',
views: [[false, 'list'], [false, 'form']],
context: {
'search_default_week_id': true,
},
domain: [['employee_id.user_id','=', this.props.action.context.user_id]],
target: 'current',
});
} else {
// If not 'CWF', call the base functionality
return super.hr_timesheets();
}
},
});

View File

@ -1,27 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="timesheet_form">
<div class="container">
<h2>Weekly Timesheet</h2>
<form class="timesheet-form">
<div class="form-group">
<label for="employee">Employee</label>
<input t-att-value="state.employee" type="text" id="employee" class="form-control"/>
</div>
<div class="form-group">
<label for="weekStartDate">Week Start Date</label>
<input type="datetime-local" t-model="state.weekStartDate" id="weekStartDate" class="form-control"/>
</div>
<div class="form-group">
<label for="weekEndDate">Week End Date</label>
<input type="datetime-local" t-model="state.weekEndDate" id="weekEndDate" class="form-control"/>
</div>
<div class="form-group">
<label for="totalHours">Total Hours Worked</label>
<input type="number" t-model="state.totalHours" id="totalHours" class="form-control" min="0"/>
</div>
<button type="button" t-on-click="submitForm" class="btn btn-primary">Submit Timesheet</button>
</form>
</div>
</t>
</templates>
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="timesheet_form">
<div class="container">
<h2>Weekly Timesheet</h2>
<form class="timesheet-form">
<div class="form-group">
<label for="employee">Employee</label>
<input t-att-value="state.employee" type="text" id="employee" class="form-control"/>
</div>
<div class="form-group">
<label for="weekStartDate">Week Start Date</label>
<input type="datetime-local" t-model="state.weekStartDate" id="weekStartDate" class="form-control"/>
</div>
<div class="form-group">
<label for="weekEndDate">Week End Date</label>
<input type="datetime-local" t-model="state.weekEndDate" id="weekEndDate" class="form-control"/>
</div>
<div class="form-group">
<label for="totalHours">Total Hours Worked</label>
<input type="number" t-model="state.totalHours" id="totalHours" class="form-control" min="0"/>
</div>
<button type="button" t-on-click="submitForm" class="btn btn-primary">Submit Timesheet</button>
</form>
</div>
</t>
</templates>

View File

@ -1,104 +1,104 @@
<odoo>
<record id="view_cwf_timesheet_calendar_form" model="ir.ui.view">
<field name="name">cwf.timesheet.calendar.form</field>
<field name="model">cwf.timesheet.calendar</field>
<field name="arch" type="xml">
<form string="CWF Timesheet Calendar">
<sheet>
<group>
<field name="name"/>
<button name="action_generate_weeks" string="Generate Week Periods" type="object"
class="oe_highlight"/>
</group>
<notebook>
<page string="Weeks">
<field name="week_period" context="{'order': 'week_start_date asc'}">
<list editable="bottom" decoration-success="status == 'submitted'">
<field name="name"/>
<field name="week_start_date"/>
<field name="week_end_date"/>
<field name="start_month" column_invisible="1"/>
<field name="end_month" column_invisible="1"/>
<field name="status"/>
<button name="send_timesheet_update_email" string="Send Update Email"
invisible="status == 'submitted'" type="object"
confirm="You can't revert this action. Please check twice before Submitting?"
class="oe_highlight"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_cwf_timesheet_calendar_list" model="ir.ui.view">
<field name="name">cwf.timesheet.calendar.list</field>
<field name="model">cwf.timesheet.calendar</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
</list>
</field>
</record>
<record id="action_cwf_timesheet_calendar" model="ir.actions.act_window">
<field name="name">CWF Timesheet Calendar</field>
<field name="res_model">cwf.timesheet.calendar</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_cwf_attendance_attendance" name="CWF" parent="hr_attendance.menu_hr_attendance_root"
sequence="6" groups="hr_employee_extended.group_external_user,hr_attendance.group_hr_attendance_manager"/>
<menuitem id="menu_timesheet_calendar_form" name="CWF Timesheet Calendar" parent="menu_cwf_attendance_attendance"
action="cwf_timesheet.action_cwf_timesheet_calendar" groups="hr_attendance.group_hr_attendance_manager"/>
<record id="view_timesheet_form" model="ir.ui.view">
<field name="name">cwf.timesheet.form</field>
<field name="model">cwf.timesheet</field>
<field name="arch" type="xml">
<form string="Timesheet">
<header>
<button name="send_timesheet_update_email" string="Send Email" type="object"
invisible="status != 'draft'"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1>
<field name="name" class="oe_inline" readonly="status != 'draft'"/>
</h1>
</div>
<group>
<!-- Section for Employee and Date Range -->
<group>
<field name="week_start_date" readonly="status != 'draft'"/>
<field name="week_end_date" readonly="status != 'draft'"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_timesheet_list" model="ir.ui.view">
<field name="name">cwf.timesheet.list</field>
<field name="model">cwf.timesheet</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="week_start_date"/>
<field name="week_end_date"/>
</list>
</field>
</record>
<record id="action_cwf_timesheet" model="ir.actions.act_window">
<field name="name">CWF Timesheet</field>
<field name="res_model">cwf.timesheet</field>
<field name="view_mode">list,form</field>
</record>
</odoo>
<odoo>
<record id="view_cwf_timesheet_calendar_form" model="ir.ui.view">
<field name="name">cwf.timesheet.calendar.form</field>
<field name="model">cwf.timesheet.calendar</field>
<field name="arch" type="xml">
<form string="CWF Timesheet Calendar">
<sheet>
<group>
<field name="name"/>
<button name="action_generate_weeks" string="Generate Week Periods" type="object"
class="oe_highlight"/>
</group>
<notebook>
<page string="Weeks">
<field name="week_period" context="{'order': 'week_start_date asc'}">
<list editable="bottom" decoration-success="status == 'submitted'">
<field name="name"/>
<field name="week_start_date"/>
<field name="week_end_date"/>
<field name="start_month" column_invisible="1"/>
<field name="end_month" column_invisible="1"/>
<field name="status"/>
<button name="send_timesheet_update_email" string="Send Update Email"
invisible="status == 'submitted'" type="object"
confirm="You can't revert this action. Please check twice before Submitting?"
class="oe_highlight"/>
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_cwf_timesheet_calendar_list" model="ir.ui.view">
<field name="name">cwf.timesheet.calendar.list</field>
<field name="model">cwf.timesheet.calendar</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
</list>
</field>
</record>
<record id="action_cwf_timesheet_calendar" model="ir.actions.act_window">
<field name="name">CWF Timesheet Calendar</field>
<field name="res_model">cwf.timesheet.calendar</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_cwf_attendance_attendance" name="CWF" parent="hr_attendance.menu_hr_attendance_root"
sequence="6" groups="hr_employee_extended.group_external_user,hr_attendance.group_hr_attendance_manager"/>
<menuitem id="menu_timesheet_calendar_form" name="CWF Timesheet Calendar" parent="menu_cwf_attendance_attendance"
action="cwf_timesheet.action_cwf_timesheet_calendar" groups="hr_attendance.group_hr_attendance_manager"/>
<record id="view_timesheet_form" model="ir.ui.view">
<field name="name">cwf.timesheet.form</field>
<field name="model">cwf.timesheet</field>
<field name="arch" type="xml">
<form string="Timesheet">
<header>
<button name="send_timesheet_update_email" string="Send Email" type="object"
invisible="status != 'draft'"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1>
<field name="name" class="oe_inline" readonly="status != 'draft'"/>
</h1>
</div>
<group>
<!-- Section for Employee and Date Range -->
<group>
<field name="week_start_date" readonly="status != 'draft'"/>
<field name="week_end_date" readonly="status != 'draft'"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_timesheet_list" model="ir.ui.view">
<field name="name">cwf.timesheet.list</field>
<field name="model">cwf.timesheet</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="week_start_date"/>
<field name="week_end_date"/>
</list>
</field>
</record>
<record id="action_cwf_timesheet" model="ir.actions.act_window">
<field name="name">CWF Timesheet</field>
<field name="res_model">cwf.timesheet</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@ -1,158 +1,158 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_cwf_weekly_timesheet_form" model="ir.ui.view">
<field name="name">cwf.weekly.timesheet.form</field>
<field name="model">cwf.weekly.timesheet</field>
<field name="arch" type="xml">
<form string="CWF Weekly Timesheet">
<header>
<button name="update_attendance" string="Update"
type="object" class="oe_highlight" invisible="status != 'draft'"/>
<button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"
class="oe_highlight" invisible="status != 'draft'"/>
<field name="status" readonly="1" widget="statusbar"/>
</header>
<sheet>
<group>
<field name="month_id"/>
<field name="week_id" readonly="0" domain="['|',('start_month','=',month_id),('end_month','=',month_id)]"/>
<field name="employee_id" readonly="0" groups="hr_attendance.group_hr_attendance_manager"/>
<field name="employee_id" readonly="1" groups="hr_employee_extended.group_external_user"/>
<label for="week_start_date" string="Dates"/>
<div class="o_row">
<field name="week_start_date" widget="daterange" options="{'end_date_field.month()': 'week_end_date'}"/>
<field name="week_end_date" invisible="1"/>
</div>
</group>
<notebook>
<page string="Timesheet Lines">
<field name="cwf_timesheet_lines">
<list editable="bottom">
<field name="employee_id"/>
<field name="week_day"/>
<field name="check_in_date"/>
<field name="check_out_date"/>
<field name="state_type"/>
<!-- <button name="action_submit" string="Submit" type="object"-->
<!-- confirm="Are you sure you want to submit?" class="oe_highlight"/>-->
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_cwf_weekly_timesheet_list" model="ir.ui.view">
<field name="name">cwf.weekly.timesheet.list</field>
<field name="model">cwf.weekly.timesheet</field>
<field name="arch" type="xml">
<list>
<field name="week_id"/>
<field name="employee_id"/>
<field name="status"/>
<!-- <button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"-->
<!-- class="oe_highlight"/>-->
</list>
</field>
</record>
<record id="view_cwf_weekly_timesheet_search" model="ir.ui.view">
<field name="name">cwf.weekly.timesheet.search</field>
<field name="model">cwf.weekly.timesheet</field>
<field name="arch" type="xml">
<search>
<!-- Search by Week ID -->
<field name="week_id"/>
<!-- Search by Employee -->
<field name="employee_id"/>
<!-- Search by Status -->
<field name="status"/>
<!-- Optional: Add custom filters if needed -->
<filter string="Draft" name="filter_draft" domain="[('status','=','draft')]"/>
<filter string="Submitted" name="filter_submitted" domain="[('status','=','submit')]"/>
<group expand="0" string="Group By">
<filter string="Week" name="by_week_id" domain="[]" context="{'group_by':'week_id'}"/>
<separator/>
<filter string="Employee" name="by_employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
<filter string="Status" name="state_type" domain="[]" context="{'group_by': 'status'}"/>
</group>
</search>
</field>
</record>
<record id="action_cwf_weekly_timesheet" model="ir.actions.act_window">
<field name="name">CWF Weekly Timesheet</field>
<field name="res_model">cwf.weekly.timesheet</field>
<field name="view_mode">list,form</field>
<field name="context">
{
"search_default_by_week_id": 1,
"search_default_by_employee_id": 2
}
</field>
<field name="search_view_id" ref="cwf_timesheet.view_cwf_weekly_timesheet_search"/>
</record>
<record id="view_cwf_timesheet_line_list" model="ir.ui.view">
<field name="name">cwf.timesheet.line.list</field>
<field name="model">cwf.timesheet.line</field>
<field name="arch" type="xml">
<list editable="bottom" create="0" delete="0" decoration-success="is_updated == True">
<field name="employee_id" readonly="1" force_save="1"/>
<field name="week_day" readonly="1" force_save="1"/>
<field name="check_in_date" readonly="is_updated == True"/>
<field name="check_out_date" readonly="is_updated == True"/>
<field name="state_type" readonly="is_updated == True"/>
<!-- <button name="action_submit" type="object" string="Submit" class="btn btn-outline-primary"-->
<!-- invisible="is_updated == True"/>-->
</list>
</field>
</record>
<record id="view_cwf_timesheet_line_search" model="ir.ui.view">
<field name="name">cwf.timesheet.line.search</field>
<field name="model">cwf.timesheet.line</field>
<field name="arch" type="xml">
<search string="Timesheets">
<field name="employee_id"/>
<field name="week_id"/>
<field name="week_day"/>
<field name="check_in_date"/>
<field name="check_out_date"/>
<field name="state_type"/>
<group expand="0" string="Group By">
<filter string="Employee" name="by_employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
<separator/>
<filter string="Week" name="by_week_id" domain="[]" context="{'group_by':'week_id'}"/>
<filter string="Status" name="state_type" domain="[]" context="{'group_by': 'state_type'}"/>
</group>
</search>
</field>
</record>
<record id="action_cwf_timesheet_line" model="ir.actions.act_window">
<field name="name">CWF Timesheet Lines</field>
<field name="res_model">cwf.timesheet.line</field>
<field name="view_mode">list</field>
<field name="context">{"search_default_by_week_id": 1, "search_default_by_employee_id": 1}</field>
<field name="search_view_id" ref="cwf_timesheet.view_cwf_timesheet_line_search"/>
</record>
<menuitem id="menu_timesheet_form" name="CWF Weekly Timesheet "
parent="menu_cwf_attendance_attendance" action="action_cwf_weekly_timesheet" groups="hr_employee_extended.group_external_user,hr_attendance.group_hr_attendance_manager"/>
<menuitem id="menu_timesheet_form_line" name="CWF Timesheet Lines"
parent="menu_cwf_attendance_attendance" action="action_cwf_timesheet_line" groups="hr_employee_extended.group_external_user,hr_attendance.group_hr_attendance_manager"/>
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_cwf_weekly_timesheet_form" model="ir.ui.view">
<field name="name">cwf.weekly.timesheet.form</field>
<field name="model">cwf.weekly.timesheet</field>
<field name="arch" type="xml">
<form string="CWF Weekly Timesheet">
<header>
<button name="update_attendance" string="Update"
type="object" class="oe_highlight" invisible="status != 'draft'"/>
<button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"
class="oe_highlight" invisible="status != 'draft'"/>
<field name="status" readonly="1" widget="statusbar"/>
</header>
<sheet>
<group>
<field name="month_id"/>
<field name="week_id" readonly="0" domain="['|',('start_month','=',month_id),('end_month','=',month_id)]"/>
<field name="employee_id" readonly="0" groups="hr_attendance.group_hr_attendance_manager"/>
<field name="employee_id" readonly="1" groups="hr_employee_extended.group_external_user"/>
<label for="week_start_date" string="Dates"/>
<div class="o_row">
<field name="week_start_date" widget="daterange" options="{'end_date_field.month()': 'week_end_date'}"/>
<field name="week_end_date" invisible="1"/>
</div>
</group>
<notebook>
<page string="Timesheet Lines">
<field name="cwf_timesheet_lines">
<list editable="bottom">
<field name="employee_id"/>
<field name="week_day"/>
<field name="check_in_date"/>
<field name="check_out_date"/>
<field name="state_type"/>
<!-- <button name="action_submit" string="Submit" type="object"-->
<!-- confirm="Are you sure you want to submit?" class="oe_highlight"/>-->
</list>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_cwf_weekly_timesheet_list" model="ir.ui.view">
<field name="name">cwf.weekly.timesheet.list</field>
<field name="model">cwf.weekly.timesheet</field>
<field name="arch" type="xml">
<list>
<field name="week_id"/>
<field name="employee_id"/>
<field name="status"/>
<!-- <button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"-->
<!-- class="oe_highlight"/>-->
</list>
</field>
</record>
<record id="view_cwf_weekly_timesheet_search" model="ir.ui.view">
<field name="name">cwf.weekly.timesheet.search</field>
<field name="model">cwf.weekly.timesheet</field>
<field name="arch" type="xml">
<search>
<!-- Search by Week ID -->
<field name="week_id"/>
<!-- Search by Employee -->
<field name="employee_id"/>
<!-- Search by Status -->
<field name="status"/>
<!-- Optional: Add custom filters if needed -->
<filter string="Draft" name="filter_draft" domain="[('status','=','draft')]"/>
<filter string="Submitted" name="filter_submitted" domain="[('status','=','submit')]"/>
<group expand="0" string="Group By">
<filter string="Week" name="by_week_id" domain="[]" context="{'group_by':'week_id'}"/>
<separator/>
<filter string="Employee" name="by_employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
<filter string="Status" name="state_type" domain="[]" context="{'group_by': 'status'}"/>
</group>
</search>
</field>
</record>
<record id="action_cwf_weekly_timesheet" model="ir.actions.act_window">
<field name="name">CWF Weekly Timesheet</field>
<field name="res_model">cwf.weekly.timesheet</field>
<field name="view_mode">list,form</field>
<field name="context">
{
"search_default_by_week_id": 1,
"search_default_by_employee_id": 2
}
</field>
<field name="search_view_id" ref="cwf_timesheet.view_cwf_weekly_timesheet_search"/>
</record>
<record id="view_cwf_timesheet_line_list" model="ir.ui.view">
<field name="name">cwf.timesheet.line.list</field>
<field name="model">cwf.timesheet.line</field>
<field name="arch" type="xml">
<list editable="bottom" create="0" delete="0" decoration-success="is_updated == True">
<field name="employee_id" readonly="1" force_save="1"/>
<field name="week_day" readonly="1" force_save="1"/>
<field name="check_in_date" readonly="is_updated == True"/>
<field name="check_out_date" readonly="is_updated == True"/>
<field name="state_type" readonly="is_updated == True"/>
<!-- <button name="action_submit" type="object" string="Submit" class="btn btn-outline-primary"-->
<!-- invisible="is_updated == True"/>-->
</list>
</field>
</record>
<record id="view_cwf_timesheet_line_search" model="ir.ui.view">
<field name="name">cwf.timesheet.line.search</field>
<field name="model">cwf.timesheet.line</field>
<field name="arch" type="xml">
<search string="Timesheets">
<field name="employee_id"/>
<field name="week_id"/>
<field name="week_day"/>
<field name="check_in_date"/>
<field name="check_out_date"/>
<field name="state_type"/>
<group expand="0" string="Group By">
<filter string="Employee" name="by_employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
<separator/>
<filter string="Week" name="by_week_id" domain="[]" context="{'group_by':'week_id'}"/>
<filter string="Status" name="state_type" domain="[]" context="{'group_by': 'state_type'}"/>
</group>
</search>
</field>
</record>
<record id="action_cwf_timesheet_line" model="ir.actions.act_window">
<field name="name">CWF Timesheet Lines</field>
<field name="res_model">cwf.timesheet.line</field>
<field name="view_mode">list</field>
<field name="context">{"search_default_by_week_id": 1, "search_default_by_employee_id": 1}</field>
<field name="search_view_id" ref="cwf_timesheet.view_cwf_timesheet_line_search"/>
</record>
<menuitem id="menu_timesheet_form" name="CWF Weekly Timesheet "
parent="menu_cwf_attendance_attendance" action="action_cwf_weekly_timesheet" groups="hr_employee_extended.group_external_user,hr_attendance.group_hr_attendance_manager"/>
<menuitem id="menu_timesheet_form_line" name="CWF Timesheet Lines"
parent="menu_cwf_attendance_attendance" action="action_cwf_timesheet_line" groups="hr_employee_extended.group_external_user,hr_attendance.group_hr_attendance_manager"/>
</odoo>

View File

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models

View File

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Disciplinary',
'version': '1.0.0',
'category': 'Apps',
'summary': 'Disciplinary',
'description': 'Employee Disciplinary',
'sequence': '10',
'author': '',
'company': 'FTPROTECH',
'website': 'https://www.ftprotech.in',
'depends': ['mail', 'hr', 'base', 'website_hr_recruitment', 'contacts', 'point_of_sale'],
'demo': [],
'data': [
'data/sequence.xml',
'security/ir.model.access.csv',
'views/disciplinary_view.xml',
'views/employee_displance.xml',
'views/mistake_type_views.xml',
'views/incident_sub_type.xml',
'views/disciplinary_complaint_type.xml',
],
'installable': True,
'application': False,
'auto_install': False,
'license': 'LGPL-3',
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<data noupdate="1">
<record id="incident_report_sequence" model="ir.sequence">
<field name="name">Employee Disciplinary</field>
<field name="code">employee.disciplinary</field>
<field name="prefix">IR</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
<record id="manage_incident_report_sequence" model="ir.sequence">
<field name="name">Manage Incident</field>
<field name="code">manage.incident</field>
<field name="prefix">MI</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
<record id="seq_employee_disciplinary_sequence" model="ir.sequence">
<field name="name">Disciplinary Sequence</field>
<field name="code">hr.employee.sequence</field>
<field name="prefix">ED</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>

View File

@ -0,0 +1,2 @@
from . import disciplinary
from . import employee_displane

View File

@ -0,0 +1,188 @@
from datetime import datetime, date
from odoo import fields, models, api
#
# class NameChangeHrEmployee(models.Model):
# _inherit = "hr.employee"
#
# employee_name_ids1 = fields.One2many('employee.disciplinary', 'disp_name')
# employee_self_service_line_ids = fields.One2many('manage.incident', 'emp_incident', domain=[('state', '=', 'closed')])
#
# def name_get(self):
# result = []
# for record in self:
# if self.env.context.get('new_custom_name', False):
# result.append((record.id, "{} - {}".format(record.name, record.identification_id)))
# else:
# return super(NameChangeHrEmployee, self).name_get()
# return result
class EmployeeDisciplinary(models.Model):
_name = 'employee.disciplinary'
_inherit = ['mail.thread', 'mail.activity.mixin']
_rec_name = 'incident_type'
incident_date = fields.Datetime(string='Incident Date & Time', tracking=True, default=datetime.now(), required=True)
incident_type = fields.Many2one('incident.employee', string='Incident Type', tracking=True, required=True)
incident_sub_type = fields.Many2many('incident.sub.employee', string='Incident Sub Type', tracking=True,
required=True)
incident_details = fields.Char(string='Incident Details', tracking=True, required=True)
seized_items = fields.Char(string='Seized Items', tracking=True)
incident_summary = fields.Text(string='Incident Summary', tracking=True, required=True)
attach = fields.Many2many('ir.attachment', string='Attachments', tracking=True)
emp_many_disp = fields.Many2many('hr.employee', 'new_custom_table', string='Employees Involved in the Incident',
tracking=True, required=True)
date_action = fields.Date(string='Date')
employee = fields.Many2one('manage.incident')
employee_code = fields.Many2one("hr.employee", string="Employee Name", required=True)
employee_name = fields.Char(related="employee_code.identification_id")
disp_name = fields.Many2one('hr.employee')
@api.onchange('incident_type')
def return_incident_sub_type(self):
print(self.incident_type.sub_type)
listed = []
for recs in self.incident_type.sub_type:
listed.append(recs.id)
return {'domain': {'incident_sub_type': [('id', 'in', listed)]}}
@api.constrains('incident_type')
def create_manage_incidents(self):
for rec in self:
print('created')
self.env['manage.incident'].create({
'employee_disciplinary_id': rec.id,
})
@api.constrains('employee')
def holds_hr_employee(self):
for rec in self:
rec.disp_name = rec.employee.employee_code_list1
class IncidentEmployee(models.Model):
_name = 'incident.employee'
name = fields.Char(string='Incident')
sub_type = fields.Many2many('incident.sub.employee', string='Sub type')
class IncidentSubEmployee(models.Model):
_name = 'incident.sub.employee'
name = fields.Char(string='Incident Sub')
class DisciplinaryMistakeType(models.Model):
_name = 'disciplinary.mistake.type'
_description = 'Disciplinary Mistake Type'
name = fields.Char(string="Mistake Type", required=True)
class IncidentSubEmployee(models.Model):
_name = 'incident.sub.employee'
_description = 'Incident Sub Type'
name = fields.Char(string="Incident Sub Type", required=True)
class EmployeeDisciplinaryLines(models.Model):
_name = 'employee.disciplinary.line'
_rec_name = 'hr_emp_many'
emp_many = fields.Many2one('employee.disciplinary', string='Employee Disp')
hr_emp_many = fields.Many2one('hr.employee', string='Employee Number')
hr_emp_many_name = fields.Char(related='hr_emp_many.name', string='Employee Name')
class ManageIncident(models.Model):
_name = 'manage.incident'
_inherit = ['mail.thread', 'mail.activity.mixin']
# employee_name_ids = fields.One2many('employee.disciplinary','employee',string="Employee Name")
employee_disciplinary_id = fields.Many2one("employee.disciplinary", string="Employee Disp")
employee_code_list1 = fields.Many2many("hr.employee", string="Employees Involved in the Incident",
related='employee_disciplinary_id.emp_many_disp', tracking=True)
incident_dat = fields.Datetime(related='employee_disciplinary_id.incident_date', string='Incident Date & Time',
tracking=True)
employee_by_code = fields.Many2one(related='employee_disciplinary_id.employee_code',
string="Reported By Employee Name")
incident_sum = fields.Text(related='employee_disciplinary_id.incident_summary', string='Incident Summary',
tracking=True)
incident_typ = fields.Many2one(related='employee_disciplinary_id.incident_type', string="Incident Type",
tracking=True)
incident_sub_typ = fields.Many2many(related='employee_disciplinary_id.incident_sub_type',
string="Incident Sub Type", tracking=True)
# corrective_action_emp_id = fields.Many2one(related='employee_disciplinary_id.corrective_action_id',
# string="Corrective Action", tracking=True)
state = fields.Selection(([
('pending_inquiry', 'Pending Inquiry'),
('in_progress', 'In Process'),
('closed', 'Closed')
]), string="Status", default='pending_inquiry', tracking=True)
emp_incident = fields.Many2one('hr.employee')
employee_inquiry = fields.One2many('manage.incident.line', 'employee_inquiry_state')
def button_in_progress(self):
self.state = 'in_progress'
# def button_closed(self):
# for rec in self:
# rec.state = 'closed'
def button_closed(self):
for rec in self:
rec.state = 'closed'
update_into_employee = rec.env['hr.employee'].search([('id', '=', rec.employee_code_list1.id)])
records = {
}
if records:
update_into_employee.write(records)
print('triggered 2')
class CorrectiveActions(models.Model):
_name = "corrective.actions"
name = fields.Char(string="Name")
class ManageIncidentLine(models.Model):
_name = 'manage.incident.line'
_inherit = ['mail.thread']
_description = 'Manage Incident Line'
corrective_action_id = fields.Many2one('corrective.actions', string="Corrective Action", tracking=True,
required=True)
internal_panel = fields.Many2many('hr.employee', string="Internal Panel Members", tracking=True,
required=True)
external_panel = fields.Char(string="External Panel Members")
due_date = fields.Date(string="Due Date")
last_action_date = fields.Datetime(string="Last Action Date", compute='_compute_last_action_date',
default=date.today())
recommendation = fields.Char(string="Recommendation", tracking=True, required=True)
venue = fields.Char(string='Venue')
inquiry_summary = fields.Char(string='Inquiry Summary', tracking=True, required=True)
is_guilty = fields.Selection(([
('yes', 'Yes'),
('no', 'No'),
]), string="Is the Employee Guilt of the Incident", default='no', tracking=True)
inquiry_date = fields.Datetime(string="Inquiry Date and Time", required=True)
employee_inquiry_state = fields.Many2one('manage.incident')
@api.depends('inquiry_date')
def _compute_last_action_date(self):
for line in self:
if not line.employee_inquiry_state or line == line.employee_inquiry_state.employee_inquiry[0]:
line.last_action_date = False
else:
previous_line = line.employee_inquiry_state.employee_inquiry.filtered(lambda l: l.inquiry_date < line.inquiry_date)
sorted_previous_line = previous_line.sorted(key=lambda l: l.inquiry_date, reverse=True)
if sorted_previous_line:
line.last_action_date = sorted_previous_line[0].inquiry_date
else:
line.last_action_date = False

View File

@ -0,0 +1,179 @@
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from datetime import date
class HRDisciplinaryAction(models.Model):
_name = 'hr.employee.disciplinary'
_inherit = ['mail.thread', 'mail.activity.mixin']
_description = 'Employee Disciplinary Management'
active = fields.Boolean(default=True)
name = fields.Char('Reference', copy=False, readonly=True, default=lambda x: _('New'))
employee_id = fields.Many2one('hr.employee', string="Employee", required=True)
company_id = fields.Many2one('res.company', string="Company", required=True, default=lambda self: self.env.company)
employee_code = fields.Char(string='Employee Code', related='employee_id.employee_id',tracking=True,required=True)
# unit_id = fields.Many2one('unit.master', string="Unit",tracking=True)
department_id = fields.Many2one('hr.department', string="Department",tracking=True)
designation_id = fields.Many2one('hr.job', string="Designation",tracking=True)
doj = fields.Date(string="Date of Joining",tracking=True)
referred_by_id = fields.Many2one('res.users', string="Referred By",tracking=True)
loss_of_cost = fields.Float(string="Loss of Cost")
# employee_section_id = fields.Many2one('section.master',string='Section')
disciplinary_complaint_line_ids = fields.One2many('hr.disciplinary.complaint.line','disciplinary_id',string = 'Complaint Lines')
disciplinary_action_line_ids = fields.One2many('hr.disciplinary.action.line','disciplinary_id',string = 'Action Lines')
state = fields.Selection([
('new', 'New'),
('submitted', 'Submitted'),
('pending', 'Pending'),
('closed', 'Closed'),
('cancel', 'Cancel')
], default='new',tracking=True,string='State')
complaint_name = fields.Text('Complaint', compute='_compute_complaint_name', store=True)
name_1 = fields.Char('Name')
disciplinary_id = fields.Many2one('hr.employee.disciplinary', string="Disciplinary")
complaint_date = fields.Date('Complaint Date')
language_id = fields.Many2one('res.lang', 'Language')
complaint_type_id = fields.Many2one('disciplinary.complaint.type', string="Complaint Type")
mistake_type_id = fields.Many2one('disciplinary.mistake.type', string="Mistake Type", required=True)
complaint = fields.Char(string='Complaints')
employee_id_2 = fields.Many2one('hr.employee', string='Employee')
related_record_count = fields.Integer(string="Disciplinary Action Records Count", compute="_compute_related_record_count")
# general_cat = fields.Many2one('general.category', string="General Category", tracking=True)
# cat_id = fields.Many2one('hr.category','Category')
occurrences = fields.Integer('Occurrences', store=True)
severe = fields.Char('Severe')
major = fields.Char('Major')
less_major = fields.Char('Less Major')
negligible = fields.Char('Negligible')
normal = fields.Char('Normal')
total_mistakes = fields.Char('Total Mistakes')
memo = fields.Char('Memo')
explanation = fields.Char('Explanation')
show_cause = fields.Char('Show Cause')
charge_sheet = fields.Char('Charge Sheet')
warning = fields.Char('Warning')
enquiry_notice = fields.Char('Enquiry Notice')
recovery_order = fields.Char('Recovery_ Order')
stoppage_of_increment = fields.Char('Stoppage Of Increment')
demotion = fields.Char('Demotion')
total_actions = fields.Char('Total Actions')
normal_action = fields.Char('Normal Actions')
suspension = fields.Char('Suspension')
total_cost = fields.Float('Total Cost')
@api.depends('employee_id')
def _compute_related_record_count(self):
for record in self:
record.related_record_count = self.env['hr.employee.disciplinary'].search_count([('employee_id', '=', record.employee_id.id)])
def action_open_related_records(self):
return {
'name': 'Disciplinary Action Records',
'type': 'ir.actions.act_window',
'res_model': 'hr.employee.disciplinary',
'view_mode': 'list',
'domain': [('employee_id', '=', self.employee_id.id)],
'context': {'default_employee_id': self.employee_id.id},
}
@api.depends('disciplinary_complaint_line_ids.complaint')
def _compute_complaint_name(self):
for record in self:
complaints = record.disciplinary_complaint_line_ids.mapped('complaint')
record.complaint_name = "\n".join(filter(None, complaints))
def action_set_submitted(self):
self.state = 'submitted'
def action_set_pending(self):
self.state = 'pending'
def action_set_closed(self):
self.state = 'closed'
def action_set_cancel(self):
self.state = 'cancel'
def action_reset_to_new(self):
self.state = 'new'
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('name') or vals['name'] == _('New'):
vals['name'] = self.env['ir.sequence'].next_by_code('hr.employee.sequence') or _('New')
return super().create(vals_list)
@api.onchange('employee_id')
def _onchange_employee_id(self):
for rec in self:
if rec.employee_id:
rec.employee_code = rec.employee_id.employee_id or ''
rec.department_id = rec.employee_id.department_id.id
rec.designation_id = rec.employee_id.job_id.id
rec.doj = rec.employee_id.doj
rec.company_id = rec.employee_id.company_id.id
# rec.unit_id = rec.employee_id.unit_name_hr.id if rec.employee_id.unit_name_hr else False
# rec.employee_section_id = rec.employee_id.section_name_hr.id if rec.employee_id.section_name_hr else False
else:
rec.employee_code = False
rec.department_id = False
rec.designation_id = False
rec.doj = False
class DisciplinaryComplaintLine(models.Model):
_name = 'hr.disciplinary.complaint.line'
_description = 'Disciplinary Complaint Line'
name = fields.Char('Name')
disciplinary_id = fields.Many2one('hr.employee.disciplinary',string="Disciplinary")
complaint_date = fields.Date('Complaint Date')
language_id = fields.Many2one('res.lang','Language')
complaint_type_id = fields.Many2one('disciplinary.complaint.type',string="Complaint Type")
mistake_type_id = fields.Many2one('disciplinary.mistake.type',string="Mistake Type")
complaint = fields.Char(string='Complaints')
employee_id = fields.Many2one('hr.employee', string='Employee')
class DisciplinaryActionLine(models.Model):
_name = 'hr.disciplinary.action.line'
_description = 'Disciplinary Action Line'
name = fields.Char('Name')
disciplinary_id = fields.Many2one('hr.employee.disciplinary',string="Disciplinary")
action_taken_date = fields.Date('Action On')
action_type_id = fields.Many2one('disciplinary.action.type',string="Action Type")
action = fields.Char(string='Description')
action_name = fields.Char('ActionName')
related_complaint_id = fields.Many2one('hr.disciplinary.complaint.line', string="Related Complaint",
domain="[('disciplinary_id', '=', disciplinary_id)]")
employee_id = fields.Many2one('hr.employee', string='Employee')
@api.constrains('action_taken_date')
def _check_action_taken_date(self):
for record in self:
if record.action_taken_date and record.action_taken_date > date.today():
raise ValidationError("The Action On date cannot be in the future.")
class DisciplinaryActionType(models.Model):
_name = 'disciplinary.action.type'
_description = 'Action Type'
name = fields.Char('Name', required=True)
class DisciplinaryComplaintType(models.Model):
_name = 'disciplinary.complaint.type'
_description = 'Complaint Type'
name = fields.Char('Name', required=True)
class DisciplinaryMistakeType(models.Model):
_name = 'disciplinary.mistake.type'
_description = 'Mistake Type'
name = fields.Char('Name', required=True)

View File

@ -0,0 +1,22 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_employee_disciplinary,employee_disciplinary,model_employee_disciplinary,,1,1,1,1
access_incident_employee,incident_employee,model_incident_employee,,1,1,1,1
access_incident_sub_employee,incident_sub_employee,model_incident_sub_employee,,1,1,1,1
access_employee_disciplinary_line,employee_disciplinary_line,model_employee_disciplinary_line,,1,1,1,1
access_manage_incident,manage_incident,model_manage_incident,,1,1,1,1
access_manage_incident_line,manage_incident_line,model_manage_incident_line,,1,1,1,1
access_corrective_actions,corrective_actions,model_corrective_actions,,1,1,1,1
access_hr_employee_disciplinary,hr.employee.disciplinary,model_hr_employee_disciplinary,,1,1,1,1
access_hr_disciplinary_complaint_line,hr.disciplinary.complaint.line,model_hr_disciplinary_complaint_line,,1,1,1,1
access_hr_disciplinary_action_line,hr.disciplinary.action.line,model_hr_disciplinary_action_line,,1,1,1,1
access_disciplinary_action_type,disciplinary.action.type,model_disciplinary_action_type,,1,1,1,1
access_disciplinary_complaint_type,disciplinary.complaint.type,model_disciplinary_complaint_type,,1,1,1,1
access_disciplinary_mistake_type,disciplinary.mistake.type,model_disciplinary_mistake_type,,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_employee_disciplinary employee_disciplinary model_employee_disciplinary 1 1 1 1
3 access_incident_employee incident_employee model_incident_employee 1 1 1 1
4 access_incident_sub_employee incident_sub_employee model_incident_sub_employee 1 1 1 1
5 access_employee_disciplinary_line employee_disciplinary_line model_employee_disciplinary_line 1 1 1 1
6 access_manage_incident manage_incident model_manage_incident 1 1 1 1
7 access_manage_incident_line manage_incident_line model_manage_incident_line 1 1 1 1
8 access_corrective_actions corrective_actions model_corrective_actions 1 1 1 1
9 access_hr_employee_disciplinary hr.employee.disciplinary model_hr_employee_disciplinary 1 1 1 1
10 access_hr_disciplinary_complaint_line hr.disciplinary.complaint.line model_hr_disciplinary_complaint_line 1 1 1 1
11 access_hr_disciplinary_action_line hr.disciplinary.action.line model_hr_disciplinary_action_line 1 1 1 1
12 access_disciplinary_action_type disciplinary.action.type model_disciplinary_action_type 1 1 1 1
13 access_disciplinary_complaint_type disciplinary.complaint.type model_disciplinary_complaint_type 1 1 1 1
14 access_disciplinary_mistake_type disciplinary.mistake.type model_disciplinary_mistake_type 1 1 1 1

View File

@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_disciplinary_complaint_type_list" model="ir.ui.view">
<field name="name">disciplinary.complaint.type</field>
<field name="model">disciplinary.complaint.type</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
</list>
</field>
</record>
<record id="view_disciplinary_complaint_type_form" model="ir.ui.view">
<field name="name">disciplinary.complaint.type.form</field>
<field name="model">disciplinary.complaint.type</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name" />
</group>
</sheet>
</form>
</field>
</record>
<record id="action_disciplinary_complaint_type" model="ir.actions.act_window">
<field name="name">Employee Disciplinary Complaint Type</field>
<field name="res_model">disciplinary.complaint.type</field>
<field name="view_mode">list,form</field>
</record>
<menuitem
id="menu_view_disciplinary_complaint"
name="Disciplinary Complaints"
action="action_disciplinary_complaint_type"
parent="menu_employee_disciplinary_root"
sequence="19"/>
<record id="view_employee_disciplinary_complaint_line_list" model="ir.ui.view">
<field name="name">hr.disciplinary.action.line</field>
<field name="model">hr.disciplinary.action.line</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
</list>
</field>
</record>
<record id="view_employee_disciplinary_complaint_line_form" model="ir.ui.view">
<field name="name">hr.disciplinary.action.line.form</field>
<field name="model">hr.disciplinary.action.line</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name" />
<field name="disciplinary_id" />
<field name="action_taken_date" />
<field name="action_type_id" />
<field name="action" />
<field name="related_complaint_id" />
<field name="employee_id" />
</group>
</sheet>
</form>
</field>
</record>
<record id="action_employee_disciplinary_complaint_line" model="ir.actions.act_window">
<field name="name">Employee Disciplinary Action</field>
<field name="res_model">hr.disciplinary.action.line</field>
<field name="view_mode">list,form</field>
</record>
<menuitem
id="menu_view_employee__disciplinary_complaint"
name="Employee Disciplinary Complaints"
action="action_employee_disciplinary_complaint_line"
parent="menu_employee_disciplinary_root"
sequence="20"/>
<record id="view_disciplinary_action_type_list" model="ir.ui.view">
<field name="name">disciplinary.action.type</field>
<field name="model">disciplinary.action.type</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
</list>
</field>
</record>
<record id="view_disciplinary_action_type_form" model="ir.ui.view">
<field name="name">disciplinary.action.type.form</field>
<field name="model">disciplinary.action.type</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name" />
</group>
</sheet>
</form>
</field>
</record>
<record id="action_disciplinary_action_type" model="ir.actions.act_window">
<field name="name">Employee Disciplinary Action Type</field>
<field name="res_model">disciplinary.action.type</field>
<field name="view_mode">list,form</field>
</record>
<menuitem
id="menu_view_disciplinary_action_type"
name="Disciplinary Action Type"
action="action_disciplinary_action_type"
parent="menu_employee_disciplinary_root"
sequence="21"/>
</odoo>

View File

@ -0,0 +1,264 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="employee_disciplinary_list" model="ir.ui.view">
<field name="name">Employee Disciplinary list</field>
<field name="model">employee.disciplinary</field>
<field name="arch" type="xml">
<list>
<field name="incident_date"/>
<field name="incident_type"/>
<field name="incident_sub_type" widget="many2many_tags"/>
</list>
</field>
</record>
<record id="employee_disciplinary_form" model="ir.ui.view">
<field name="name">Employee Disciplinary form</field>
<field name="model">employee.disciplinary</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="incident_date"/>
<field name="incident_type" options="{'no_open': True,}"/>
<field name="incident_sub_type" widget="many2many_tags" readonly="0"
options="{'no_open': True}"/>
<label for="employee_code" string="Reported By Employee Code"/>
<div class="address_format">
<field name="employee_code" style="width: 50%" options="{'no_open': True,}"/>
<field name="employee_name" style="width: 50%"/>
</div>
<field name="incident_details"/>
<field name="seized_items"/>
<field name="incident_summary"/>
<field name="attach" widget="many2many_binary" options="{'preview_image': True}"/>
<field name="emp_many_disp" widget="many2many_tags" context="{'new_custom_name': True}"/>
</group>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="manage_incident_list" model="ir.ui.view">
<field name="name">Manage Incident list</field>
<field name="model">manage.incident</field>
<field name="arch" type="xml">
<list create="0">
<field name="employee_code_list1" widget="many2many_tags" context="{'new_custom_name': True}"/>
<!-- <field name="incident_dat"/>-->
<field name="incident_typ"/>
<field name="incident_sub_typ" widget="many2many_tags"/>
<field name="incident_sum"/>
<field name="state"/>
</list>
</field>
</record>
<record id="manage_incident_form" model="ir.ui.view">
<field name="name">Manage Incident form</field>
<field name="model">manage.incident</field>
<field name="arch" type="xml">
<form create="0">
<header>
<button name="button_in_progress" string="In Progress" class="oe_highlight" type="object"/>
<!-- states="pending_inquiry"/>-->
<!-- attrs="{'invisible' : ('state','!=','pending_inquiry')}"/>-->
<button name="button_closed" string="Closed" class="oe_highlight" type="object"/>
<!-- states="in_progress"/>-->
<!-- attrs="{'invisible' : ('state','!=','in_progress')}"/>-->
<field name="state" widget="statusbar"/>
</header>
<sheet>
<group>
<group>
<field name="employee_code_list1" widget="many2many_tags"
context="{'new_custom_name': True}"/>
<field name="employee_by_code" options="{'no_open': True,}"/>
<!-- <field name="incident_dat"/>-->
</group>
<group>
<field name="incident_typ" options="{'no_open': True,}"/>
<field name="incident_sub_typ" widget="many2many_tags"/>
<field name="incident_sum"/>
</group>
</group>
<field name="employee_inquiry" string="Manage Incident">
<!-- attrs="{'readonly': [('state', '=','closed')]}">-->
<list>
<field name="inquiry_date"/>
<field name="corrective_action_id"/>
<field name="due_date"/>
<field name="last_action_date"/>
</list>
<form>
<group>
<field name="inquiry_date"/>
<field name="venue"/>
<field name="internal_panel" widget="many2many_tags"
context="{'new_custom_name': True}"/>
<field name="external_panel"/>
<field name="inquiry_summary"/>
<field name="is_guilty" widget="radio" options="{'horizontal':true}"/>
<field name="corrective_action_id" options="{'no_open': True,}"/>
<field name="recommendation"/>
<field name="due_date"/>
<field name="last_action_date"/>
</group>
</form>
</field>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- <record id="career_history_tab_sub_menu" model="ir.ui.view">-->
<!-- <field name="name">Career History Tab Sub Menu</field>-->
<!-- <field name="model">hr.employee</field>-->
<!-- <field name="inherit_id" ref="employee_life_cycle.career_history_tab_menu"/>-->
<!-- <field name="arch" type="xml">-->
<!-- <xpath expr="//page/field[@name='career_history_field']" position="after">-->
<!-- &lt;!&ndash; <group name="career_hist_sub_menu" string="Disciplinary Actions">&ndash;&gt;-->
<!-- &lt;!&ndash; <field name="employee_name_ids1" string="Manage Incident">&ndash;&gt;-->
<!-- &lt;!&ndash; <list editable="0" create="0">&ndash;&gt;-->
<!-- &lt;!&ndash; <field name="incident_date"/>&ndash;&gt;-->
<!-- &lt;!&ndash; <field name="incident_type"/>&ndash;&gt;-->
<!-- &lt;!&ndash; <field name="incident_sub_type"/>&ndash;&gt;-->
<!-- &lt;!&ndash;&lt;!&ndash; <field name="corrective_action_id"/>&ndash;&gt;&ndash;&gt;-->
<!-- &lt;!&ndash; </list>&ndash;&gt;-->
<!-- &lt;!&ndash; </field>&ndash;&gt;-->
<!-- &lt;!&ndash; </group>&ndash;&gt;-->
<!-- <field name="employee_self_service_line_ids" string="Manage Incident" readonly="1">-->
<!-- <list>-->
<!-- <field name="incident_dat"/>-->
<!-- <field name="incident_typ"/>-->
<!-- <field name="incident_sub_typ"/>-->
<!-- </list>-->
<!-- </field>-->
<!-- </xpath>-->
<!-- </field>-->
<!-- </record>-->
<!-- <record id="career_history_tab_sub_menu_self" model="ir.ui.view">-->
<!-- <field name="name">Career History Tab Sub Menu Self Service</field>-->
<!-- <field name="model">hr.employee</field>-->
<!-- <field name="inherit_id" ref="employee_self_service.view_employee_form_self_service"/>-->
<!-- <field name="arch" type="xml">-->
<!-- <xpath expr="//page/field[@name='career_history_field']" position="after">-->
<!-- &lt;!&ndash; <group name="career_hist_sub_menu" string="Disciplinary Actions">&ndash;&gt;-->
<!-- &lt;!&ndash; <field name="employee_name_ids1" string="Manage Incident">&ndash;&gt;-->
<!-- &lt;!&ndash; <list editable="0" create="0">&ndash;&gt;-->
<!-- &lt;!&ndash; <field name="incident_date"/>&ndash;&gt;-->
<!-- &lt;!&ndash; <field name="incident_type"/>&ndash;&gt;-->
<!-- &lt;!&ndash; <field name="incident_sub_type"/>&ndash;&gt;-->
<!-- &lt;!&ndash; &lt;!&ndash; <field name="corrective_action_id"/>&ndash;&gt;&ndash;&gt;-->
<!-- &lt;!&ndash; </list>&ndash;&gt;-->
<!-- &lt;!&ndash; </field>&ndash;&gt;-->
<!-- &lt;!&ndash; </group>&ndash;&gt;-->
<!-- <field name="employee_self_service_line_ids" string="Manage Incident" readonly="1">-->
<!-- <list>-->
<!-- <field name="incident_dat"/>-->
<!-- <field name="incident_typ"/>-->
<!-- <field name="incident_sub_typ"/>-->
<!-- </list>-->
<!-- </field>-->
<!-- </xpath>-->
<!-- </field>-->
<!-- </record>-->
<record id="incident_employee_list" model="ir.ui.view">
<field name="name">incident Employee list</field>
<field name="model">incident.employee</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="sub_type" widget="many2many_tags"/>
</list>
</field>
</record>
<record id="incident_employee_form" model="ir.ui.view">
<field name="name">incident Employee form</field>
<field name="model">incident.employee</field>
<field name="arch" type="xml">
<form>
<sheet>
<form>
<group>
<field name="name"/>
<field name="sub_type" widget="many2many_tags"/>
</group>
</form>
</sheet>
</form>
</field>
</record>
<record id="employee_disciplinary_action" model="ir.actions.act_window">
<field name="name">Incident Reporting</field>
<field name="res_model">employee.disciplinary</field>
<field name="view_mode">list,form</field>
</record>
<record id="manage_incident_action" model="ir.actions.act_window">
<field name="name">Manage Incident</field>
<field name="res_model">manage.incident</field>
<field name="view_mode">list,form</field>
</record>
<record id="incident_employee_action" model="ir.actions.act_window">
<field name="name">Incident Type</field>
<field name="res_model">incident.employee</field>
<field name="view_mode">list,form</field>
</record>
<record id="action_hr_employee_disciplinary" model="ir.actions.act_window">
<field name="name">Employee Disciplinary</field>
<field name="res_model">hr.employee.disciplinary</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="employee_disciplinary_menu"
name="Employee Disciplinary Management"
parent="hr.menu_hr_root"
action="employee_disciplinary_action"
sequence="105"/>
<!-- Child Menu (moved inside) -->
<menuitem id="menu_employee_disciplinary_root"
name="Employee Disciplinary Configuration"
parent="employee_disciplinary_menu"
sequence="10"/>
<!-- Sub Menu -->
<menuitem id="menu_employee_disciplinary"
name="Employee Disciplinary"
parent="employee_disciplinary_menu"
action="action_hr_employee_disciplinary"
sequence="01"/>
<!-- <menuitem id="manage_incident_employee"-->
<!-- name="Incident Type"-->
<!-- parent="employee_disciplinary_menu"-->
<!-- action="incident_employee_action"-->
<!-- sequence="3"/>-->
<!-- <menuitem id="manage_incident_sub_menu"-->
<!-- name="Manage Incident"-->
<!-- parent="employee_disciplinary_menu"-->
<!-- action="manage_incident_action"-->
<!-- sequence="2"/>-->
<!-- <menuitem id="employee_disciplinary_sub_menu"-->
<!-- name="Incident Reporting"-->
<!-- parent="employee_disciplinary_menu"-->
<!-- action="employee_disciplinary_action"-->
<!-- sequence="1"/>-->
</odoo>

View File

@ -0,0 +1,169 @@
<odoo>
<!-- Disciplinary Form View -->
<record id="view_hr_employee_disciplinary_form" model="ir.ui.view">
<field name="name">employee.disciplinary.form</field>
<field name="model">hr.employee.disciplinary</field>
<field name="arch" type="xml">
<form string="Employee Disciplinary">
<header>
<button name="action_set_submitted"
type="object"
string="Submit"
class="btn-primary"
invisible="state != 'new'"/>
<button name="action_set_pending"
type="object"
string="Pending"
class="btn-warning"
invisible="state != 'submitted'"/>
<button name="action_set_closed"
type="object"
string="Closed"
class="btn-success"
invisible="state != 'pending'"/>
<button name="action_set_cancel" type="object" string="Cancel"
class="btn-danger" invisible="state not in ['new', 'submitted', 'pending']"/>
<button name="action_reset_to_new" type="object"
string="Reset to New" class="btn-secondary" invisible="state != 'cancel'"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button name="action_open_related_records"
type="object"
icon="fa-gavel"
class="oe_stat_button full-width-button"
string="Disciplinary">
<field name="related_record_count" widget="statinfo"/>
</button>
</div>
<div>
<h3>
<field name="name" readonly="1"/>
</h3>
</div>
<group>
<group>
<field name="employee_code"/>
<field name="employee_id"/>
<field name="designation_id"/>
<field name="doj"/>
<!-- <field name="general_cat"/>-->
<field name="referred_by_id"/>
<!-- <field name="occurrences"/>-->
</group>
<group>
<field name="company_id"/>
<!-- <field name="unit_id"/>-->
<field name="department_id"/>
<!-- <field name="employee_section_id"/>-->
<field name="loss_of_cost"/>
<!-- <field name="cat_id"/>-->
<field name="total_cost"/>
</group>
</group>
<group string="Complaints" colspan="2">
<group colspan="1">
<field name="complaint_date"/>
<field name="language_id"/>
<field name="complaint_type_id"/>
</group>
<group colspan="1">
<field name="mistake_type_id"/>
<field name="complaint"/>
</group>
</group>
<notebook>
<!-- <page name="'complaints" string = "Complaints">-->
<!-- <field name="disciplinary_complaint_line_ids">-->
<!-- <list string="complaints" editable="bottom">-->
<field name="name" column_invisible="1"/>
<field name="complaint_date"/>
<field name="language_id"/>
<field name="complaint_type_id"/>
<field name="mistake_type_id"/>
<field name="complaint"/>
<field name="disciplinary_id" column_invisible="1"/>
<field name="employee_id" column_invisible="1"/>
<!-- </list>-->
<!-- </field>-->
<!-- </page>-->
<page name="'actions" string="Actions">
<field name="disciplinary_action_line_ids">
<list string="Action Lines" editable="bottom">
<field name="name" column_invisible="1"/>
<field name="action_name"/>
<!-- <field name="action_taken_date"/>-->
<field name="action_taken_date"
context="{'max_date': time.strftime('%Y-%m-%d')}"/>
<field name="action_type_id"/>
<field name="action"/>
<field name="disciplinary_id" column_invisible="1"/>
<field name="employee_id" column_invisible="1"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- list View for Employee Disciplinary -->
<record id="view_hr_employee_disciplinary_list" model="ir.ui.view">
<field name="name">hr.employee.disciplinary.list</field>
<field name="model">hr.employee.disciplinary</field>
<field name="arch" type="xml">
<list string="Employee Disciplinary">
<field name="name"/>
<field name="employee_id" string="Employee"/>
<field name="designation_id" string="Designation"/>
<field name="doj" string="Date of Joining"/>
<field name="company_id" string="Company"/>
<!-- <field name="unit_id" string="Unit"/>-->
<field name="department_id" string="Department"/>
<field name="state"
widget="badge"
decoration-primary="state == 'new'"
decoration-warning="state == 'submitted'"
decoration-info="state == 'pending'"
decoration-success="state == 'closed'"
decoration-danger="state == 'cancel'"/>
</list>
</field>
</record>
<record id="view_hr_employee_disciplinary_search" model="ir.ui.view">
<field name="name">hr.employee.disciplinary.search</field>
<field name="model">hr.employee.disciplinary</field>
<field name="arch" type="xml">
<search string="Search Employee Disciplinary">
<field name="employee_id" string="Employee"/>
<field name="employee_code" string="Employee Code"/>
</search>
</field>
</record>
<record id="action_hr_employee_disciplinary" model="ir.actions.act_window">
<field name="name">Employee Disciplinary</field>
<field name="res_model">hr.employee.disciplinary</field>
<field name="view_mode">list,form</field>
<field name="search_view_id" ref="view_hr_employee_disciplinary_search"/>
</record>
<!-- <menuitem id="menu_employee_disciplinary_root" name="Employee Disciplinary" sequence="15" parent="hr.menu_hr_root"/>-->
<!-- <menuitem id="menu_employee_disciplinary" name="Employee Disciplinary"-->
<!-- parent="menu_employee_disciplinary_root"-->
<!-- action="action_hr_employee_disciplinary"-->
<!-- sequence="10"/>-->
</odoo>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<!-- list View -->
<record id="view_incident_sub_employee_list" model="ir.ui.view">
<field name="name">incident.sub.employee.list</field>
<field name="model">incident.sub.employee</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_incident_sub_employee_form" model="ir.ui.view">
<field name="name">incident.sub.employee.form</field>
<field name="model">incident.sub.employee</field>
<field name="arch" type="xml">
<form string="Incident Sub Type">
<sheet>
<group>
<field name="name"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- Action -->
<record id="action_incident_sub_employee" model="ir.actions.act_window">
<field name="name">Incident Sub Type</field>
<field name="res_model">incident.sub.employee</field>
<field name="view_mode">list,form</field>
</record>
<!-- Menu -->
<!-- <menuitem id="menu_incident_sub_employee"-->
<!-- name="Incident Sub Type"-->
<!-- parent="employee_disciplinary_menu"-->
<!-- action="action_incident_sub_employee"/>-->
</data>
</odoo>

View File

@ -0,0 +1,42 @@
<odoo>
<!-- Tree View -->
<record id="view_mistake_type_list" model="ir.ui.view">
<field name="name">mistake.type.list</field>
<field name="model">disciplinary.mistake.type</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="view_mistake_type_form" model="ir.ui.view">
<field name="name">mistake.type.form</field>
<field name="model">disciplinary.mistake.type</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name"/>
</group>
</sheet>
</form>
</field>
</record>
<!-- Action -->
<record id="action_mistake_type" model="ir.actions.act_window">
<field name="name">Mistake Type</field>
<field name="res_model">disciplinary.mistake.type</field>
<field name="view_mode">list,form</field>
</record>
<menuitem
id="menu_mistake_type"
name="Mistake Type"
parent="menu_employee_disciplinary_root"
action="action_mistake_type"
sequence="02"/>
</odoo>

View File

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

View File

@ -0,0 +1,19 @@
{
"name": "Document Parser",
"summary": "Reusable AI-assisted document text and data extraction",
"version": "1.0.0",
"category": "Tools",
"author": "Pranay",
"website": "https://www.ftprotech.com",
"license": "LGPL-3",
"depends": ["base"],
"data": [
"views/res_config_settings_views.xml",
],
"installable": True,
"application": False,
"auto_install": False,
"external_dependencies": {
"python": ["requests","python-docx"],
},
}

View File

@ -0,0 +1,2 @@
from . import document_parser_service
from . import res_config_settings

View File

@ -0,0 +1,529 @@
import base64
import json
import logging
import mimetypes
import re
from io import BytesIO
import requests
from odoo import _, api, models
from odoo.exceptions import UserError
try:
import pytesseract
except Exception: # pragma: no cover - optional dependency
pytesseract = None
try:
from PIL import Image
except Exception: # pragma: no cover - optional dependency
Image = None
try:
from pdf2image import convert_from_bytes
except Exception: # pragma: no cover - optional dependency
convert_from_bytes = None
try:
from pypdf import PdfReader
except Exception: # pragma: no cover - optional dependency
PdfReader = None
try:
from docx import Document
except Exception: # pragma: no cover - optional dependency
Document = None
_logger = logging.getLogger(__name__)
class DocumentParserService(models.AbstractModel):
_name = "document.parser.service"
_description = "Document Parser Service"
TOGETHER_ENDPOINT = "https://api.together.xyz/v1/chat/completions"
OPENROUTER_ENDPOINT = "https://openrouter.ai/api/v1/chat/completions"
TOGETHER_MODELS = [
"Qwen/Qwen2.5-7B-Instruct-Turbo",
"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
]
OPENROUTER_MODELS = [
"qwen/qwen-2.5-7b-instruct",
"qwen/qwen-2.5-7b-instruct:free",
"deepseek/deepseek-chat:free",
]
@api.model
def parse_document(
self,
file_content,
filename=None,
required_fields=None,
extra_instructions=None,
json_schema=None,
):
if not file_content:
raise UserError(_("No document provided."))
if not filename:
raise UserError(_("Filename is required."))
binary = self._decode_file_content(file_content)
mimetype = self._detect_mimetype(binary, filename)
text_content = self._extract_text(binary, mimetype)
fields_spec = self._normalize_required_fields(required_fields or {})
if not text_content.strip():
return {
"filename": filename,
"mimetype": mimetype,
"text": "",
"result": {},
"provider": False,
"errors": [_("No text could be extracted from the document.")],
"error": _("No text could be extracted from the document."),
}
schema_text = json_schema or self._build_json_schema_text(fields_spec)
ai_result, provider_used, provider_errors = self._send_to_ai(
text_content=text_content[:45000],
schema_text=schema_text,
extra_instructions=extra_instructions,
)
if not ai_result:
ai_result = self._extract_with_heuristics(text_content, fields_spec)
ai_result = ai_result or {}
error_message = False
if not ai_result and provider_errors:
error_message = "; ".join(provider_errors[:3])
return {
"filename": filename,
"mimetype": mimetype,
"text": text_content,
"result": ai_result,
"provider": provider_used,
"errors": provider_errors,
"error": error_message,
}
@api.model
def extract_requested_data(self, file_content, filename, required_fields, extra_instructions=None, json_schema=None):
return self.parse_document(
file_content=file_content,
filename=filename,
required_fields=required_fields,
extra_instructions=extra_instructions,
json_schema=json_schema,
)["result"]
def _decode_file_content(self, file_content):
if isinstance(file_content, bytes):
if file_content.startswith((b"%PDF", b"\xFF\xD8", b"\x89PNG", b"PK")):
return file_content
try:
return base64.b64decode(file_content)
except Exception:
return file_content
if isinstance(file_content, str):
try:
return base64.b64decode(file_content)
except Exception as exc:
raise UserError(_("Invalid base64 document.")) from exc
raise UserError(_("Unsupported file format."))
def _detect_mimetype(self, binary, filename):
if filename:
guessed = mimetypes.guess_type(filename)[0]
if guessed:
return guessed
if binary.startswith(b"%PDF"):
return "application/pdf"
if binary.startswith(b"\xFF\xD8"):
return "image/jpeg"
if binary.startswith(b"\x89PNG"):
return "image/png"
if binary[:2] == b"PK":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
return "application/octet-stream"
def _extract_text(self, binary, mimetype):
text_content = ""
try:
if mimetype == "application/pdf":
text_content = self._extract_text_from_pdf(binary)
elif mimetype in {"image/png", "image/jpeg", "image/jpg"}:
text_content = self._extract_text_from_image(binary)
elif mimetype == "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
text_content = self._extract_text_from_docx(binary)
elif mimetype.startswith("text/"):
text_content = binary.decode("utf-8", errors="ignore")
except Exception as exc:
_logger.exception("Document text extraction failed: %s", exc)
return (text_content or "").strip()
def _extract_text_from_pdf(self, binary):
extracted_parts = []
if PdfReader:
try:
reader = PdfReader(BytesIO(binary))
extracted_parts.extend(page.extract_text() or "" for page in reader.pages)
except Exception as exc:
_logger.warning("PdfReader extraction failed: %s", exc)
text_content = "\n".join(part for part in extracted_parts if part).strip()
if text_content:
return text_content
if convert_from_bytes and pytesseract:
try:
images = convert_from_bytes(binary, dpi=300)
return "\n".join(
pytesseract.image_to_string(image)
for image in images
).strip()
except Exception as exc:
_logger.warning("PDF OCR extraction failed: %s", exc)
return ""
def _extract_text_from_image(self, binary):
if not pytesseract or not Image:
return ""
try:
image = Image.open(BytesIO(binary))
return pytesseract.image_to_string(image).strip()
except Exception as exc:
_logger.warning("Image OCR extraction failed: %s", exc)
return ""
def _extract_text_from_docx(self, binary):
if not Document:
return ""
try:
document = Document(BytesIO(binary))
return "\n".join(
paragraph.text for paragraph in document.paragraphs if paragraph.text
).strip()
except Exception as exc:
_logger.warning("DOCX extraction failed: %s", exc)
return ""
def _send_to_ai(self, text_content, schema_text, extra_instructions=None):
prompt = self._build_prompt(text_content, schema_text, extra_instructions)
errors = []
together_key = self._get_param("document_parser.together_ai_key") or self._get_param("document_parser.together_api_key")
openrouter_key = self._get_param("document_parser.openrouter_ai_key") or self._get_param("document_parser.openrouter_api_key")
if together_key:
result, provider_errors = self._call_provider(
provider_name="Together",
endpoint=self.TOGETHER_ENDPOINT,
headers={
"Authorization": f"Bearer {together_key}",
"Content-Type": "application/json",
},
models=self.TOGETHER_MODELS,
prompt=prompt,
)
if result:
return result, "together", errors
errors.extend(provider_errors)
else:
errors.append(_("Together AI key is not configured."))
if openrouter_key:
result, provider_errors = self._call_provider(
provider_name="OpenRouter",
endpoint=self.OPENROUTER_ENDPOINT,
headers={
"Authorization": f"Bearer {openrouter_key}",
"Content-Type": "application/json",
"HTTP-Referer": self._get_param("web.base.url") or "odoo.local",
"X-Title": "Document Parser",
},
models=self.OPENROUTER_MODELS,
prompt=prompt,
)
if result:
return result, "openrouter", errors
errors.extend(provider_errors)
else:
errors.append(_("OpenRouter key is not configured."))
return {}, False, errors
def _build_prompt(self, text_content, schema_text, extra_instructions=None):
return f"""
You are a strict JSON generator.
RULES:
- Output ONLY valid raw JSON.
- No explanation.
- No markdown.
- No backticks.
- No extra text.
- Follow schema strictly.
- If a field is missing in text, return null.
- Scan the entire document carefully before answering.
- Extract ONLY what exists in text.
- FOR ANY DATES CHANGE FORMAT TO %Y-%m-%d
FIELD RULES:
- If "skills" exists, extract only explicit technical skills written in the document.
- Do NOT infer similar skills from role names, responsibilities, or projects.
- Normalize names like "Expert Python" to "Python".
- Exclude soft skills and business phrases.
- Exclude responsibility-style phrases like Cross-Functional Collaboration, Cost Saving, Resource Utilization, Documentation, Reporting, and Team Handling.
- Prefer concrete tools, methods, technologies, platforms, certifications, engineering/process methods, and domain techniques explicitly written in the resume.
- If the resume explicitly mentions items like AutoCAD, Root Cause Analysis, Project Management, Manufacturing Processes, Lean, Six Sigma, or Quality Control, include them.
- Remove duplicates and return each skill only once.
- If "email" exists, return one valid normalized email.
- If "name" exists, prefer the full name at the top and exclude titles, companies, and addresses.
- If "phone" exists, return the most complete phone number found.
- If "experience" exists, return only clearly supported numeric values.
Schema:
{schema_text}
Instructions:
{extra_instructions or "None"}
Document:
{text_content}
"""
def _call_provider(self, provider_name, endpoint, headers, models, prompt):
errors = []
for model in models:
payload = {
"model": model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0,
"max_tokens": 1500,
}
try:
response = requests.post(endpoint, headers=headers, json=payload, timeout=90)
if response.status_code != 200:
message = _("%(provider)s model %(model)s failed with %(status)s: %(body)s") % {
"provider": provider_name,
"model": model,
"status": response.status_code,
"body": (response.text or "")[:300],
}
_logger.warning(message)
errors.append(message)
continue
body = response.json()
content = self._extract_message_content(body)
parsed = self._safe_json_load(content)
if parsed:
return parsed, errors
message = _("%(provider)s model %(model)s returned invalid JSON.") % {
"provider": provider_name,
"model": model,
}
_logger.warning(message)
errors.append(message)
except Exception as exc:
message = _("%(provider)s model %(model)s error: %(error)s") % {
"provider": provider_name,
"model": model,
"error": str(exc),
}
_logger.warning(message)
errors.append(message)
return {}, errors
def _extract_message_content(self, response_body):
try:
content = response_body["choices"][0]["message"]["content"]
except Exception:
return ""
if isinstance(content, list):
parts = []
for item in content:
if isinstance(item, dict):
if item.get("type") == "text":
parts.append(item.get("text", ""))
elif item.get("text"):
parts.append(item.get("text"))
else:
parts.append(str(item))
return "\n".join(part for part in parts if part)
if isinstance(content, dict):
return content.get("text", "")
return content or ""
def _safe_json_load(self, content):
if not content:
return {}
content = content.strip().replace("```json", "").replace("```", "").strip()
try:
return json.loads(content)
except Exception:
pass
match = re.search(r"\{[\s\S]*\}", content)
if match:
try:
return json.loads(match.group(0))
except Exception:
pass
_logger.warning("JSON parse failed for provider response: %s", content[:500])
return {}
def _extract_with_heuristics(self, text_content, fields):
result = {}
email_match = re.search(r"([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})", text_content or "", re.I)
phone_match = re.search(r"(\+?\d[\d\-\s()]{7,}\d)", text_content or "")
linkedin_match = re.search(r"(https?://(?:www\.)?linkedin\.com/[^\s]+)", text_content or "", re.I)
name_guess = self._guess_name(text_content or "")
skills_guess = self._guess_skills(text_content or "")
for field_name, field_spec in fields.items():
field_type = field_spec.get("type", "string")
if field_name in {"email", "email_from"}:
result[field_name] = email_match.group(1).lower() if email_match else None
elif field_name in {"phone", "mobile", "partner_phone"}:
result[field_name] = phone_match.group(1).strip() if phone_match else None
elif field_name in {"linkedin_profile", "linkedin"}:
result[field_name] = linkedin_match.group(1).strip() if linkedin_match else None
elif field_name in {"name", "full_name", "partner_name"}:
result[field_name] = name_guess
elif field_name == "skills" and field_type == "list":
result[field_name] = skills_guess
else:
result[field_name] = None
return result
def _guess_name(self, text_content):
for line in [line.strip() for line in (text_content or "").splitlines() if line.strip()][:12]:
cleaned = re.sub(r"[^A-Za-z .'-]", "", line).strip()
if len(cleaned.split()) in {2, 3, 4} and not re.search(r"(resume|cv|email|phone|linkedin|skills|experience)", cleaned, re.I):
return cleaned
return None
def _guess_skills(self, text_content):
section = re.search(r"(skills|technical skills|core competencies)(.*?)(experience|education|projects|certifications|$)", text_content or "", re.I | re.S)
if not section:
return []
parts = re.split(r"[,;\n|•]", section.group(2))
cleaned = []
for part in parts:
value = re.sub(r"\s+", " ", part).strip(" -:\t\r\n")
if value and 1 < len(value) < 50 and not re.search(r"^(skills?|experience|education)$", value, re.I):
cleaned.append(value)
return list(dict.fromkeys(cleaned[:25]))
def _get_param(self, key):
return self.env["ir.config_parameter"].sudo().get_param(key)
@api.model
def validate_explicit_skills(self, resume_text, skills):
if not skills:
return []
prompt = f"""
You are validating resume skills.
Resume:
{resume_text[:30000]}
Extracted Skills:
{json.dumps(skills)}
Keep ONLY skills explicitly claimed by the candidate.
A skill is explicit if:
- It is presented as the candidate's expertise.
- It appears in a skill list or competency list.
Reject skills appearing only in:
- job responsibilities
- project descriptions
- achievements
- employer history
Return ONLY JSON.
Example:
{{
"skills": ["Python", "Django"]
}}
"""
errors = []
together_key = self._get_param(
"document_parser.together_ai_key"
) or self._get_param(
"document_parser.together_api_key"
)
openrouter_key = self._get_param(
"document_parser.openrouter_ai_key"
) or self._get_param(
"document_parser.openrouter_api_key"
)
if together_key:
result, provider_errors = self._call_provider(
provider_name="Together",
endpoint=self.TOGETHER_ENDPOINT,
headers={
"Authorization": f"Bearer {together_key}",
"Content-Type": "application/json",
},
models=self.TOGETHER_MODELS,
prompt=prompt,
)
if result:
return result.get("skills", skills)
errors.extend(provider_errors)
if openrouter_key:
result, provider_errors = self._call_provider(
provider_name="OpenRouter",
endpoint=self.OPENROUTER_ENDPOINT,
headers={
"Authorization": f"Bearer {openrouter_key}",
"Content-Type": "application/json",
"HTTP-Referer": self._get_param("web.base.url") or "odoo.local",
"X-Title": "Document Parser",
},
models=self.OPENROUTER_MODELS,
prompt=prompt,
)
if result:
return result.get("skills", skills)
errors.extend(provider_errors)
return skills
def _normalize_required_fields(self, fields):
if isinstance(fields, dict):
normalized = {}
for field_name, field_value in fields.items():
if isinstance(field_value, dict):
normalized[field_name] = {
"type": field_value.get("type", "string"),
"description": field_value.get("description", field_name.replace("_", " ").title()),
}
else:
normalized[field_name] = {
"type": "string",
"description": str(field_value or field_name.replace("_", " ").title()),
}
return normalized
if isinstance(fields, list):
return {field_name: {"type": "string", "description": field_name.replace("_", " ").title()} for field_name in fields}
return {}
def _build_json_schema_text(self, fields):
return json.dumps(fields, ensure_ascii=True)

View File

@ -0,0 +1,67 @@
import requests
from odoo import _, fields, models
from odoo.exceptions import UserError
class ResConfigSettings(models.TransientModel):
_inherit = "res.config.settings"
together_ai_key = fields.Char(
string="Together AI Key",
config_parameter="document_parser.together_ai_key",
)
openrouter_ai_key = fields.Char(
string="OpenRouter AI Key",
config_parameter="document_parser.openrouter_ai_key",
)
def action_test_together_ai_connection(self):
self.ensure_one()
if not self.together_ai_key:
raise UserError(_("Please add the Together AI key first."))
response = requests.get(
"https://api.together.xyz/v1/models",
headers={"Authorization": f"Bearer {self.together_ai_key}"},
timeout=20,
)
if response.ok:
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("Together AI Connection"),
"message": _("Connection successful."),
"type": "success",
"sticky": False,
},
}
raise UserError(_("Together AI connection failed: %s") % (response.text or response.reason))
def action_test_openrouter_ai_connection(self):
self.ensure_one()
if not self.openrouter_ai_key:
raise UserError(_("Please add the OpenRouter key first."))
response = requests.get(
"https://openrouter.ai/api/v1/models",
headers={
"Authorization": f"Bearer {self.openrouter_ai_key}",
"HTTP-Referer": self.env["ir.config_parameter"].sudo().get_param("web.base.url", ""),
"X-Title": "Odoo Document Parser",
},
timeout=20,
)
if response.ok:
return {
"type": "ir.actions.client",
"tag": "display_notification",
"params": {
"title": _("OpenRouter Connection"),
"message": _("Connection successful."),
"type": "success",
"sticky": False,
},
}
raise UserError(_("OpenRouter connection failed: %s") % (response.text or response.reason))

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form_document_parser" model="ir.ui.view">
<field name="name">res.config.settings.view.form.document.parser</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="80"/>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//form" position="inside">
<app string="Document Parser" name="document_parser" groups="base.group_system">
<block title="AI Providers" name="document_parser_ai_provider_block">
<setting string="Together AI Key"
help="Primary provider used first for structured document extraction."
id="document_parser_together_ai_key">
<div class="d-flex align-items-center gap-2">
<field name="together_ai_key" password="True" placeholder="together.ai API key"/>
<button name="action_test_together_ai_connection"
string="Test Connection"
type="object"
class="btn btn-secondary"/>
</div>
</setting>
<setting string="OpenRouter AI Key"
help="Fallback provider used when Together AI is unavailable or quota is exhausted."
id="document_parser_openrouter_ai_key">
<div class="d-flex align-items-center gap-2">
<field name="openrouter_ai_key" password="True" placeholder="openrouter API key"/>
<button name="action_test_openrouter_ai_connection"
string="Test Connection"
type="object"
class="btn btn-secondary"/>
</div>
</setting>
</block>
</app>
</xpath>
</field>
</record>
</odoo>

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from . import models
from . import controllers
from . import wizard
# -*- coding: utf-8 -*-
from . import models
from . import controllers
from . import wizard

View File

@ -1,94 +1,94 @@
# -*- coding: utf-8 -*-
{
'name': "Documents",
'summary': "Collect, organize and share documents.",
'description': """
App to upload and manage your documents.
""",
'category': 'Productivity/Documents',
'sequence': 80,
'version': '1.4',
'application': True,
'website': 'https://www.ftprotech.in/',
# any module necessary for this one to work correctly
'depends': ['base', 'mail', 'portal', 'attachment_indexation', 'digest'],
# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'data/digest_data.xml',
'data/mail_template_data.xml',
'data/mail_activity_type_data.xml',
'data/documents_tag_data.xml',
'data/documents_document_data.xml',
'data/ir_config_parameter_data.xml',
'data/documents_tour.xml',
'views/res_config_settings_views.xml',
'views/res_partner_views.xml',
'views/documents_access_views.xml',
'views/documents_document_views.xml',
'views/documents_folder_views.xml',
'views/documents_tag_views.xml',
'views/mail_activity_views.xml',
'views/mail_activity_plan_views.xml',
'views/mail_alias_views.xml',
'views/documents_menu_views.xml',
'views/documents_templates_portal.xml',
'views/documents_templates_share.xml',
'wizard/documents_link_to_record_wizard_views.xml',
'wizard/documents_request_wizard_views.xml',
# Need the `ir.actions.act_window` to exist
'data/ir_actions_server_data.xml',
],
'demo': [
'demo/documents_document_demo.xml',
],
'license': 'OEEL-1',
'assets': {
'web.assets_backend': [
'documents/static/src/model/**/*',
'documents/static/src/scss/documents_views.scss',
'documents/static/src/scss/documents_kanban_view.scss',
'documents/static/src/attachments/**/*',
'documents/static/src/core/**/*',
'documents/static/src/js/**/*',
'documents/static/src/owl/**/*',
'documents/static/src/views/**/*',
('remove', 'documents/static/src/views/activity/**'),
('after', 'web/static/src/core/errors/error_dialogs.xml', 'documents/static/src/web/error_dialog/error_dialog_patch.xml'),
'documents/static/src/web/**/*',
'documents/static/src/components/**/*',
],
'web.assets_backend_lazy': [
'documents/static/src/views/activity/**',
],
'web._assets_primary_variables': [
'documents/static/src/scss/documents.variables.scss',
],
"web.dark_mode_variables": [
('before', 'documents/static/src/scss/documents.variables.scss', 'documents/static/src/scss/documents.variables.dark.scss'),
],
'documents.public_page_assets': [
('include', 'web._assets_helpers'),
('include', 'web._assets_backend_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/bootstrap/scss/_variables-dark.scss',
'web/static/lib/bootstrap/scss/_maps.scss',
('include', 'web._assets_bootstrap_backend'),
'documents/static/src/scss/documents_public_pages.scss',
],
'documents.webclient': [
('include', 'web.assets_backend'),
# documents webclient overrides
'documents/static/src/portal_webclient/**/*',
'web/static/src/start.js',
],
}
}
# -*- coding: utf-8 -*-
{
'name': "Documents",
'summary': "Collect, organize and share documents.",
'description': """
App to upload and manage your documents.
""",
'category': 'Productivity/Documents',
'sequence': 80,
'version': '1.4',
'application': True,
'website': 'https://www.ftprotech.in/',
# any module necessary for this one to work correctly
'depends': ['base', 'mail', 'portal', 'attachment_indexation', 'digest'],
# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'data/digest_data.xml',
'data/mail_template_data.xml',
'data/mail_activity_type_data.xml',
'data/documents_tag_data.xml',
'data/documents_document_data.xml',
'data/ir_config_parameter_data.xml',
'data/documents_tour.xml',
'views/res_config_settings_views.xml',
'views/res_partner_views.xml',
'views/documents_access_views.xml',
'views/documents_document_views.xml',
'views/documents_folder_views.xml',
'views/documents_tag_views.xml',
'views/mail_activity_views.xml',
'views/mail_activity_plan_views.xml',
'views/mail_alias_views.xml',
'views/documents_menu_views.xml',
'views/documents_templates_portal.xml',
'views/documents_templates_share.xml',
'wizard/documents_link_to_record_wizard_views.xml',
'wizard/documents_request_wizard_views.xml',
# Need the `ir.actions.act_window` to exist
'data/ir_actions_server_data.xml',
],
'demo': [
'demo/documents_document_demo.xml',
],
'license': 'OEEL-1',
'assets': {
'web.assets_backend': [
'documents/static/src/model/**/*',
'documents/static/src/scss/documents_views.scss',
'documents/static/src/scss/documents_kanban_view.scss',
'documents/static/src/attachments/**/*',
'documents/static/src/core/**/*',
'documents/static/src/js/**/*',
'documents/static/src/owl/**/*',
'documents/static/src/views/**/*',
('remove', 'documents/static/src/views/activity/**'),
('after', 'web/static/src/core/errors/error_dialogs.xml', 'documents/static/src/web/error_dialog/error_dialog_patch.xml'),
'documents/static/src/web/**/*',
'documents/static/src/components/**/*',
],
'web.assets_backend_lazy': [
'documents/static/src/views/activity/**',
],
'web._assets_primary_variables': [
'documents/static/src/scss/documents.variables.scss',
],
"web.dark_mode_variables": [
('before', 'documents/static/src/scss/documents.variables.scss', 'documents/static/src/scss/documents.variables.dark.scss'),
],
'documents.public_page_assets': [
('include', 'web._assets_helpers'),
('include', 'web._assets_backend_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
'web/static/lib/bootstrap/scss/_variables-dark.scss',
'web/static/lib/bootstrap/scss/_maps.scss',
('include', 'web._assets_bootstrap_backend'),
'documents/static/src/scss/documents_public_pages.scss',
],
'documents.webclient': [
('include', 'web.assets_backend'),
# documents webclient overrides
'documents/static/src/portal_webclient/**/*',
'web/static/src/start.js',
],
}
}

View File

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from . import documents
from . import home
from . import portal
# -*- coding: utf-8 -*-
from . import documents
from . import home
from . import portal

File diff suppressed because it is too large Load Diff

View File

@ -1,86 +1,86 @@
from http import HTTPStatus
from urllib.parse import urlencode
from odoo.http import request, route
from odoo.addons.web.controllers import home as web_home
from odoo.addons.web.controllers.utils import ensure_db
from .documents import ShareRoute
class Home(web_home.Home):
def _web_client_readonly(self):
""" Force a read/write cursor for documents.access """
path = request.httprequest.path
if (
path.startswith('/odoo/documents')
and (request.httprequest.args.get('access_token') or path.removeprefix('/odoo/documents/'))
and request.session.uid
):
return False
return super()._web_client_readonly()
@route(readonly=_web_client_readonly)
def web_client(self, s_action=None, **kw):
""" Handle direct access to a document with a backend URL (/odoo/documents/<access_token>).
It redirects to the document either in:
- the backend if the user is logged and has access to the Documents module
- or a lightweight version of the backend if the user is logged and has not access
to the Document module but well to the documents.document model
- or the document portal otherwise
Goal: Allow to share directly the backend URL of a document.
"""
subpath = kw.get('subpath', '')
access_token = request.params.get('access_token') or subpath.removeprefix('documents/')
if not subpath.startswith('documents') or not access_token or '/' in access_token:
return super().web_client(s_action, **kw)
# This controller should be auth='public' but it actually is
# auth='none' for technical reasons (see super). Those three
# lines restore the public behavior.
ensure_db()
request.update_env(user=request.session.uid)
request.env['ir.http']._authenticate_explicit('public')
# Public/Portal users use the /documents/<access_token> route
if not request.env.user._is_internal():
return request.redirect(
f'/documents/{access_token}',
HTTPStatus.TEMPORARY_REDIRECT,
)
document_sudo = ShareRoute._from_access_token(access_token, follow_shortcut=False)
if not document_sudo:
Redirect = request.env['documents.redirect'].sudo()
if document_sudo := Redirect._get_redirection(access_token):
return request.redirect(
f'/odoo/documents/{document_sudo.access_token}',
HTTPStatus.MOVED_PERMANENTLY,
)
# We want (1) the webclient renders the webclient template and load
# the document action. We also want (2) the router rewrites
# /odoo/documents/<id> to /odoo/documents/<access-token> in the
# URL.
# We redirect on /web so this override does kicks in again,
# super() is loaded and renders the normal home template. We add
# custom fragments so we can load them inside the router and
# rewrite the URL.
query = {}
if request.session.debug:
query['debug'] = request.session.debug
fragment = {
'action': request.env.ref("documents.document_action").id,
'menu_id': request.env.ref('documents.menu_root').id,
'model': 'documents.document',
}
if document_sudo:
fragment.update({
f'documents_init_{key}': value
for key, value
in ShareRoute._documents_get_init_data(document_sudo, request.env.user).items()
})
return request.redirect(f'/web?{urlencode(query)}#{urlencode(fragment)}')
from http import HTTPStatus
from urllib.parse import urlencode
from odoo.http import request, route
from odoo.addons.web.controllers import home as web_home
from odoo.addons.web.controllers.utils import ensure_db
from .documents import ShareRoute
class Home(web_home.Home):
def _web_client_readonly(self):
""" Force a read/write cursor for documents.access """
path = request.httprequest.path
if (
path.startswith('/odoo/documents')
and (request.httprequest.args.get('access_token') or path.removeprefix('/odoo/documents/'))
and request.session.uid
):
return False
return super()._web_client_readonly()
@route(readonly=_web_client_readonly)
def web_client(self, s_action=None, **kw):
""" Handle direct access to a document with a backend URL (/odoo/documents/<access_token>).
It redirects to the document either in:
- the backend if the user is logged and has access to the Documents module
- or a lightweight version of the backend if the user is logged and has not access
to the Document module but well to the documents.document model
- or the document portal otherwise
Goal: Allow to share directly the backend URL of a document.
"""
subpath = kw.get('subpath', '')
access_token = request.params.get('access_token') or subpath.removeprefix('documents/')
if not subpath.startswith('documents') or not access_token or '/' in access_token:
return super().web_client(s_action, **kw)
# This controller should be auth='public' but it actually is
# auth='none' for technical reasons (see super). Those three
# lines restore the public behavior.
ensure_db()
request.update_env(user=request.session.uid)
request.env['ir.http']._authenticate_explicit('public')
# Public/Portal users use the /documents/<access_token> route
if not request.env.user._is_internal():
return request.redirect(
f'/documents/{access_token}',
HTTPStatus.TEMPORARY_REDIRECT,
)
document_sudo = ShareRoute._from_access_token(access_token, follow_shortcut=False)
if not document_sudo:
Redirect = request.env['documents.redirect'].sudo()
if document_sudo := Redirect._get_redirection(access_token):
return request.redirect(
f'/odoo/documents/{document_sudo.access_token}',
HTTPStatus.MOVED_PERMANENTLY,
)
# We want (1) the webclient renders the webclient template and load
# the document action. We also want (2) the router rewrites
# /odoo/documents/<id> to /odoo/documents/<access-token> in the
# URL.
# We redirect on /web so this override does kicks in again,
# super() is loaded and renders the normal home template. We add
# custom fragments so we can load them inside the router and
# rewrite the URL.
query = {}
if request.session.debug:
query['debug'] = request.session.debug
fragment = {
'action': request.env.ref("documents.document_action").id,
'menu_id': request.env.ref('documents.menu_root').id,
'model': 'documents.document',
}
if document_sudo:
fragment.update({
f'documents_init_{key}': value
for key, value
in ShareRoute._documents_get_init_data(document_sudo, request.env.user).items()
})
return request.redirect(f'/web?{urlencode(query)}#{urlencode(fragment)}')

Some files were not shown because too many files have changed in this diff Show More