Compare commits
86 Commits
feature/od
...
feature/sh
| Author | SHA1 | Date |
|---|---|---|
|
|
5e617d3ff8 | |
|
|
625bd67064 | |
|
|
3de90ee7ce | |
|
|
257e05cc34 | |
|
|
80e90ca88a | |
|
|
a1320afd5b | |
|
|
cf0b469b21 | |
|
|
1b34ae2c5c | |
|
|
08569e0ae0 | |
|
|
77827a188f | |
|
|
aecdbab7c2 | |
|
|
f40d78f5dc | |
|
|
3ff97c7c8a | |
|
|
adc4733e15 | |
|
|
a8570dea30 | |
|
|
7f8a627a8c | |
|
|
19669a7a3a | |
|
|
d448713449 | |
|
|
a9a9f08ff9 | |
|
|
7960d1926b | |
|
|
e2c8a25c7b | |
|
|
9ce8c135d0 | |
|
|
8877a4ebc4 | |
|
|
12ec001b38 | |
|
|
064bd90c58 | |
|
|
29af1ebf29 | |
|
|
41f493840b | |
|
|
54541998c5 | |
|
|
68e62956b7 | |
|
|
14e725be4f | |
|
|
adfe801d8e | |
|
|
5c6341d8b7 | |
|
|
71f416e8e8 | |
|
|
b5f643fb18 | |
|
|
3092032fee | |
|
|
f118600ab6 | |
|
|
05bdddc472 | |
|
|
582225e11e | |
|
|
478c1708fb | |
|
|
945aedc0b4 | |
|
|
eb17d717dd | |
|
|
604d556501 | |
|
|
90211776a1 | |
|
|
923304f759 | |
|
|
96493be796 | |
|
|
5460f6c207 | |
|
|
4db7e5ade2 | |
|
|
f2788e025d | |
|
|
0e51ac85e9 | |
|
|
19e5f1db80 | |
|
|
c2e33753bb | |
|
|
f6bfd46f2c | |
|
|
c9746456b8 | |
|
|
b0ec5ee508 | |
|
|
e2d6a8c417 | |
|
|
9c33507a45 | |
|
|
a9a6c683d7 | |
|
|
723dcbe225 | |
|
|
4139e5fa33 | |
|
|
ce93d9601c | |
|
|
1b21175e75 | |
|
|
74526cc1a2 | |
|
|
6a32ac3f37 | |
|
|
fa3833bac3 | |
|
|
73a27d8921 | |
|
|
26923e20b9 | |
|
|
ff806505e2 | |
|
|
b5b276f552 | |
|
|
5d6c2c09aa | |
|
|
4b85cc0f59 | |
|
|
ad5967d420 | |
|
|
53f90a7834 | |
|
|
87824199d0 | |
|
|
92543295d6 | |
|
|
7b6d108ace | |
|
|
6f77059f85 | |
|
|
c93d208990 | |
|
|
bfd7890cbc | |
|
|
d362ef87aa | |
|
|
20d22c1f04 | |
|
|
44e5ee7e2f | |
|
|
0fa84c6d43 | |
|
|
4ce02e58fa | |
|
|
66077d1819 | |
|
|
2dbdb58127 | |
|
|
5f267e96da |
|
|
@ -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"]
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import maintenance_equipment
|
||||
from . import res_company
|
||||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import custom_qrcode_generator
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import equipment_label_layout
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<function model="project.project" name="_sync_all_team_lines_from_members"/>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import project
|
||||
from . import bench_management
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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 > 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import wizard
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>-->
|
||||
|
||||
<!-- <!– Attach Travel - Finance group –>-->
|
||||
<!-- <field name="groups_id" eval="[(4, ref('business_travel_expense_management.group_travel_finance'))]"/>-->
|
||||
<!-- </record>-->
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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'))
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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}."
|
||||
)
|
||||
|
|
@ -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"
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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'}">-->
|
||||
|
||||
<!-- <!– Only LIST here –>-->
|
||||
<!-- <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>
|
||||
|
||||
<!-- <!– Menu –>-->
|
||||
<!-- <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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import trip_reject_wizard
|
||||
|
|
@ -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
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
@ -1 +1 @@
|
|||
from . import models
|
||||
from . import models
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
File diff suppressed because it is too large
Load Diff
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', {})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import disciplinary
|
||||
from . import employee_displane
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
@ -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">-->
|
||||
<!-- <!– <group name="career_hist_sub_menu" string="Disciplinary Actions">–>-->
|
||||
<!-- <!– <field name="employee_name_ids1" string="Manage Incident">–>-->
|
||||
<!-- <!– <list editable="0" create="0">–>-->
|
||||
<!-- <!– <field name="incident_date"/>–>-->
|
||||
<!-- <!– <field name="incident_type"/>–>-->
|
||||
<!-- <!– <field name="incident_sub_type"/>–>-->
|
||||
<!-- <!–<!– <field name="corrective_action_id"/>–>–>-->
|
||||
<!-- <!– </list>–>-->
|
||||
<!-- <!– </field>–>-->
|
||||
<!-- <!– </group>–>-->
|
||||
<!-- <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">-->
|
||||
<!-- <!– <group name="career_hist_sub_menu" string="Disciplinary Actions">–>-->
|
||||
<!-- <!– <field name="employee_name_ids1" string="Manage Incident">–>-->
|
||||
<!-- <!– <list editable="0" create="0">–>-->
|
||||
<!-- <!– <field name="incident_date"/>–>-->
|
||||
<!-- <!– <field name="incident_type"/>–>-->
|
||||
<!-- <!– <field name="incident_sub_type"/>–>-->
|
||||
<!-- <!– <!– <field name="corrective_action_id"/>–>–>-->
|
||||
<!-- <!– </list>–>-->
|
||||
<!-- <!– </field>–>-->
|
||||
<!-- <!– </group>–>-->
|
||||
<!-- <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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
|
|
@ -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"],
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
from . import document_parser_service
|
||||
from . import res_config_settings
|
||||
|
|
@ -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)
|
||||
|
|
@ -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))
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
Loading…
Reference in New Issue