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``.
|
"""Low-level method to send ``notification_type`` and ``message`` to ``target``.
|
||||||
|
|
||||||
Using ``_bus_send()`` from ``bus.listener.mixin`` is recommended for simplicity and
|
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
|
When using ``_sendone`` directly, ``target`` (if str) should not be guessable by an
|
||||||
attacker.
|
attacker.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'category': 'Accounting/Localizations/Point of Sale',
|
'category': 'Accounting/Localizations/Point of Sale',
|
||||||
'description': """
|
'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.
|
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):
|
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
|
""" 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
|
allowing further manipulation of validity issues
|
||||||
|
|
||||||
* survey_wrong: survey does not exist;
|
* survey_wrong: survey does not exist;
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
},
|
},
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'data': [
|
'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/website_security.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/image_library.xml',
|
'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)',
|
'name': 'Consolidated Payslip Grid (OWL)',
|
||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'category': 'Human Resources',
|
'category': 'Human Resources',
|
||||||
'summary': 'Editable Consolidated Payslip Grid (OWL + pqGrid)',
|
'summary': 'Editable Consolidated Payslip Grid (OWL + pqGrid)',
|
||||||
'author': 'Raman Marikanti',
|
'author': 'Raman Marikanti',
|
||||||
'depends': ['hr_payroll', 'web'],
|
'depends': ['hr_payroll', 'web'],
|
||||||
'data': [
|
'data': [
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'views/batch_payslip_view.xml',
|
'views/batch_payslip_view.xml',
|
||||||
],
|
],
|
||||||
'assets': {
|
'assets': {
|
||||||
'web.assets_backend': [
|
'web.assets_backend': [
|
||||||
# Internal module JS and XML files (ensure correct paths within 'static/src')
|
# 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.js',
|
||||||
'consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.xml',
|
'consolidated_batch_payslip/static/src/components/pqgrid_batch_payslip/pqgrid_batch_payslip.xml',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
'installable': True,
|
'installable': True,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,312 +1,312 @@
|
||||||
from odoo import models, fields, api, _
|
from odoo import models, fields, api, _
|
||||||
from odoo.exceptions import UserError, ValidationError
|
from odoo.exceptions import UserError, ValidationError
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HrPayslipRun(models.Model):
|
class HrPayslipRun(models.Model):
|
||||||
_inherit = 'hr.payslip.run'
|
_inherit = 'hr.payslip.run'
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def get_consolidated_attendance_data(self, payslip_run_id):
|
def get_consolidated_attendance_data(self, payslip_run_id):
|
||||||
"""
|
"""
|
||||||
Returns consolidated attendance and leave data for all employees in the payslip run
|
Returns consolidated attendance and leave data for all employees in the payslip run
|
||||||
"""
|
"""
|
||||||
# Get all payslips in this batch
|
# Get all payslips in this batch
|
||||||
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
|
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', payslip_run_id)])
|
||||||
|
|
||||||
if not payslips:
|
if not payslips:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for slip in payslips:
|
for slip in payslips:
|
||||||
employee = slip.employee_id
|
employee = slip.employee_id
|
||||||
contract = slip.contract_id
|
contract = slip.contract_id
|
||||||
|
|
||||||
# Get attendance data
|
# Get attendance data
|
||||||
attendance_days = self._get_attendance_days(slip)
|
attendance_days = self._get_attendance_days(slip)
|
||||||
worked_days = self._get_worked_days(slip)
|
worked_days = self._get_worked_days(slip)
|
||||||
leave_days = self._get_leave_days(slip)
|
leave_days = self._get_leave_days(slip)
|
||||||
lop_days = self._get_lop_days(slip)
|
lop_days = self._get_lop_days(slip)
|
||||||
|
|
||||||
# Get leave balances
|
# Get leave balances
|
||||||
leave_balances = self._get_leave_balances(employee,slip.date_from,slip.date_to)
|
leave_balances = self._get_leave_balances(employee,slip.date_from,slip.date_to)
|
||||||
leave_taken = self._get_leave_taken(slip)
|
leave_taken = self._get_leave_taken(slip)
|
||||||
|
|
||||||
result.append({
|
result.append({
|
||||||
'id': slip.id,
|
'id': slip.id,
|
||||||
'employee_id': (employee.id, employee.name),
|
'employee_id': (employee.id, employee.name),
|
||||||
'employee_code': employee.employee_id or '',
|
'employee_code': employee.employee_id or '',
|
||||||
'department_id': (employee.department_id.id,
|
'department_id': (employee.department_id.id,
|
||||||
employee.department_id.name) if employee.department_id else False,
|
employee.department_id.name) if employee.department_id else False,
|
||||||
'total_days': (slip.date_to - slip.date_from).days + 1,
|
'total_days': (slip.date_to - slip.date_from).days + 1,
|
||||||
'worked_days': worked_days,
|
'worked_days': worked_days,
|
||||||
'attendance_days': attendance_days,
|
'attendance_days': attendance_days,
|
||||||
'leave_days': leave_days,
|
'leave_days': leave_days,
|
||||||
'lop_days': lop_days,
|
'lop_days': lop_days,
|
||||||
'doj':contract.date_start,
|
'doj':contract.date_start,
|
||||||
'birthday':employee.birthday,
|
'birthday':employee.birthday,
|
||||||
'bank': employee.bank_account_id.display_name if employee.bank_account_id else '-',
|
'bank': employee.bank_account_id.display_name if employee.bank_account_id else '-',
|
||||||
'sick_leave_balance': leave_balances.get('LEAVE110', 0),
|
'sick_leave_balance': leave_balances.get('LEAVE110', 0),
|
||||||
'casual_leave_balance': leave_balances.get('LEAVE120', 0),
|
'casual_leave_balance': leave_balances.get('LEAVE120', 0),
|
||||||
'privilege_leave_balance': leave_balances.get('LEAVE100', 0),
|
'privilege_leave_balance': leave_balances.get('LEAVE100', 0),
|
||||||
'sick_leave_taken': leave_taken.get('sick', 0),
|
'sick_leave_taken': leave_taken.get('sick', 0),
|
||||||
'casual_leave_taken': leave_taken.get('casual', 0),
|
'casual_leave_taken': leave_taken.get('casual', 0),
|
||||||
'privilege_leave_taken': leave_taken.get('privilege', 0),
|
'privilege_leave_taken': leave_taken.get('privilege', 0),
|
||||||
'state': slip.state,
|
'state': slip.state,
|
||||||
'lines':self.get_payslip_lines_data(slip),
|
'lines':self.get_payslip_lines_data(slip),
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def sub_columns(self,payslip_run_id):
|
def sub_columns(self,payslip_run_id):
|
||||||
payslips = self.env['hr.payslip'].search([('payslip_run_id', '=', 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)
|
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}
|
code_name_dict = {line.code+line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code : line.name for line in names}
|
||||||
|
|
||||||
columns = []
|
columns = []
|
||||||
for code, name in code_name_dict.items():
|
for code, name in code_name_dict.items():
|
||||||
columns.append({
|
columns.append({
|
||||||
'title': name,
|
'title': name,
|
||||||
'dataIndx': code,
|
'dataIndx': code,
|
||||||
'width': 150,
|
'width': 150,
|
||||||
'editable': False,
|
'editable': False,
|
||||||
'summary': {'type': "sum_"},
|
'summary': {'type': "sum_"},
|
||||||
})
|
})
|
||||||
return columns
|
return columns
|
||||||
|
|
||||||
def save_consolidated_attendance_data(self, payslip_run_id, data):
|
def save_consolidated_attendance_data(self, payslip_run_id, data):
|
||||||
"""
|
"""
|
||||||
Saves the edited attendance and leave data from the grid
|
Saves the edited attendance and leave data from the grid
|
||||||
"""
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
|
|
||||||
for item in data:
|
for item in data:
|
||||||
slip = self.env['hr.payslip'].browse(item['id'])
|
slip = self.env['hr.payslip'].browse(item['id'])
|
||||||
if slip.state != 'draft':
|
if slip.state != 'draft':
|
||||||
raise UserError(_("Cannot edit payslip in %s state") % slip.state)
|
raise UserError(_("Cannot edit payslip in %s state") % slip.state)
|
||||||
|
|
||||||
# Update LOP days
|
# Update LOP days
|
||||||
if 'lop_days' in item:
|
if 'lop_days' in item:
|
||||||
self._update_lop_days(slip, float(item['lop_days']))
|
self._update_lop_days(slip, float(item['lop_days']))
|
||||||
|
|
||||||
# Update leave days taken
|
# Update leave days taken
|
||||||
leave_updates = {}
|
leave_updates = {}
|
||||||
if 'sick_leave_taken' in item:
|
if 'sick_leave_taken' in item:
|
||||||
leave_updates['sick'] = float(item['sick_leave_taken'])
|
leave_updates['sick'] = float(item['sick_leave_taken'])
|
||||||
if 'casual_leave_taken' in item:
|
if 'casual_leave_taken' in item:
|
||||||
leave_updates['casual'] = float(item['casual_leave_taken'])
|
leave_updates['casual'] = float(item['casual_leave_taken'])
|
||||||
if 'privilege_leave_taken' in item:
|
if 'privilege_leave_taken' in item:
|
||||||
leave_updates['privilege'] = float(item['privilege_leave_taken'])
|
leave_updates['privilege'] = float(item['privilege_leave_taken'])
|
||||||
|
|
||||||
if leave_updates:
|
if leave_updates:
|
||||||
self._update_leave_taken(slip, leave_updates)
|
self._update_leave_taken(slip, leave_updates)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def recalculate_lop_days(self, payslip_run_id):
|
def recalculate_lop_days(self, payslip_run_id):
|
||||||
"""
|
"""
|
||||||
Recalculates LOP days for all payslips in the batch based on attendance
|
Recalculates LOP days for all payslips in the batch based on attendance
|
||||||
"""
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
payslips = self.env['hr.payslip'].search([
|
payslips = self.env['hr.payslip'].search([
|
||||||
('payslip_run_id', '=', payslip_run_id),
|
('payslip_run_id', '=', payslip_run_id),
|
||||||
('state', '=', 'draft')
|
('state', '=', 'draft')
|
||||||
])
|
])
|
||||||
|
|
||||||
for slip in payslips:
|
for slip in payslips:
|
||||||
attendance_days = self._get_attendance_days(slip)
|
attendance_days = self._get_attendance_days(slip)
|
||||||
expected_days = (slip.date_to - slip.date_from).days + 1
|
expected_days = (slip.date_to - slip.date_from).days + 1
|
||||||
lop_days = expected_days - attendance_days
|
lop_days = expected_days - attendance_days
|
||||||
self._update_lop_days(slip, lop_days)
|
self._update_lop_days(slip, lop_days)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def validate_all_attendance_data(self, payslip_run_id):
|
def validate_all_attendance_data(self, payslip_run_id):
|
||||||
"""
|
"""
|
||||||
Marks all payslips in the batch as validated
|
Marks all payslips in the batch as validated
|
||||||
"""
|
"""
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
payslips = self.env['hr.payslip'].search([
|
payslips = self.env['hr.payslip'].search([
|
||||||
('payslip_run_id', '=', payslip_run_id),
|
('payslip_run_id', '=', payslip_run_id),
|
||||||
('state', '=', 'draft')
|
('state', '=', 'draft')
|
||||||
])
|
])
|
||||||
|
|
||||||
if not payslips:
|
if not payslips:
|
||||||
raise UserError(_("No draft payslips found in this batch"))
|
raise UserError(_("No draft payslips found in this batch"))
|
||||||
|
|
||||||
payslips.write({'state': 'verify'})
|
payslips.write({'state': 'verify'})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Helper methods
|
# Helper methods
|
||||||
def _get_attendance_days(self, payslip):
|
def _get_attendance_days(self, payslip):
|
||||||
"""
|
"""
|
||||||
Returns number of days employee was present (based on attendance records)
|
Returns number of days employee was present (based on attendance records)
|
||||||
"""
|
"""
|
||||||
attendance_records = self.env['hr.attendance'].search([
|
attendance_records = self.env['hr.attendance'].search([
|
||||||
('employee_id', '=', payslip.employee_id.id),
|
('employee_id', '=', payslip.employee_id.id),
|
||||||
('check_in', '>=', payslip.date_from),
|
('check_in', '>=', payslip.date_from),
|
||||||
('check_in', '<=', payslip.date_to)
|
('check_in', '<=', payslip.date_to)
|
||||||
])
|
])
|
||||||
|
|
||||||
# Group by day
|
# Group by day
|
||||||
unique_days = set()
|
unique_days = set()
|
||||||
for att in attendance_records:
|
for att in attendance_records:
|
||||||
unique_days.add(att.check_in.date())
|
unique_days.add(att.check_in.date())
|
||||||
|
|
||||||
return len(unique_days)
|
return len(unique_days)
|
||||||
|
|
||||||
def _get_worked_days(self, payslip):
|
def _get_worked_days(self, payslip):
|
||||||
"""
|
"""
|
||||||
Returns number of working days (excluding weekends and holidays)
|
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
|
return payslip._get_worked_days_line_number_of_days('WORK100') # Assuming WORK100 is your work code
|
||||||
|
|
||||||
def get_payslip_lines_data(self, payslip_id):
|
def get_payslip_lines_data(self, payslip_id):
|
||||||
list = []
|
list = []
|
||||||
for line in payslip_id.line_ids:
|
for line in payslip_id.line_ids:
|
||||||
list.append({
|
list.append({
|
||||||
'name': line.name,
|
'name': line.name,
|
||||||
'code': line.code + line.name.replace(" ", "_")if line.code in ['REIMBURSEMENT','DEDUCTION'] else line.code,
|
'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,
|
'category_id': line.category_id.name if line.category_id else False,
|
||||||
'amount': line.amount,
|
'amount': line.amount,
|
||||||
'quantity': line.quantity,
|
'quantity': line.quantity,
|
||||||
'rate': line.rate
|
'rate': line.rate
|
||||||
})
|
})
|
||||||
return list
|
return list
|
||||||
|
|
||||||
def _get_leave_days(self, payslip):
|
def _get_leave_days(self, payslip):
|
||||||
"""
|
"""
|
||||||
Returns total leave days taken in this period
|
Returns total leave days taken in this period
|
||||||
"""
|
"""
|
||||||
leave_lines = payslip.worked_days_line_ids.filtered(
|
leave_lines = payslip.worked_days_line_ids.filtered(
|
||||||
lambda l: l.code in ['LEAVE110', 'LEAVE90', 'LEAVE100', 'LEAVE120'] # Your leave codes
|
lambda l: l.code in ['LEAVE110', 'LEAVE90', 'LEAVE100', 'LEAVE120'] # Your leave codes
|
||||||
)
|
)
|
||||||
return sum(leave_lines.mapped('number_of_days'))
|
return sum(leave_lines.mapped('number_of_days'))
|
||||||
|
|
||||||
def _get_lop_days(self, payslip):
|
def _get_lop_days(self, payslip):
|
||||||
"""
|
"""
|
||||||
Returns LOP days from payslip
|
Returns LOP days from payslip
|
||||||
"""
|
"""
|
||||||
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LEAVE90')
|
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LEAVE90')
|
||||||
return lop_line.number_of_days if lop_line else 0
|
return lop_line.number_of_days if lop_line else 0
|
||||||
|
|
||||||
|
|
||||||
def _get_leave_taken(self, payslip):
|
def _get_leave_taken(self, payslip):
|
||||||
"""
|
"""
|
||||||
Returns leave days taken in this payslip period
|
Returns leave days taken in this payslip period
|
||||||
"""
|
"""
|
||||||
leave_lines = payslip.worked_days_line_ids
|
leave_lines = payslip.worked_days_line_ids
|
||||||
return {
|
return {
|
||||||
'sick': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE110').mapped('number_of_days')),
|
'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')),
|
'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')),
|
'privilege': sum(leave_lines.filtered(lambda l: l.code == 'LEAVE100').mapped('number_of_days')),
|
||||||
}
|
}
|
||||||
|
|
||||||
def _update_lop_days(self, payslip, days):
|
def _update_lop_days(self, payslip, days):
|
||||||
"""
|
"""
|
||||||
Updates LOP days in the payslip
|
Updates LOP days in the payslip
|
||||||
"""
|
"""
|
||||||
WorkedDays = self.env['hr.payslip.worked_days']
|
WorkedDays = self.env['hr.payslip.worked_days']
|
||||||
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LOP')
|
lop_line = payslip.worked_days_line_ids.filtered(lambda l: l.code == 'LOP')
|
||||||
|
|
||||||
if lop_line:
|
if lop_line:
|
||||||
if days > 0:
|
if days > 0:
|
||||||
lop_line.write({'number_of_days': days})
|
lop_line.write({'number_of_days': days})
|
||||||
else:
|
else:
|
||||||
lop_line.unlink()
|
lop_line.unlink()
|
||||||
elif days > 0:
|
elif days > 0:
|
||||||
WorkedDays.create({
|
WorkedDays.create({
|
||||||
'payslip_id': payslip.id,
|
'payslip_id': payslip.id,
|
||||||
'name': _('Loss of Pay'),
|
'name': _('Loss of Pay'),
|
||||||
'code': 'LOP',
|
'code': 'LOP',
|
||||||
'number_of_days': days,
|
'number_of_days': days,
|
||||||
'number_of_hours': days * 8, # Assuming 8-hour work day
|
'number_of_hours': days * 8, # Assuming 8-hour work day
|
||||||
'contract_id': payslip.contract_id.id,
|
'contract_id': payslip.contract_id.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
def _get_leave_balances(self, employee, date_from, date_to):
|
def _get_leave_balances(self, employee, date_from, date_to):
|
||||||
Leave = self.env['hr.leave']
|
Leave = self.env['hr.leave']
|
||||||
Allocation = self.env['hr.leave.allocation']
|
Allocation = self.env['hr.leave.allocation']
|
||||||
leave_types = self.env['hr.leave.type'].search([])
|
leave_types = self.env['hr.leave.type'].search([])
|
||||||
|
|
||||||
balances = {}
|
balances = {}
|
||||||
for leave_type in leave_types:
|
for leave_type in leave_types:
|
||||||
# Approved allocations within or before payslip period
|
# Approved allocations within or before payslip period
|
||||||
allocations = Allocation.search([
|
allocations = Allocation.search([
|
||||||
('employee_id', '=', employee.id),
|
('employee_id', '=', employee.id),
|
||||||
('state', '=', 'validate'),
|
('state', '=', 'validate'),
|
||||||
('holiday_status_id', '=', leave_type.id),
|
('holiday_status_id', '=', leave_type.id),
|
||||||
('date_from', '<=', str(date_to)), # Allocation should be active during payslip
|
('date_from', '<=', str(date_to)), # Allocation should be active during payslip
|
||||||
])
|
])
|
||||||
allocated = sum(alloc.number_of_days for alloc in allocations)
|
allocated = sum(alloc.number_of_days for alloc in allocations)
|
||||||
|
|
||||||
# Approved leaves within the payslip period
|
# Approved leaves within the payslip period
|
||||||
leaves = Leave.search([
|
leaves = Leave.search([
|
||||||
('employee_id', '=', employee.id),
|
('employee_id', '=', employee.id),
|
||||||
('state', '=', 'validate'),
|
('state', '=', 'validate'),
|
||||||
('holiday_status_id', '=', leave_type.id),
|
('holiday_status_id', '=', leave_type.id),
|
||||||
('request_date_to', '<=', str(date_to))
|
('request_date_to', '<=', str(date_to))
|
||||||
])
|
])
|
||||||
used = sum(leave.number_of_days for leave in leaves)
|
used = sum(leave.number_of_days for leave in leaves)
|
||||||
|
|
||||||
# Key: leave code or fallback to name
|
# Key: leave code or fallback to name
|
||||||
code = leave_type.work_entry_type_id.code
|
code = leave_type.work_entry_type_id.code
|
||||||
balances[code] = allocated - used
|
balances[code] = allocated - used
|
||||||
|
|
||||||
return balances
|
return balances
|
||||||
|
|
||||||
def _update_leave_taken(self, payslip, leave_data):
|
def _update_leave_taken(self, payslip, leave_data):
|
||||||
"""
|
"""
|
||||||
Updates leave days taken in the payslip
|
Updates leave days taken in the payslip
|
||||||
"""
|
"""
|
||||||
WorkedDays = self.env['hr.payslip.worked_days']
|
WorkedDays = self.env['hr.payslip.worked_days']
|
||||||
|
|
||||||
for leave_type, days in leave_data.items():
|
for leave_type, days in leave_data.items():
|
||||||
code = leave_type.upper()
|
code = leave_type.upper()
|
||||||
line = payslip.worked_days_line_ids.filtered(lambda l: l.code == code)
|
line = payslip.worked_days_line_ids.filtered(lambda l: l.code == code)
|
||||||
|
|
||||||
|
|
||||||
if line:
|
if line:
|
||||||
if days > 0:
|
if days > 0:
|
||||||
line.write({'number_of_days': days})
|
line.write({'number_of_days': days})
|
||||||
else:
|
else:
|
||||||
line.unlink()
|
line.unlink()
|
||||||
elif days > 0:
|
elif days > 0:
|
||||||
WorkedDays.create({
|
WorkedDays.create({
|
||||||
'payslip_id': payslip.id,
|
'payslip_id': payslip.id,
|
||||||
'name': _(leave_type.capitalize() + ' Leave'),
|
'name': _(leave_type.capitalize() + ' Leave'),
|
||||||
'code': code,
|
'code': code,
|
||||||
'number_of_days': days,
|
'number_of_days': days,
|
||||||
'number_of_hours': days * 8, # Assuming 8-hour work day
|
'number_of_hours': days * 8, # Assuming 8-hour work day
|
||||||
'contract_id': payslip.contract_id.id,
|
'contract_id': payslip.contract_id.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HrPayslip(models.Model):
|
class HrPayslip(models.Model):
|
||||||
_inherit = 'hr.payslip'
|
_inherit = 'hr.payslip'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_payslip_lines_data(self, payslip_id):
|
def get_payslip_lines_data(self, payslip_id):
|
||||||
payslip = self.browse(payslip_id)
|
payslip = self.browse(payslip_id)
|
||||||
return [{
|
return [{
|
||||||
'name': line.name,
|
'name': line.name,
|
||||||
'code': line.code,
|
'code': line.code,
|
||||||
'category_id': (line.category_id.id, line.category_id.name),
|
'category_id': (line.category_id.id, line.category_id.name),
|
||||||
'amount': line.amount,
|
'amount': line.amount,
|
||||||
'quantity': line.quantity,
|
'quantity': line.quantity,
|
||||||
'rate': line.rate
|
'rate': line.rate
|
||||||
} for line in payslip.line_ids]
|
} for line in payslip.line_ids]
|
||||||
|
|
||||||
def action_open_payslips(self):
|
def action_open_payslips(self):
|
||||||
self.ensure_one()
|
self.ensure_one()
|
||||||
action = self.env["ir.actions.actions"]._for_xml_id("hr_payroll.action_view_hr_payslip_month_form")
|
action = self.env["ir.actions.actions"]._for_xml_id("hr_payroll.action_view_hr_payslip_month_form")
|
||||||
action['views'] = [[False, "form"]]
|
action['views'] = [[False, "form"]]
|
||||||
action['res_id'] = self.id
|
action['res_id'] = self.id
|
||||||
action['target'] = 'new'
|
action['target'] = 'new'
|
||||||
return action
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<templates xml:space="preserve">
|
<templates xml:space="preserve">
|
||||||
<t t-name="ConsolidatedPayslipGrid" owl="1">
|
<t t-name="ConsolidatedPayslipGrid" owl="1">
|
||||||
<div t-ref="gridContainer" style="width: 100%; height: 600px;"></div>
|
<div t-ref="gridContainer" style="width: 100%; height: 600px;"></div>
|
||||||
</t>
|
</t>
|
||||||
</templates>
|
</templates>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
<odoo>
|
<odoo>
|
||||||
<record id="view_hr_payslip_run_form_inherit" model="ir.ui.view">
|
<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="name">hr.payslip.run.form.inherit.consolidated.pqgrid.owl</field>
|
||||||
<field name="model">hr.payslip.run</field>
|
<field name="model">hr.payslip.run</field>
|
||||||
<field name="inherit_id" ref="hr_payroll.hr_payslip_run_form"/>
|
<field name="inherit_id" ref="hr_payroll.hr_payslip_run_form"/>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<xpath expr="//sheet" position="inside">
|
<xpath expr="//sheet" position="inside">
|
||||||
<notebook>
|
<notebook>
|
||||||
<page string="Consolidated Payslip">
|
<page string="Consolidated Payslip">
|
||||||
<div class="o_consolidated_grid" style="height:600px; margin-top:10px;">
|
<div class="o_consolidated_grid" style="height:600px; margin-top:10px;">
|
||||||
<widget name="ConsolidatedPayslipGrid" batchId="context.active_id"/>
|
<widget name="ConsolidatedPayslipGrid" batchId="context.active_id"/>
|
||||||
</div>
|
</div>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
</xpath>
|
</xpath>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,22 @@
|
||||||
{
|
{
|
||||||
'name': 'CWF Timesheet Update',
|
'name': 'CWF Timesheet Update',
|
||||||
'version': '1.0',
|
'version': '1.0',
|
||||||
'category': 'Human Resources',
|
'category': 'Human Resources',
|
||||||
'summary': 'Manage and update weekly timesheets for CWF department',
|
'summary': 'Manage and update weekly timesheets for CWF department',
|
||||||
'author': 'Your Name or Company',
|
'author': 'Your Name or Company',
|
||||||
'depends': ['hr_attendance_extended','web', 'mail', 'base','hr_emp_dashboard','hr_employee_extended'],
|
'depends': ['hr_attendance_extended','web', 'mail', 'base','hr_emp_dashboard','hr_employee_extended'],
|
||||||
'data': [
|
'data': [
|
||||||
# 'views/timesheet_form.xml',
|
# 'views/timesheet_form.xml',
|
||||||
'security/security.xml',
|
'security/security.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'views/timesheet_view.xml',
|
'views/timesheet_view.xml',
|
||||||
'views/timesheet_weekly_view.xml',
|
'views/timesheet_weekly_view.xml',
|
||||||
'data/email_template.xml',
|
'data/email_template.xml',
|
||||||
],
|
],
|
||||||
'assets': {
|
'assets': {
|
||||||
'web.assets_backend': [
|
'web.assets_backend': [
|
||||||
'cwf_timesheet/static/src/js/timesheet_form.js',
|
'cwf_timesheet/static/src/js/timesheet_form.js',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'application': True,
|
'application': True,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
from odoo import http
|
from odoo import http
|
||||||
from odoo.http import request
|
from odoo.http import request
|
||||||
|
|
||||||
class TimesheetController(http.Controller):
|
class TimesheetController(http.Controller):
|
||||||
|
|
||||||
@http.route('/timesheet/form', auth='user', website=True)
|
@http.route('/timesheet/form', auth='user', website=True)
|
||||||
def timesheet_form(self, **kw):
|
def timesheet_form(self, **kw):
|
||||||
# This will render the template for the timesheet form
|
# This will render the template for the timesheet form
|
||||||
return request.render('timesheet_form', {})
|
return request.render('timesheet_form', {})
|
||||||
|
|
|
||||||
|
|
@ -1,45 +1,45 @@
|
||||||
<odoo>
|
<odoo>
|
||||||
<data noupdate="0">
|
<data noupdate="0">
|
||||||
<record id="email_template_timesheet_weekly_update" model="mail.template">
|
<record id="email_template_timesheet_weekly_update" model="mail.template">
|
||||||
<field name="name">Timesheet Update Reminder</field>
|
<field name="name">Timesheet Update Reminder</field>
|
||||||
<field name="model_id" ref="cwf_timesheet.model_cwf_weekly_timesheet"/>
|
<field name="model_id" ref="cwf_timesheet.model_cwf_weekly_timesheet"/>
|
||||||
<field name="email_from">{{ user.email_formatted }}</field>
|
<field name="email_from">{{ user.email_formatted }}</field>
|
||||||
<field name="email_to">{{ object.employee_id.user_id.email }}</field>
|
<field name="email_to">{{ object.employee_id.user_id.email }}</field>
|
||||||
<field name="subject">Reminder: Update Your Weekly Timesheet</field>
|
<field name="subject">Reminder: Update Your Weekly Timesheet</field>
|
||||||
<field name="description">
|
<field name="description">
|
||||||
Reminder to employee to update their weekly timesheet.
|
Reminder to employee to update their weekly timesheet.
|
||||||
</field>
|
</field>
|
||||||
<field name="body_html" type="html">
|
<field name="body_html" type="html">
|
||||||
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
<p style="margin: 0px; padding: 0px; font-size: 13px;">
|
||||||
Dear <t t-esc="ctx['employee_name']">Employee</t>,
|
Dear <t t-esc="ctx['employee_name']">Employee</t>,
|
||||||
<br/>
|
<br/>
|
||||||
<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
|
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>
|
<strong>
|
||||||
<t t-esc="ctx['week_from']"/>
|
<t t-esc="ctx['week_from']"/>
|
||||||
</strong>
|
</strong>
|
||||||
to
|
to
|
||||||
<strong>
|
<strong>
|
||||||
<t t-esc="ctx['week_to']"/>
|
<t t-esc="ctx['week_to']"/>
|
||||||
</strong>.
|
</strong>.
|
||||||
Timely updates are crucial for maintaining accurate records and ensuring smooth processing.
|
Timely updates are crucial for maintaining accurate records and ensuring smooth processing.
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
To make things easier, you can use the link below to update your timesheet:
|
To make things easier, you can use the link below to update your timesheet:
|
||||||
<br/>
|
<br/>
|
||||||
<a href="https://ftprotech.in/odoo/action-261" class="cta-button" target="_blank">Update Timesheet</a>
|
<a href="https://ftprotech.in/odoo/action-261" class="cta-button" target="_blank">Update Timesheet</a>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
Thank you for your attention.
|
Thank you for your attention.
|
||||||
<br/>
|
<br/>
|
||||||
Best regards,
|
Best regards,
|
||||||
<br/>
|
<br/>
|
||||||
<strong>Fast Track Project Pvt Ltd.</strong>
|
<strong>Fast Track Project Pvt Ltd.</strong>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="https://ftprotech.in/" target="_blank">Visit our site</a> for more information.
|
<a href="https://ftprotech.in/" target="_blank">Visit our site</a> for more information.
|
||||||
</p>
|
</p>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
</data>
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -1,399 +1,398 @@
|
||||||
from odoo import models, fields, api
|
from odoo import models, fields, api
|
||||||
from odoo.exceptions import ValidationError, UserError
|
from odoo.exceptions import ValidationError, UserError
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
from odoo import _
|
from odoo import _
|
||||||
from calendar import month_name, month
|
from calendar import month_name, month
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
class CwfTimesheetYearly(models.Model):
|
class CwfTimesheetYearly(models.Model):
|
||||||
_name = 'cwf.timesheet.calendar'
|
_name = 'cwf.timesheet.calendar'
|
||||||
_description = "CWF Timesheet Calendar"
|
_description = "CWF Timesheet Calendar"
|
||||||
_rec_name = 'name'
|
_rec_name = 'name'
|
||||||
|
|
||||||
name = fields.Char(string='Year Name', required=True)
|
name = fields.Char(string='Year Name', required=True)
|
||||||
week_period = fields.One2many('cwf.timesheet','cwf_calendar_id')
|
week_period = fields.One2many('cwf.timesheet','cwf_calendar_id')
|
||||||
|
|
||||||
_sql_constraints = [
|
_sql_constraints = [
|
||||||
('unique_year', 'unique(name)', 'The year must be unique!')
|
('unique_year', 'unique(name)', 'The year must be unique!')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@api.constrains('name')
|
@api.constrains('name')
|
||||||
def _check_year_format(self):
|
def _check_year_format(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
if not record.name.isdigit() or len(record.name) != 4:
|
if not record.name.isdigit() or len(record.name) != 4:
|
||||||
raise ValidationError("Year Name must be a 4-digit number.")
|
raise ValidationError("Year Name must be a 4-digit number.")
|
||||||
|
|
||||||
def generate_week_period(self):
|
def generate_week_period(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
record.week_period.unlink()
|
record.week_period.unlink()
|
||||||
year = int(record.name)
|
year = int(record.name)
|
||||||
|
|
||||||
# Find the first Monday of the year
|
# Find the first Monday of the year
|
||||||
start_date = datetime(year, 1, 1)
|
start_date = datetime(year, 1, 1)
|
||||||
while start_date.weekday() != 0: # Monday is 0 in weekday()
|
while start_date.weekday() != 0: # Monday is 0 in weekday()
|
||||||
start_date += timedelta(days=1)
|
start_date += timedelta(days=1)
|
||||||
|
|
||||||
# Generate weeks from Monday to Sunday
|
# Generate weeks from Monday to Sunday
|
||||||
while start_date.year == year or (start_date - timedelta(days=1)).year == year:
|
while start_date.year == year or (start_date - timedelta(days=1)).year == year:
|
||||||
end_date = start_date + timedelta(days=6)
|
end_date = start_date + timedelta(days=6)
|
||||||
|
|
||||||
self.env['cwf.timesheet'].create({
|
self.env['cwf.timesheet'].create({
|
||||||
'name': f'Week {start_date.strftime("%W")}, {year}',
|
'name': f'Week {start_date.strftime("%W")}, {year}',
|
||||||
'week_start_date': start_date.date(),
|
'week_start_date': start_date.date(),
|
||||||
'week_end_date': end_date.date(),
|
'week_end_date': end_date.date(),
|
||||||
'cwf_calendar_id': record.id,
|
'cwf_calendar_id': record.id,
|
||||||
})
|
})
|
||||||
|
|
||||||
start_date += timedelta(days=7)
|
start_date += timedelta(days=7)
|
||||||
|
|
||||||
def action_generate_weeks(self):
|
def action_generate_weeks(self):
|
||||||
self.generate_week_period()
|
self.generate_week_period()
|
||||||
return {
|
return {
|
||||||
'type': 'ir.actions.client',
|
'type': 'ir.actions.client',
|
||||||
'tag': 'reload',
|
'tag': 'reload',
|
||||||
}
|
}
|
||||||
|
|
||||||
class CwfTimesheet(models.Model):
|
class CwfTimesheet(models.Model):
|
||||||
_name = 'cwf.timesheet'
|
_name = 'cwf.timesheet'
|
||||||
_description = 'CWF Weekly Timesheet'
|
_description = 'CWF Weekly Timesheet'
|
||||||
_rec_name = 'name'
|
_rec_name = 'name'
|
||||||
|
|
||||||
name = fields.Char(string='Week Name', required=True)
|
name = fields.Char(string='Week Name', required=True)
|
||||||
week_start_date = fields.Date(string='Week Start Date', required=True)
|
week_start_date = fields.Date(string='Week Start Date', required=True)
|
||||||
week_end_date = fields.Date(string='Week End Date', required=True)
|
week_end_date = fields.Date(string='Week End Date', required=True)
|
||||||
status = fields.Selection([
|
status = fields.Selection([
|
||||||
('draft', 'Draft'),
|
('draft', 'Draft'),
|
||||||
('submitted', 'Submitted')
|
('submitted', 'Submitted')
|
||||||
], default='draft', string='Status')
|
], default='draft', string='Status')
|
||||||
lines = fields.One2many('cwf.timesheet.line','week_id')
|
lines = fields.One2many('cwf.timesheet.line','week_id')
|
||||||
cwf_calendar_id = fields.Many2one('cwf.timesheet.calendar')
|
cwf_calendar_id = fields.Many2one('cwf.timesheet.calendar')
|
||||||
start_month = fields.Selection(
|
start_month = fields.Selection(
|
||||||
[(str(i), month_name[i]) for i in range(1, 13)],
|
[(str(i), month_name[i]) for i in range(1, 13)],
|
||||||
string='Start Month',
|
string='Start Month',
|
||||||
compute='_compute_months',
|
compute='_compute_months',
|
||||||
store=True
|
store=True
|
||||||
)
|
)
|
||||||
|
|
||||||
end_month = fields.Selection(
|
end_month = fields.Selection(
|
||||||
[(str(i), month_name[i]) for i in range(1, 13)],
|
[(str(i), month_name[i]) for i in range(1, 13)],
|
||||||
string='End Month',
|
string='End Month',
|
||||||
compute='_compute_months',
|
compute='_compute_months',
|
||||||
store=True
|
store=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.depends('week_start_date', 'week_end_date')
|
@api.depends('week_start_date', 'week_end_date')
|
||||||
def _compute_months(self):
|
def _compute_months(self):
|
||||||
for rec in self:
|
for rec in self:
|
||||||
if rec.week_start_date:
|
if rec.week_start_date:
|
||||||
rec.start_month = str(rec.week_start_date.month)
|
rec.start_month = str(rec.week_start_date.month)
|
||||||
else:
|
else:
|
||||||
rec.start_month = False
|
rec.start_month = False
|
||||||
|
|
||||||
if rec.week_end_date:
|
if rec.week_end_date:
|
||||||
rec.end_month = str(rec.week_end_date.month)
|
rec.end_month = str(rec.week_end_date.month)
|
||||||
else:
|
else:
|
||||||
rec.end_month = False
|
rec.end_month = False
|
||||||
|
|
||||||
@api.depends('name','week_start_date','week_end_date')
|
@api.depends('name','week_start_date','week_end_date')
|
||||||
def _compute_display_name(self):
|
def _compute_display_name(self):
|
||||||
for rec in 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') )
|
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):
|
def send_timesheet_update_email(self):
|
||||||
template = self.env.ref('cwf_timesheet.email_template_timesheet_weekly_update')
|
template = self.env.ref('cwf_timesheet.email_template_timesheet_weekly_update')
|
||||||
# Ensure that we have a valid employee email
|
# Ensure that we have a valid employee email
|
||||||
current_date = fields.Date.from_string(self.week_start_date)
|
current_date = fields.Date.from_string(self.week_start_date)
|
||||||
end_date = fields.Date.from_string(self.week_end_date)
|
end_date = fields.Date.from_string(self.week_end_date)
|
||||||
|
|
||||||
if current_date > end_date:
|
if current_date > end_date:
|
||||||
raise UserError('The start date cannot be after the end date.')
|
raise UserError('The start date cannot be after the end date.')
|
||||||
|
|
||||||
# Get all employees in the department
|
# Get all employees in the department
|
||||||
external_group_id = self.env.ref("hr_employee_extended.group_external_user")
|
external_group_id = self.env.ref("hr_employee_extended.group_external_user")
|
||||||
users = self.env["res.users"].search([("groups_id", "=", external_group_id.id)])
|
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)])
|
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
|
||||||
# Loop through each day of the week and create timesheet lines for each employee
|
while current_date <= end_date:
|
||||||
while current_date <= end_date:
|
for employee in employees:
|
||||||
for employee in employees:
|
existing_record = self.env['cwf.weekly.timesheet'].sudo().search([
|
||||||
existing_record = self.env['cwf.weekly.timesheet'].sudo().search([
|
('week_id', '=', self.id),
|
||||||
('week_id', '=', self.id),
|
('employee_id', '=', employee.id)
|
||||||
('employee_id', '=', employee.id)
|
], limit=1)
|
||||||
], limit=1)
|
if not existing_record:
|
||||||
if not existing_record:
|
self.env['cwf.timesheet.line'].sudo().create({
|
||||||
self.env['cwf.timesheet.line'].sudo().create({
|
'week_id': self.id,
|
||||||
'week_id': self.id,
|
'employee_id': employee.id,
|
||||||
'employee_id': employee.id,
|
'week_day':current_date,
|
||||||
'week_day':current_date,
|
})
|
||||||
})
|
current_date += timedelta(days=1)
|
||||||
current_date += timedelta(days=1)
|
self.status = 'submitted'
|
||||||
self.status = 'submitted'
|
for employee in employees:
|
||||||
for employee in employees:
|
weekly_timesheet_exists = self.env['cwf.weekly.timesheet'].sudo().search([('week_id','=',self.id),('employee_id','=',employee.id)])
|
||||||
weekly_timesheet_exists = self.env['cwf.weekly.timesheet'].sudo().search([('week_id','=',self.id),('employee_id','=',employee.id)])
|
|
||||||
|
if not weekly_timesheet_exists:
|
||||||
if not weekly_timesheet_exists:
|
weekly_timesheet = self.env['cwf.weekly.timesheet'].sudo().create({
|
||||||
weekly_timesheet = self.env['cwf.weekly.timesheet'].sudo().create({
|
'week_id': self.id,
|
||||||
'week_id': self.id,
|
'employee_id': employee.id,
|
||||||
'employee_id': employee.id,
|
'status': 'draft'
|
||||||
'status': 'draft'
|
})
|
||||||
})
|
else:
|
||||||
else:
|
weekly_timesheet = weekly_timesheet_exists
|
||||||
weekly_timesheet = weekly_timesheet_exists
|
|
||||||
|
# Generate the URL for the newly created weekly_timesheet
|
||||||
# Generate the URL for the newly created weekly_timesheet
|
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||||
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"
|
||||||
record_url = f"{base_url}/web#id={weekly_timesheet.id}&view_type=form&model=cwf.weekly.timesheet"
|
|
||||||
|
weekly_timesheet.update_attendance()
|
||||||
weekly_timesheet.update_attendance()
|
if employee.work_email and weekly_timesheet:
|
||||||
if employee.work_email and weekly_timesheet:
|
email_values = {
|
||||||
email_values = {
|
'email_to': employee.work_email, # Email body from template
|
||||||
'email_to': employee.work_email, # Email body from template
|
'subject': 'Timesheet Update Notification',
|
||||||
'subject': 'Timesheet Update Notification',
|
}
|
||||||
}
|
body_html = template.body_html.replace(
|
||||||
body_html = template.body_html.replace(
|
'https://ftprotech.in/odoo/action-261',
|
||||||
'https://ftprotech.in/odoo/action-261',
|
record_url
|
||||||
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}
|
||||||
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)
|
||||||
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):
|
||||||
class CwfWeeklyTimesheet(models.Model):
|
_name = "cwf.weekly.timesheet"
|
||||||
_name = "cwf.weekly.timesheet"
|
_description = "CWF Weekly Timesheet"
|
||||||
_description = "CWF Weekly Timesheet"
|
_rec_name = 'employee_id'
|
||||||
_rec_name = 'employee_id'
|
|
||||||
|
|
||||||
|
def _default_week_id(self):
|
||||||
def _default_week_id(self):
|
current_date = fields.Date.today()
|
||||||
current_date = fields.Date.today()
|
timesheet = self.env['cwf.timesheet'].sudo().search([
|
||||||
timesheet = self.env['cwf.timesheet'].sudo().search([
|
('week_start_date', '<=', current_date),
|
||||||
('week_start_date', '<=', current_date),
|
('week_end_date', '>=', current_date)
|
||||||
('week_end_date', '>=', current_date)
|
], limit=1)
|
||||||
], limit=1)
|
return timesheet.id if timesheet else False
|
||||||
return timesheet.id if timesheet else False
|
|
||||||
|
def _get_week_id_domain(self):
|
||||||
def _get_week_id_domain(self):
|
for rec in self:
|
||||||
for rec in self:
|
return [('week_start_date.month_number','=',2)]
|
||||||
return [('week_start_date.month_number','=',2)]
|
pass
|
||||||
pass
|
|
||||||
|
month_id = fields.Selection(
|
||||||
month_id = fields.Selection(
|
[(str(i), month_name[i]) for i in range(1, 13)],
|
||||||
[(str(i), month_name[i]) for i in range(1, 13)],
|
string='Month'
|
||||||
string='Month'
|
)
|
||||||
)
|
week_id = fields.Many2one('cwf.timesheet', 'Week', default=lambda self: self._default_week_id())
|
||||||
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)
|
||||||
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')
|
||||||
cwf_timesheet_lines = fields.One2many('cwf.timesheet.line' ,'weekly_timesheet')
|
status = fields.Selection([('draft','Draft'),('submitted','Submitted')], default='draft')
|
||||||
status = fields.Selection([('draft','Draft'),('submitted','Submitted')], default='draft')
|
week_start_date = fields.Date(related='week_id.week_start_date')
|
||||||
week_start_date = fields.Date(related='week_id.week_start_date')
|
week_end_date = fields.Date(related='week_id.week_end_date')
|
||||||
week_end_date = fields.Date(related='week_id.week_end_date')
|
|
||||||
|
@api.onchange('month_id')
|
||||||
@api.onchange('month_id')
|
def _onchange_month(self):
|
||||||
def _onchange_month(self):
|
if self.month_id:
|
||||||
if self.month_id:
|
year = self.week_start_date.year if self.week_start_date else fields.Date.today().year
|
||||||
year = self.week_start_date.year if self.week_start_date else fields.Date.today().year
|
start = date(year, int(self.month_id), 1)
|
||||||
start = date(year, int(self.month_id), 1)
|
if int(self.month_id) == 12:
|
||||||
if int(self.month_id) == 12:
|
end = date(year + 1, 1, 1) - timedelta(days=1)
|
||||||
end = date(year + 1, 1, 1) - timedelta(days=1)
|
else:
|
||||||
else:
|
end = date(year, int(self.month_id) + 1, 1) - timedelta(days=1)
|
||||||
end = date(year, int(self.month_id) + 1, 1) - timedelta(days=1)
|
self = self.with_context(month_start=start, month_end=end)
|
||||||
self = self.with_context(month_start=start, month_end=end)
|
|
||||||
|
@api.onchange('week_id')
|
||||||
@api.onchange('week_id')
|
def _onchange_week_id(self):
|
||||||
def _onchange_week_id(self):
|
if self.week_id and self.week_id.week_start_date:
|
||||||
if self.week_id and self.week_id.week_start_date:
|
self.month_id = str(self.week_id.week_start_date.month)
|
||||||
self.month_id = str(self.week_id.week_start_date.month)
|
|
||||||
|
@api.constrains('week_id', 'employee_id')
|
||||||
@api.constrains('week_id', 'employee_id')
|
def _check_unique_week_employee(self):
|
||||||
def _check_unique_week_employee(self):
|
for record in self:
|
||||||
for record in self:
|
if record.week_id.week_start_date > fields.Date.today():
|
||||||
if record.week_id.week_start_date > fields.Date.today():
|
raise ValidationError(_("You Can't select future week period"))
|
||||||
raise ValidationError(_("You Can't select future week period"))
|
# Search for existing records with the same week_id and employee_id
|
||||||
# Search for existing records with the same week_id and employee_id
|
existing_record = self.env['cwf.weekly.timesheet'].search([
|
||||||
existing_record = self.env['cwf.weekly.timesheet'].search([
|
('week_id', '=', record.week_id.id),
|
||||||
('week_id', '=', record.week_id.id),
|
('employee_id', '=', record.employee_id.id)
|
||||||
('employee_id', '=', record.employee_id.id)
|
], limit=1)
|
||||||
], limit=1)
|
|
||||||
|
# If an existing record is found and it's not the current record (in case of update), raise an error
|
||||||
# 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:
|
||||||
if existing_record and existing_record.id != record.id:
|
raise ValidationError("A timesheet for this employee already exists for the selected week.")
|
||||||
raise ValidationError("A timesheet for this employee already exists for the selected week.")
|
|
||||||
|
def update_attendance(self):
|
||||||
def update_attendance(self):
|
for rec in self:
|
||||||
for rec in self:
|
# Get the week start and end date
|
||||||
# Get the week start and end date
|
week_start_date = rec.week_id.week_start_date
|
||||||
week_start_date = rec.week_id.week_start_date
|
week_end_date = rec.week_id.week_end_date
|
||||||
week_end_date = rec.week_id.week_end_date
|
|
||||||
|
# Convert start and end dates to datetime objects for proper filtering
|
||||||
# Convert start and end dates to datetime objects for proper filtering
|
week_start_datetime = datetime.combine(week_start_date, datetime.min.time())
|
||||||
week_start_datetime = datetime.combine(week_start_date, datetime.min.time())
|
week_end_datetime = datetime.combine(week_end_date, datetime.max.time())
|
||||||
week_end_datetime = datetime.combine(week_end_date, datetime.max.time())
|
|
||||||
|
# Delete timesheet lines that are outside the week range
|
||||||
# Delete timesheet lines that are outside the week range
|
rec.cwf_timesheet_lines.filtered(lambda line:
|
||||||
rec.cwf_timesheet_lines.filtered(lambda line:
|
line.week_day < week_start_date or line.week_day > week_end_date
|
||||||
line.week_day < week_start_date or line.week_day > week_end_date
|
).unlink()
|
||||||
).unlink()
|
|
||||||
|
# Search for attendance records that fall within the week period and match the employee
|
||||||
# Search for attendance records that fall within the week period and match the employee
|
hr_attendance_records = self.env['hr.attendance'].sudo().search([
|
||||||
hr_attendance_records = self.env['hr.attendance'].sudo().search([
|
('check_in', '>=', week_start_datetime),
|
||||||
('check_in', '>=', week_start_datetime),
|
('check_out', '<=', week_end_datetime),
|
||||||
('check_out', '<=', week_end_datetime),
|
('employee_id', '=', rec.employee_id.id)
|
||||||
('employee_id', '=', rec.employee_id.id)
|
])
|
||||||
])
|
|
||||||
|
# Group the attendance records by date
|
||||||
# Group the attendance records by date
|
attendance_by_date = {}
|
||||||
attendance_by_date = {}
|
for attendance in hr_attendance_records:
|
||||||
for attendance in hr_attendance_records:
|
attendance_date = attendance.check_in.date()
|
||||||
attendance_date = attendance.check_in.date()
|
if attendance_date not in attendance_by_date:
|
||||||
if attendance_date not in attendance_by_date:
|
attendance_by_date[attendance_date] = []
|
||||||
attendance_by_date[attendance_date] = []
|
attendance_by_date[attendance_date].append(attendance)
|
||||||
attendance_by_date[attendance_date].append(attendance)
|
|
||||||
|
# Get all the dates within the week period
|
||||||
# Get all the dates within the week period
|
all_week_dates = [week_start_date + timedelta(days=i) for i in
|
||||||
all_week_dates = [week_start_date + timedelta(days=i) for i in
|
range((week_end_date - week_start_date).days + 1)]
|
||||||
range((week_end_date - week_start_date).days + 1)]
|
|
||||||
|
# Create or update timesheet lines for each day in the week
|
||||||
# Create or update timesheet lines for each day in the week
|
for date in all_week_dates:
|
||||||
for date in all_week_dates:
|
# Check if there is attendance for this date
|
||||||
# Check if there is attendance for this date
|
if date in attendance_by_date:
|
||||||
if date in attendance_by_date:
|
# If there are multiple attendance records, take the earliest check_in and latest check_out
|
||||||
# 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])
|
||||||
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])
|
||||||
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:
|
||||||
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))
|
||||||
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:
|
||||||
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))
|
||||||
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
|
||||||
# Check if a timesheet line for this employee, week, and date already exists
|
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
|
||||||
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
|
('week_day', '=', date),
|
||||||
('week_day', '=', date),
|
('employee_id', '=', rec.employee_id.id),
|
||||||
('employee_id', '=', rec.employee_id.id),
|
('week_id', '=', rec.week_id.id),
|
||||||
('week_id', '=', rec.week_id.id),
|
('weekly_timesheet', '=', rec.id)
|
||||||
('weekly_timesheet', '=', rec.id)
|
], limit=1)
|
||||||
], limit=1)
|
|
||||||
|
if existing_timesheet_line:
|
||||||
if existing_timesheet_line:
|
# If it exists, update the existing record
|
||||||
# If it exists, update the existing record
|
existing_timesheet_line.write({
|
||||||
existing_timesheet_line.write({
|
'check_in_date': earliest_check_in,
|
||||||
'check_in_date': earliest_check_in,
|
'check_out_date': latest_check_out,
|
||||||
'check_out_date': latest_check_out,
|
'state_type': 'present',
|
||||||
'state_type': 'present',
|
})
|
||||||
})
|
else:
|
||||||
else:
|
# If it doesn't exist, create a new timesheet line with present state_type
|
||||||
# If it doesn't exist, create a new timesheet line with present state_type
|
self.env['cwf.timesheet.line'].create({
|
||||||
self.env['cwf.timesheet.line'].create({
|
'weekly_timesheet': rec.id,
|
||||||
'weekly_timesheet': rec.id,
|
'employee_id': rec.employee_id.id,
|
||||||
'employee_id': rec.employee_id.id,
|
'week_id': rec.week_id.id,
|
||||||
'week_id': rec.week_id.id,
|
'week_day': date,
|
||||||
'week_day': date,
|
'check_in_date': earliest_check_in,
|
||||||
'check_in_date': earliest_check_in,
|
'check_out_date': latest_check_out,
|
||||||
'check_out_date': latest_check_out,
|
'state_type': 'present',
|
||||||
'state_type': 'present',
|
})
|
||||||
})
|
else:
|
||||||
else:
|
# If no attendance exists for this date, create a new timesheet line with time_off state_type
|
||||||
# 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([
|
||||||
existing_timesheet_line = self.env['cwf.timesheet.line'].sudo().search([
|
('week_day', '=', date),
|
||||||
('week_day', '=', date),
|
('employee_id', '=', rec.employee_id.id),
|
||||||
('employee_id', '=', rec.employee_id.id),
|
('week_id', '=', rec.week_id.id),
|
||||||
('week_id', '=', rec.week_id.id),
|
('weekly_timesheet', '=', rec.id)
|
||||||
('weekly_timesheet', '=', rec.id)
|
], limit=1)
|
||||||
], limit=1)
|
|
||||||
|
if not existing_timesheet_line:
|
||||||
if not existing_timesheet_line:
|
if date.weekday() != 5 and date.weekday() != 6:
|
||||||
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
|
||||||
# If no record exists for this date, create a new timesheet line with time_off state_type
|
self.env['cwf.timesheet.line'].create({
|
||||||
self.env['cwf.timesheet.line'].create({
|
'weekly_timesheet': rec.id,
|
||||||
'weekly_timesheet': rec.id,
|
'employee_id': rec.employee_id.id,
|
||||||
'employee_id': rec.employee_id.id,
|
'week_id': rec.week_id.id,
|
||||||
'week_id': rec.week_id.id,
|
'week_day': date,
|
||||||
'week_day': date,
|
'state_type': 'time_off',
|
||||||
'state_type': 'time_off',
|
})
|
||||||
})
|
|
||||||
|
def action_submit(self):
|
||||||
def action_submit(self):
|
for rec in self:
|
||||||
for rec in self:
|
for timesheet in rec.cwf_timesheet_lines:
|
||||||
for timesheet in rec.cwf_timesheet_lines:
|
timesheet.action_submit()
|
||||||
timesheet.action_submit()
|
rec.status = 'submitted'
|
||||||
rec.status = 'submitted'
|
|
||||||
|
class CwfTimesheetLine(models.Model):
|
||||||
class CwfTimesheetLine(models.Model):
|
_name = 'cwf.timesheet.line'
|
||||||
_name = 'cwf.timesheet.line'
|
_description = 'CWF Weekly Timesheet Lines'
|
||||||
_description = 'CWF Weekly Timesheet Lines'
|
_rec_name = 'employee_id'
|
||||||
_rec_name = 'employee_id'
|
|
||||||
|
weekly_timesheet = fields.Many2one('cwf.weekly.timesheet')
|
||||||
weekly_timesheet = fields.Many2one('cwf.weekly.timesheet')
|
employee_id = fields.Many2one('hr.employee', string='Employee', related='weekly_timesheet.employee_id')
|
||||||
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_id = fields.Many2one('cwf.timesheet', 'Week', related='weekly_timesheet.week_id')
|
week_day = fields.Date(string='Date')
|
||||||
week_day = fields.Date(string='Date')
|
check_in_date = fields.Datetime(string='Checkin')
|
||||||
check_in_date = fields.Datetime(string='Checkin')
|
check_out_date = fields.Datetime(string='Checkout ')
|
||||||
check_out_date = fields.Datetime(string='Checkout ')
|
is_updated = fields.Boolean('Attendance Updated')
|
||||||
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)
|
||||||
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')
|
||||||
@api.constrains('week_day', 'check_in_date', 'check_out_date')
|
def _check_week_day_and_times(self):
|
||||||
def _check_week_day_and_times(self):
|
for record in self:
|
||||||
for record in self:
|
# Ensure week_day is within the week range
|
||||||
# Ensure week_day is within the week range
|
if record.week_id:
|
||||||
if record.week_id:
|
if record.week_day < record.week_id.week_start_date or record.week_day > record.week_id.week_end_date:
|
||||||
if record.week_day < record.week_id.week_start_date or record.week_day > record.week_id.week_end_date:
|
raise ValidationError(
|
||||||
raise ValidationError(
|
"The selected 'week_day' must be within the range of the week from %s to %s." %
|
||||||
"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)
|
||||||
(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
|
||||||
# Ensure check_in_date and check_out_date are on the selected week_day
|
if record.check_in_date:
|
||||||
if record.check_in_date:
|
if record.check_in_date.date() != record.week_day:
|
||||||
if record.check_in_date.date() != record.week_day:
|
raise ValidationError(
|
||||||
raise ValidationError(
|
"The 'check_in_date' must be on the selected Date."
|
||||||
"The 'check_in_date' must be on the selected Date."
|
)
|
||||||
)
|
|
||||||
|
if record.check_out_date:
|
||||||
if record.check_out_date:
|
if record.check_out_date.date() != record.week_day:
|
||||||
if record.check_out_date.date() != record.week_day:
|
raise ValidationError(
|
||||||
raise ValidationError(
|
"The 'check_out_date' must be on the selected Date."
|
||||||
"The 'check_out_date' must be on the selected Date."
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
def action_submit(self):
|
||||||
def action_submit(self):
|
if self.state_type == 'draft' or not self.state_type:
|
||||||
if self.state_type == 'draft' or not self.state_type:
|
raise ValidationError(_('State type should not Draft or Empty'))
|
||||||
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):
|
||||||
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'))
|
||||||
raise ValidationError(_('Please enter Check details'))
|
self._update_attendance()
|
||||||
self._update_attendance()
|
|
||||||
|
|
||||||
|
def _update_attendance(self):
|
||||||
def _update_attendance(self):
|
attendance_obj = self.env['hr.attendance']
|
||||||
attendance_obj = self.env['hr.attendance']
|
for record in self:
|
||||||
for record in self:
|
if record.check_in_date != False and record.check_out_date != False and record.employee_id:
|
||||||
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()),
|
||||||
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)],
|
||||||
('check_out', '<=', record.check_out_date.date()),('employee_id','=',record.employee_id.id)],
|
limit=1, order="check_in")
|
||||||
limit=1, order="check_in")
|
last_check_out = attendance_obj.sudo().search([('check_in', '>=', record.check_in_date.date()),
|
||||||
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)],
|
||||||
('check_out', '<=', record.check_out_date.date()),('employee_id','=',record.employee_id.id)],
|
limit=1, order="check_out desc")
|
||||||
limit=1, order="check_out desc")
|
|
||||||
|
if first_check_in or last_check_out:
|
||||||
if first_check_in or last_check_out:
|
if first_check_in.sudo().check_in != record.check_in_date:
|
||||||
if first_check_in.sudo().check_in != record.check_in_date:
|
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:
|
||||||
if last_check_out.sudo().check_out != record.check_out_date:
|
last_check_out.sudo().check_out = record.check_out_date
|
||||||
last_check_out.sudo().check_out = record.check_out_date
|
else:
|
||||||
else:
|
attendance_obj.sudo().create({
|
||||||
attendance_obj.sudo().create({
|
'employee_id': record.employee_id.id,
|
||||||
'employee_id': record.employee_id.id,
|
'check_in': record.check_in_date - timedelta(hours=5, minutes=30),
|
||||||
'check_in': record.check_in_date - timedelta(hours=5, minutes=30),
|
'check_out': record.check_out_date - timedelta(hours=5, minutes=30),
|
||||||
'check_out': record.check_out_date - timedelta(hours=5, minutes=30),
|
})
|
||||||
})
|
record.is_updated = True
|
||||||
record.is_updated = True
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
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_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_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,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_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_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_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_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
|
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" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<data noupdate="0">
|
<data noupdate="0">
|
||||||
<record id="cwf_weekly_timesheet_user_rule" model="ir.rule">
|
<record id="cwf_weekly_timesheet_user_rule" model="ir.rule">
|
||||||
<field name="name">CWF Weekly Timesheet User Rule</field>
|
<field name="name">CWF Weekly Timesheet User Rule</field>
|
||||||
<field ref="cwf_timesheet.model_cwf_weekly_timesheet" name="model_id"/>
|
<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="domain_force">[('employee_id.user_id.id','=',user.id)]</field>
|
||||||
<field name="groups" eval="[(4, ref('hr_employee_extended.group_external_user'))]"/>
|
<field name="groups" eval="[(4, ref('hr_employee_extended.group_external_user'))]"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="cwf_timesheet_line_user_rule" model="ir.rule">
|
<record id="cwf_timesheet_line_user_rule" model="ir.rule">
|
||||||
<field name="name">CWF Timesheet Line User Rule</field>
|
<field name="name">CWF Timesheet Line User Rule</field>
|
||||||
<field ref="cwf_timesheet.model_cwf_timesheet_line" name="model_id"/>
|
<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="domain_force">[('employee_id.user_id.id','=',user.id)]</field>
|
||||||
<field name="groups" eval="[(4, ref('hr_employee_extended.group_external_user'))]"/>
|
<field name="groups" eval="[(4, ref('hr_employee_extended.group_external_user'))]"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="cwf_weekly_timesheet_manager_rule" model="ir.rule">
|
<record id="cwf_weekly_timesheet_manager_rule" model="ir.rule">
|
||||||
<field name="name">CWF Weekly Timesheet manager Rule</field>
|
<field name="name">CWF Weekly Timesheet manager Rule</field>
|
||||||
<field ref="cwf_timesheet.model_cwf_weekly_timesheet" name="model_id"/>
|
<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="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'))]"/>
|
<field name="groups" eval="[(4, ref('hr_attendance.group_hr_attendance_manager'))]"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="cwf_timesheet_line_manager_rule" model="ir.rule">
|
<record id="cwf_timesheet_line_manager_rule" model="ir.rule">
|
||||||
<field name="name">CWF Timesheet Line manager Rule</field>
|
<field name="name">CWF Timesheet Line manager Rule</field>
|
||||||
<field ref="cwf_timesheet.model_cwf_timesheet_line" name="model_id"/>
|
<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="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'))]"/>
|
<field name="groups" eval="[(4, ref('hr_attendance.group_hr_attendance_manager'))]"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
@ -1,54 +1,54 @@
|
||||||
/** @odoo-module **/
|
/** @odoo-module **/
|
||||||
|
|
||||||
import { patch } from "@web/core/utils/patch";
|
import { patch } from "@web/core/utils/patch";
|
||||||
import { NetflixProfileContainer } from "@hr_emp_dashboard/js/profile_component";
|
import { NetflixProfileContainer } from "@hr_emp_dashboard/js/profile_component";
|
||||||
import { user } from "@web/core/user";
|
import { user } from "@web/core/user";
|
||||||
|
|
||||||
// Apply patch to NetflixProfileContainer prototype
|
// Apply patch to NetflixProfileContainer prototype
|
||||||
patch(NetflixProfileContainer.prototype, {
|
patch(NetflixProfileContainer.prototype, {
|
||||||
/**
|
/**
|
||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
setup() {
|
setup() {
|
||||||
// Call parent setup method
|
// Call parent setup method
|
||||||
|
|
||||||
super.setup(...arguments);
|
super.setup(...arguments);
|
||||||
|
|
||||||
// Log the department of the logged-in employee (check if data is available)
|
// Log the department of the logged-in employee (check if data is available)
|
||||||
// if (this.state && this.state.login_employee) {
|
// if (this.state && this.state.login_employee) {
|
||||||
// console.log(this.state.login_employee['department_id']);
|
// console.log(this.state.login_employee['department_id']);
|
||||||
// } else {
|
// } else {
|
||||||
// console.error('Employee or department data is unavailable.');
|
// console.error('Employee or department data is unavailable.');
|
||||||
// }
|
// }
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the hr_timesheets method
|
* Override the hr_timesheets method
|
||||||
*/
|
*/
|
||||||
async hr_timesheets() {
|
async hr_timesheets() {
|
||||||
const isExternalUser = await user.hasGroup("hr_employee_extended.group_external_user");
|
const isExternalUser = await user.hasGroup("hr_employee_extended.group_external_user");
|
||||||
// Check the department of the logged-in employee
|
// Check the department of the logged-in employee
|
||||||
console.log(isExternalUser);
|
console.log(isExternalUser);
|
||||||
console.log("is external user");
|
console.log("is external user");
|
||||||
debugger;
|
debugger;
|
||||||
if (isExternalUser && this.state.login_employee.department_id) {
|
if (isExternalUser && this.state.login_employee.department_id) {
|
||||||
console.log("hello external");
|
console.log("hello external");
|
||||||
// If the department is 'CWF', perform the action to open the timesheets
|
// If the department is 'CWF', perform the action to open the timesheets
|
||||||
this.action.doAction({
|
this.action.doAction({
|
||||||
name: "Timesheets",
|
name: "Timesheets",
|
||||||
type: 'ir.actions.act_window',
|
type: 'ir.actions.act_window',
|
||||||
res_model: 'cwf.timesheet.line', // Ensure this model exists
|
res_model: 'cwf.timesheet.line', // Ensure this model exists
|
||||||
view_mode: 'list,form',
|
view_mode: 'list,form',
|
||||||
views: [[false, 'list'], [false, 'form']],
|
views: [[false, 'list'], [false, 'form']],
|
||||||
context: {
|
context: {
|
||||||
'search_default_week_id': true,
|
'search_default_week_id': true,
|
||||||
},
|
},
|
||||||
domain: [['employee_id.user_id','=', this.props.action.context.user_id]],
|
domain: [['employee_id.user_id','=', this.props.action.context.user_id]],
|
||||||
target: 'current',
|
target: 'current',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If not 'CWF', call the base functionality
|
// If not 'CWF', call the base functionality
|
||||||
return super.hr_timesheets();
|
return super.hr_timesheets();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<templates id="template" xml:space="preserve">
|
<templates id="template" xml:space="preserve">
|
||||||
<t t-name="timesheet_form">
|
<t t-name="timesheet_form">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>Weekly Timesheet</h2>
|
<h2>Weekly Timesheet</h2>
|
||||||
<form class="timesheet-form">
|
<form class="timesheet-form">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="employee">Employee</label>
|
<label for="employee">Employee</label>
|
||||||
<input t-att-value="state.employee" type="text" id="employee" class="form-control"/>
|
<input t-att-value="state.employee" type="text" id="employee" class="form-control"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="weekStartDate">Week Start Date</label>
|
<label for="weekStartDate">Week Start Date</label>
|
||||||
<input type="datetime-local" t-model="state.weekStartDate" id="weekStartDate" class="form-control"/>
|
<input type="datetime-local" t-model="state.weekStartDate" id="weekStartDate" class="form-control"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="weekEndDate">Week End Date</label>
|
<label for="weekEndDate">Week End Date</label>
|
||||||
<input type="datetime-local" t-model="state.weekEndDate" id="weekEndDate" class="form-control"/>
|
<input type="datetime-local" t-model="state.weekEndDate" id="weekEndDate" class="form-control"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="totalHours">Total Hours Worked</label>
|
<label for="totalHours">Total Hours Worked</label>
|
||||||
<input type="number" t-model="state.totalHours" id="totalHours" class="form-control" min="0"/>
|
<input type="number" t-model="state.totalHours" id="totalHours" class="form-control" min="0"/>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" t-on-click="submitForm" class="btn btn-primary">Submit Timesheet</button>
|
<button type="button" t-on-click="submitForm" class="btn btn-primary">Submit Timesheet</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</templates>
|
</templates>
|
||||||
|
|
|
||||||
|
|
@ -1,104 +1,104 @@
|
||||||
<odoo>
|
<odoo>
|
||||||
<record id="view_cwf_timesheet_calendar_form" model="ir.ui.view">
|
<record id="view_cwf_timesheet_calendar_form" model="ir.ui.view">
|
||||||
<field name="name">cwf.timesheet.calendar.form</field>
|
<field name="name">cwf.timesheet.calendar.form</field>
|
||||||
<field name="model">cwf.timesheet.calendar</field>
|
<field name="model">cwf.timesheet.calendar</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="CWF Timesheet Calendar">
|
<form string="CWF Timesheet Calendar">
|
||||||
<sheet>
|
<sheet>
|
||||||
<group>
|
<group>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<button name="action_generate_weeks" string="Generate Week Periods" type="object"
|
<button name="action_generate_weeks" string="Generate Week Periods" type="object"
|
||||||
class="oe_highlight"/>
|
class="oe_highlight"/>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
<page string="Weeks">
|
<page string="Weeks">
|
||||||
<field name="week_period" context="{'order': 'week_start_date asc'}">
|
<field name="week_period" context="{'order': 'week_start_date asc'}">
|
||||||
<list editable="bottom" decoration-success="status == 'submitted'">
|
<list editable="bottom" decoration-success="status == 'submitted'">
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="week_start_date"/>
|
<field name="week_start_date"/>
|
||||||
<field name="week_end_date"/>
|
<field name="week_end_date"/>
|
||||||
<field name="start_month" column_invisible="1"/>
|
<field name="start_month" column_invisible="1"/>
|
||||||
<field name="end_month" column_invisible="1"/>
|
<field name="end_month" column_invisible="1"/>
|
||||||
<field name="status"/>
|
<field name="status"/>
|
||||||
<button name="send_timesheet_update_email" string="Send Update Email"
|
<button name="send_timesheet_update_email" string="Send Update Email"
|
||||||
invisible="status == 'submitted'" type="object"
|
invisible="status == 'submitted'" type="object"
|
||||||
confirm="You can't revert this action. Please check twice before Submitting?"
|
confirm="You can't revert this action. Please check twice before Submitting?"
|
||||||
class="oe_highlight"/>
|
class="oe_highlight"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="view_cwf_timesheet_calendar_list" model="ir.ui.view">
|
<record id="view_cwf_timesheet_calendar_list" model="ir.ui.view">
|
||||||
<field name="name">cwf.timesheet.calendar.list</field>
|
<field name="name">cwf.timesheet.calendar.list</field>
|
||||||
<field name="model">cwf.timesheet.calendar</field>
|
<field name="model">cwf.timesheet.calendar</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<list>
|
<list>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="action_cwf_timesheet_calendar" model="ir.actions.act_window">
|
<record id="action_cwf_timesheet_calendar" model="ir.actions.act_window">
|
||||||
<field name="name">CWF Timesheet Calendar</field>
|
<field name="name">CWF Timesheet Calendar</field>
|
||||||
<field name="res_model">cwf.timesheet.calendar</field>
|
<field name="res_model">cwf.timesheet.calendar</field>
|
||||||
<field name="view_mode">list,form</field>
|
<field name="view_mode">list,form</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<menuitem id="menu_cwf_attendance_attendance" name="CWF" parent="hr_attendance.menu_hr_attendance_root"
|
<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"/>
|
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"
|
<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"/>
|
action="cwf_timesheet.action_cwf_timesheet_calendar" groups="hr_attendance.group_hr_attendance_manager"/>
|
||||||
|
|
||||||
|
|
||||||
<record id="view_timesheet_form" model="ir.ui.view">
|
<record id="view_timesheet_form" model="ir.ui.view">
|
||||||
<field name="name">cwf.timesheet.form</field>
|
<field name="name">cwf.timesheet.form</field>
|
||||||
<field name="model">cwf.timesheet</field>
|
<field name="model">cwf.timesheet</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="Timesheet">
|
<form string="Timesheet">
|
||||||
<header>
|
<header>
|
||||||
<button name="send_timesheet_update_email" string="Send Email" type="object"
|
<button name="send_timesheet_update_email" string="Send Email" type="object"
|
||||||
invisible="status != 'draft'"/>
|
invisible="status != 'draft'"/>
|
||||||
</header>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
<div class="oe_title">
|
<div class="oe_title">
|
||||||
<label for="name"/>
|
<label for="name"/>
|
||||||
<h1>
|
<h1>
|
||||||
<field name="name" class="oe_inline" readonly="status != 'draft'"/>
|
<field name="name" class="oe_inline" readonly="status != 'draft'"/>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<group>
|
<group>
|
||||||
<!-- Section for Employee and Date Range -->
|
<!-- Section for Employee and Date Range -->
|
||||||
<group>
|
<group>
|
||||||
<field name="week_start_date" readonly="status != 'draft'"/>
|
<field name="week_start_date" readonly="status != 'draft'"/>
|
||||||
<field name="week_end_date" readonly="status != 'draft'"/>
|
<field name="week_end_date" readonly="status != 'draft'"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="view_timesheet_list" model="ir.ui.view">
|
<record id="view_timesheet_list" model="ir.ui.view">
|
||||||
<field name="name">cwf.timesheet.list</field>
|
<field name="name">cwf.timesheet.list</field>
|
||||||
<field name="model">cwf.timesheet</field>
|
<field name="model">cwf.timesheet</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<list>
|
<list>
|
||||||
<field name="name"/>
|
<field name="name"/>
|
||||||
<field name="week_start_date"/>
|
<field name="week_start_date"/>
|
||||||
<field name="week_end_date"/>
|
<field name="week_end_date"/>
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
<record id="action_cwf_timesheet" model="ir.actions.act_window">
|
<record id="action_cwf_timesheet" model="ir.actions.act_window">
|
||||||
<field name="name">CWF Timesheet</field>
|
<field name="name">CWF Timesheet</field>
|
||||||
<field name="res_model">cwf.timesheet</field>
|
<field name="res_model">cwf.timesheet</field>
|
||||||
<field name="view_mode">list,form</field>
|
<field name="view_mode">list,form</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
|
|
@ -1,158 +1,158 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<odoo>
|
<odoo>
|
||||||
<record id="view_cwf_weekly_timesheet_form" model="ir.ui.view">
|
<record id="view_cwf_weekly_timesheet_form" model="ir.ui.view">
|
||||||
<field name="name">cwf.weekly.timesheet.form</field>
|
<field name="name">cwf.weekly.timesheet.form</field>
|
||||||
<field name="model">cwf.weekly.timesheet</field>
|
<field name="model">cwf.weekly.timesheet</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form string="CWF Weekly Timesheet">
|
<form string="CWF Weekly Timesheet">
|
||||||
<header>
|
<header>
|
||||||
<button name="update_attendance" string="Update"
|
<button name="update_attendance" string="Update"
|
||||||
type="object" class="oe_highlight" invisible="status != 'draft'"/>
|
type="object" class="oe_highlight" invisible="status != 'draft'"/>
|
||||||
<button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"
|
<button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"
|
||||||
class="oe_highlight" invisible="status != 'draft'"/>
|
class="oe_highlight" invisible="status != 'draft'"/>
|
||||||
<field name="status" readonly="1" widget="statusbar"/>
|
<field name="status" readonly="1" widget="statusbar"/>
|
||||||
</header>
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
<group>
|
<group>
|
||||||
<field name="month_id"/>
|
<field name="month_id"/>
|
||||||
<field name="week_id" readonly="0" domain="['|',('start_month','=',month_id),('end_month','=',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="0" groups="hr_attendance.group_hr_attendance_manager"/>
|
||||||
<field name="employee_id" readonly="1" groups="hr_employee_extended.group_external_user"/>
|
<field name="employee_id" readonly="1" groups="hr_employee_extended.group_external_user"/>
|
||||||
<label for="week_start_date" string="Dates"/>
|
<label for="week_start_date" string="Dates"/>
|
||||||
<div class="o_row">
|
<div class="o_row">
|
||||||
<field name="week_start_date" widget="daterange" options="{'end_date_field.month()': 'week_end_date'}"/>
|
<field name="week_start_date" widget="daterange" options="{'end_date_field.month()': 'week_end_date'}"/>
|
||||||
<field name="week_end_date" invisible="1"/>
|
<field name="week_end_date" invisible="1"/>
|
||||||
</div>
|
</div>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
<page string="Timesheet Lines">
|
<page string="Timesheet Lines">
|
||||||
<field name="cwf_timesheet_lines">
|
<field name="cwf_timesheet_lines">
|
||||||
<list editable="bottom">
|
<list editable="bottom">
|
||||||
<field name="employee_id"/>
|
<field name="employee_id"/>
|
||||||
<field name="week_day"/>
|
<field name="week_day"/>
|
||||||
<field name="check_in_date"/>
|
<field name="check_in_date"/>
|
||||||
<field name="check_out_date"/>
|
<field name="check_out_date"/>
|
||||||
<field name="state_type"/>
|
<field name="state_type"/>
|
||||||
<!-- <button name="action_submit" string="Submit" type="object"-->
|
<!-- <button name="action_submit" string="Submit" type="object"-->
|
||||||
<!-- confirm="Are you sure you want to submit?" class="oe_highlight"/>-->
|
<!-- confirm="Are you sure you want to submit?" class="oe_highlight"/>-->
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
</notebook>
|
</notebook>
|
||||||
</sheet>
|
</sheet>
|
||||||
</form>
|
</form>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="view_cwf_weekly_timesheet_list" model="ir.ui.view">
|
<record id="view_cwf_weekly_timesheet_list" model="ir.ui.view">
|
||||||
<field name="name">cwf.weekly.timesheet.list</field>
|
<field name="name">cwf.weekly.timesheet.list</field>
|
||||||
<field name="model">cwf.weekly.timesheet</field>
|
<field name="model">cwf.weekly.timesheet</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<list>
|
<list>
|
||||||
<field name="week_id"/>
|
<field name="week_id"/>
|
||||||
<field name="employee_id"/>
|
<field name="employee_id"/>
|
||||||
<field name="status"/>
|
<field name="status"/>
|
||||||
<!-- <button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"-->
|
<!-- <button name="action_submit" string="Submit" type="object" confirm="Are you sure you want to submit?"-->
|
||||||
<!-- class="oe_highlight"/>-->
|
<!-- class="oe_highlight"/>-->
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="view_cwf_weekly_timesheet_search" model="ir.ui.view">
|
<record id="view_cwf_weekly_timesheet_search" model="ir.ui.view">
|
||||||
<field name="name">cwf.weekly.timesheet.search</field>
|
<field name="name">cwf.weekly.timesheet.search</field>
|
||||||
<field name="model">cwf.weekly.timesheet</field>
|
<field name="model">cwf.weekly.timesheet</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<search>
|
<search>
|
||||||
<!-- Search by Week ID -->
|
<!-- Search by Week ID -->
|
||||||
<field name="week_id"/>
|
<field name="week_id"/>
|
||||||
|
|
||||||
<!-- Search by Employee -->
|
<!-- Search by Employee -->
|
||||||
<field name="employee_id"/>
|
<field name="employee_id"/>
|
||||||
|
|
||||||
<!-- Search by Status -->
|
<!-- Search by Status -->
|
||||||
<field name="status"/>
|
<field name="status"/>
|
||||||
|
|
||||||
<!-- Optional: Add custom filters if needed -->
|
<!-- Optional: Add custom filters if needed -->
|
||||||
<filter string="Draft" name="filter_draft" domain="[('status','=','draft')]"/>
|
<filter string="Draft" name="filter_draft" domain="[('status','=','draft')]"/>
|
||||||
<filter string="Submitted" name="filter_submitted" domain="[('status','=','submit')]"/>
|
<filter string="Submitted" name="filter_submitted" domain="[('status','=','submit')]"/>
|
||||||
<group expand="0" string="Group By">
|
<group expand="0" string="Group By">
|
||||||
<filter string="Week" name="by_week_id" domain="[]" context="{'group_by':'week_id'}"/>
|
<filter string="Week" name="by_week_id" domain="[]" context="{'group_by':'week_id'}"/>
|
||||||
<separator/>
|
<separator/>
|
||||||
<filter string="Employee" name="by_employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
|
<filter string="Employee" name="by_employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
|
||||||
<filter string="Status" name="state_type" domain="[]" context="{'group_by': 'status'}"/>
|
<filter string="Status" name="state_type" domain="[]" context="{'group_by': 'status'}"/>
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
</search>
|
</search>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
<record id="action_cwf_weekly_timesheet" model="ir.actions.act_window">
|
<record id="action_cwf_weekly_timesheet" model="ir.actions.act_window">
|
||||||
<field name="name">CWF Weekly Timesheet</field>
|
<field name="name">CWF Weekly Timesheet</field>
|
||||||
<field name="res_model">cwf.weekly.timesheet</field>
|
<field name="res_model">cwf.weekly.timesheet</field>
|
||||||
<field name="view_mode">list,form</field>
|
<field name="view_mode">list,form</field>
|
||||||
<field name="context">
|
<field name="context">
|
||||||
{
|
{
|
||||||
"search_default_by_week_id": 1,
|
"search_default_by_week_id": 1,
|
||||||
"search_default_by_employee_id": 2
|
"search_default_by_employee_id": 2
|
||||||
}
|
}
|
||||||
</field>
|
</field>
|
||||||
|
|
||||||
<field name="search_view_id" ref="cwf_timesheet.view_cwf_weekly_timesheet_search"/>
|
<field name="search_view_id" ref="cwf_timesheet.view_cwf_weekly_timesheet_search"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
<record id="view_cwf_timesheet_line_list" model="ir.ui.view">
|
<record id="view_cwf_timesheet_line_list" model="ir.ui.view">
|
||||||
<field name="name">cwf.timesheet.line.list</field>
|
<field name="name">cwf.timesheet.line.list</field>
|
||||||
<field name="model">cwf.timesheet.line</field>
|
<field name="model">cwf.timesheet.line</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<list editable="bottom" create="0" delete="0" decoration-success="is_updated == True">
|
<list editable="bottom" create="0" delete="0" decoration-success="is_updated == True">
|
||||||
<field name="employee_id" readonly="1" force_save="1"/>
|
<field name="employee_id" readonly="1" force_save="1"/>
|
||||||
<field name="week_day" 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_in_date" readonly="is_updated == True"/>
|
||||||
<field name="check_out_date" readonly="is_updated == True"/>
|
<field name="check_out_date" readonly="is_updated == True"/>
|
||||||
<field name="state_type" 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"-->
|
<!-- <button name="action_submit" type="object" string="Submit" class="btn btn-outline-primary"-->
|
||||||
<!-- invisible="is_updated == True"/>-->
|
<!-- invisible="is_updated == True"/>-->
|
||||||
</list>
|
</list>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="view_cwf_timesheet_line_search" model="ir.ui.view">
|
<record id="view_cwf_timesheet_line_search" model="ir.ui.view">
|
||||||
<field name="name">cwf.timesheet.line.search</field>
|
<field name="name">cwf.timesheet.line.search</field>
|
||||||
<field name="model">cwf.timesheet.line</field>
|
<field name="model">cwf.timesheet.line</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<search string="Timesheets">
|
<search string="Timesheets">
|
||||||
<field name="employee_id"/>
|
<field name="employee_id"/>
|
||||||
<field name="week_id"/>
|
<field name="week_id"/>
|
||||||
<field name="week_day"/>
|
<field name="week_day"/>
|
||||||
<field name="check_in_date"/>
|
<field name="check_in_date"/>
|
||||||
<field name="check_out_date"/>
|
<field name="check_out_date"/>
|
||||||
<field name="state_type"/>
|
<field name="state_type"/>
|
||||||
<group expand="0" string="Group By">
|
<group expand="0" string="Group By">
|
||||||
<filter string="Employee" name="by_employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
|
<filter string="Employee" name="by_employee_id" domain="[]" context="{'group_by':'employee_id'}"/>
|
||||||
<separator/>
|
<separator/>
|
||||||
<filter string="Week" name="by_week_id" domain="[]" context="{'group_by':'week_id'}"/>
|
<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'}"/>
|
<filter string="Status" name="state_type" domain="[]" context="{'group_by': 'state_type'}"/>
|
||||||
</group>
|
</group>
|
||||||
</search>
|
</search>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
<record id="action_cwf_timesheet_line" model="ir.actions.act_window">
|
<record id="action_cwf_timesheet_line" model="ir.actions.act_window">
|
||||||
<field name="name">CWF Timesheet Lines</field>
|
<field name="name">CWF Timesheet Lines</field>
|
||||||
<field name="res_model">cwf.timesheet.line</field>
|
<field name="res_model">cwf.timesheet.line</field>
|
||||||
<field name="view_mode">list</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="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"/>
|
<field name="search_view_id" ref="cwf_timesheet.view_cwf_timesheet_line_search"/>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<menuitem id="menu_timesheet_form" name="CWF Weekly Timesheet "
|
<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"/>
|
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"
|
<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"/>
|
parent="menu_cwf_attendance_attendance" action="action_cwf_timesheet_line" groups="hr_employee_extended.group_external_user,hr_attendance.group_hr_attendance_manager"/>
|
||||||
|
|
||||||
</odoo>
|
</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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
from . import controllers
|
from . import controllers
|
||||||
from . import wizard
|
from . import wizard
|
||||||
|
|
|
||||||
|
|
@ -1,94 +1,94 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
{
|
{
|
||||||
'name': "Documents",
|
'name': "Documents",
|
||||||
|
|
||||||
'summary': "Collect, organize and share documents.",
|
'summary': "Collect, organize and share documents.",
|
||||||
|
|
||||||
'description': """
|
'description': """
|
||||||
App to upload and manage your documents.
|
App to upload and manage your documents.
|
||||||
""",
|
""",
|
||||||
|
|
||||||
'category': 'Productivity/Documents',
|
'category': 'Productivity/Documents',
|
||||||
'sequence': 80,
|
'sequence': 80,
|
||||||
'version': '1.4',
|
'version': '1.4',
|
||||||
'application': True,
|
'application': True,
|
||||||
'website': 'https://www.ftprotech.in/',
|
'website': 'https://www.ftprotech.in/',
|
||||||
|
|
||||||
# any module necessary for this one to work correctly
|
# any module necessary for this one to work correctly
|
||||||
'depends': ['base', 'mail', 'portal', 'attachment_indexation', 'digest'],
|
'depends': ['base', 'mail', 'portal', 'attachment_indexation', 'digest'],
|
||||||
|
|
||||||
# always loaded
|
# always loaded
|
||||||
'data': [
|
'data': [
|
||||||
'security/security.xml',
|
'security/security.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
'data/digest_data.xml',
|
'data/digest_data.xml',
|
||||||
'data/mail_template_data.xml',
|
'data/mail_template_data.xml',
|
||||||
'data/mail_activity_type_data.xml',
|
'data/mail_activity_type_data.xml',
|
||||||
'data/documents_tag_data.xml',
|
'data/documents_tag_data.xml',
|
||||||
'data/documents_document_data.xml',
|
'data/documents_document_data.xml',
|
||||||
'data/ir_config_parameter_data.xml',
|
'data/ir_config_parameter_data.xml',
|
||||||
'data/documents_tour.xml',
|
'data/documents_tour.xml',
|
||||||
'views/res_config_settings_views.xml',
|
'views/res_config_settings_views.xml',
|
||||||
'views/res_partner_views.xml',
|
'views/res_partner_views.xml',
|
||||||
'views/documents_access_views.xml',
|
'views/documents_access_views.xml',
|
||||||
'views/documents_document_views.xml',
|
'views/documents_document_views.xml',
|
||||||
'views/documents_folder_views.xml',
|
'views/documents_folder_views.xml',
|
||||||
'views/documents_tag_views.xml',
|
'views/documents_tag_views.xml',
|
||||||
'views/mail_activity_views.xml',
|
'views/mail_activity_views.xml',
|
||||||
'views/mail_activity_plan_views.xml',
|
'views/mail_activity_plan_views.xml',
|
||||||
'views/mail_alias_views.xml',
|
'views/mail_alias_views.xml',
|
||||||
'views/documents_menu_views.xml',
|
'views/documents_menu_views.xml',
|
||||||
'views/documents_templates_portal.xml',
|
'views/documents_templates_portal.xml',
|
||||||
'views/documents_templates_share.xml',
|
'views/documents_templates_share.xml',
|
||||||
'wizard/documents_link_to_record_wizard_views.xml',
|
'wizard/documents_link_to_record_wizard_views.xml',
|
||||||
'wizard/documents_request_wizard_views.xml',
|
'wizard/documents_request_wizard_views.xml',
|
||||||
# Need the `ir.actions.act_window` to exist
|
# Need the `ir.actions.act_window` to exist
|
||||||
'data/ir_actions_server_data.xml',
|
'data/ir_actions_server_data.xml',
|
||||||
],
|
],
|
||||||
|
|
||||||
'demo': [
|
'demo': [
|
||||||
'demo/documents_document_demo.xml',
|
'demo/documents_document_demo.xml',
|
||||||
],
|
],
|
||||||
'license': 'OEEL-1',
|
'license': 'OEEL-1',
|
||||||
'assets': {
|
'assets': {
|
||||||
'web.assets_backend': [
|
'web.assets_backend': [
|
||||||
'documents/static/src/model/**/*',
|
'documents/static/src/model/**/*',
|
||||||
'documents/static/src/scss/documents_views.scss',
|
'documents/static/src/scss/documents_views.scss',
|
||||||
'documents/static/src/scss/documents_kanban_view.scss',
|
'documents/static/src/scss/documents_kanban_view.scss',
|
||||||
'documents/static/src/attachments/**/*',
|
'documents/static/src/attachments/**/*',
|
||||||
'documents/static/src/core/**/*',
|
'documents/static/src/core/**/*',
|
||||||
'documents/static/src/js/**/*',
|
'documents/static/src/js/**/*',
|
||||||
'documents/static/src/owl/**/*',
|
'documents/static/src/owl/**/*',
|
||||||
'documents/static/src/views/**/*',
|
'documents/static/src/views/**/*',
|
||||||
('remove', 'documents/static/src/views/activity/**'),
|
('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'),
|
('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/web/**/*',
|
||||||
'documents/static/src/components/**/*',
|
'documents/static/src/components/**/*',
|
||||||
],
|
],
|
||||||
'web.assets_backend_lazy': [
|
'web.assets_backend_lazy': [
|
||||||
'documents/static/src/views/activity/**',
|
'documents/static/src/views/activity/**',
|
||||||
],
|
],
|
||||||
'web._assets_primary_variables': [
|
'web._assets_primary_variables': [
|
||||||
'documents/static/src/scss/documents.variables.scss',
|
'documents/static/src/scss/documents.variables.scss',
|
||||||
],
|
],
|
||||||
"web.dark_mode_variables": [
|
"web.dark_mode_variables": [
|
||||||
('before', 'documents/static/src/scss/documents.variables.scss', 'documents/static/src/scss/documents.variables.dark.scss'),
|
('before', 'documents/static/src/scss/documents.variables.scss', 'documents/static/src/scss/documents.variables.dark.scss'),
|
||||||
],
|
],
|
||||||
'documents.public_page_assets': [
|
'documents.public_page_assets': [
|
||||||
('include', 'web._assets_helpers'),
|
('include', 'web._assets_helpers'),
|
||||||
('include', 'web._assets_backend_helpers'),
|
('include', 'web._assets_backend_helpers'),
|
||||||
'web/static/src/scss/pre_variables.scss',
|
'web/static/src/scss/pre_variables.scss',
|
||||||
'web/static/lib/bootstrap/scss/_variables.scss',
|
'web/static/lib/bootstrap/scss/_variables.scss',
|
||||||
'web/static/lib/bootstrap/scss/_variables-dark.scss',
|
'web/static/lib/bootstrap/scss/_variables-dark.scss',
|
||||||
'web/static/lib/bootstrap/scss/_maps.scss',
|
'web/static/lib/bootstrap/scss/_maps.scss',
|
||||||
('include', 'web._assets_bootstrap_backend'),
|
('include', 'web._assets_bootstrap_backend'),
|
||||||
'documents/static/src/scss/documents_public_pages.scss',
|
'documents/static/src/scss/documents_public_pages.scss',
|
||||||
],
|
],
|
||||||
'documents.webclient': [
|
'documents.webclient': [
|
||||||
('include', 'web.assets_backend'),
|
('include', 'web.assets_backend'),
|
||||||
# documents webclient overrides
|
# documents webclient overrides
|
||||||
'documents/static/src/portal_webclient/**/*',
|
'documents/static/src/portal_webclient/**/*',
|
||||||
'web/static/src/start.js',
|
'web/static/src/start.js',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from . import documents
|
from . import documents
|
||||||
from . import home
|
from . import home
|
||||||
from . import portal
|
from . import portal
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,86 +1,86 @@
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from odoo.http import request, route
|
from odoo.http import request, route
|
||||||
|
|
||||||
from odoo.addons.web.controllers import home as web_home
|
from odoo.addons.web.controllers import home as web_home
|
||||||
from odoo.addons.web.controllers.utils import ensure_db
|
from odoo.addons.web.controllers.utils import ensure_db
|
||||||
from .documents import ShareRoute
|
from .documents import ShareRoute
|
||||||
|
|
||||||
|
|
||||||
class Home(web_home.Home):
|
class Home(web_home.Home):
|
||||||
def _web_client_readonly(self):
|
def _web_client_readonly(self):
|
||||||
""" Force a read/write cursor for documents.access """
|
""" Force a read/write cursor for documents.access """
|
||||||
path = request.httprequest.path
|
path = request.httprequest.path
|
||||||
if (
|
if (
|
||||||
path.startswith('/odoo/documents')
|
path.startswith('/odoo/documents')
|
||||||
and (request.httprequest.args.get('access_token') or path.removeprefix('/odoo/documents/'))
|
and (request.httprequest.args.get('access_token') or path.removeprefix('/odoo/documents/'))
|
||||||
and request.session.uid
|
and request.session.uid
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
return super()._web_client_readonly()
|
return super()._web_client_readonly()
|
||||||
|
|
||||||
@route(readonly=_web_client_readonly)
|
@route(readonly=_web_client_readonly)
|
||||||
def web_client(self, s_action=None, **kw):
|
def web_client(self, s_action=None, **kw):
|
||||||
""" Handle direct access to a document with a backend URL (/odoo/documents/<access_token>).
|
""" Handle direct access to a document with a backend URL (/odoo/documents/<access_token>).
|
||||||
|
|
||||||
It redirects to the document either in:
|
It redirects to the document either in:
|
||||||
- the backend if the user is logged and has access to the Documents module
|
- 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
|
- 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
|
to the Document module but well to the documents.document model
|
||||||
- or the document portal otherwise
|
- or the document portal otherwise
|
||||||
|
|
||||||
Goal: Allow to share directly the backend URL of a document.
|
Goal: Allow to share directly the backend URL of a document.
|
||||||
"""
|
"""
|
||||||
subpath = kw.get('subpath', '')
|
subpath = kw.get('subpath', '')
|
||||||
access_token = request.params.get('access_token') or subpath.removeprefix('documents/')
|
access_token = request.params.get('access_token') or subpath.removeprefix('documents/')
|
||||||
if not subpath.startswith('documents') or not access_token or '/' in access_token:
|
if not subpath.startswith('documents') or not access_token or '/' in access_token:
|
||||||
return super().web_client(s_action, **kw)
|
return super().web_client(s_action, **kw)
|
||||||
|
|
||||||
# This controller should be auth='public' but it actually is
|
# This controller should be auth='public' but it actually is
|
||||||
# auth='none' for technical reasons (see super). Those three
|
# auth='none' for technical reasons (see super). Those three
|
||||||
# lines restore the public behavior.
|
# lines restore the public behavior.
|
||||||
ensure_db()
|
ensure_db()
|
||||||
request.update_env(user=request.session.uid)
|
request.update_env(user=request.session.uid)
|
||||||
request.env['ir.http']._authenticate_explicit('public')
|
request.env['ir.http']._authenticate_explicit('public')
|
||||||
|
|
||||||
# Public/Portal users use the /documents/<access_token> route
|
# Public/Portal users use the /documents/<access_token> route
|
||||||
if not request.env.user._is_internal():
|
if not request.env.user._is_internal():
|
||||||
return request.redirect(
|
return request.redirect(
|
||||||
f'/documents/{access_token}',
|
f'/documents/{access_token}',
|
||||||
HTTPStatus.TEMPORARY_REDIRECT,
|
HTTPStatus.TEMPORARY_REDIRECT,
|
||||||
)
|
)
|
||||||
|
|
||||||
document_sudo = ShareRoute._from_access_token(access_token, follow_shortcut=False)
|
document_sudo = ShareRoute._from_access_token(access_token, follow_shortcut=False)
|
||||||
|
|
||||||
if not document_sudo:
|
if not document_sudo:
|
||||||
Redirect = request.env['documents.redirect'].sudo()
|
Redirect = request.env['documents.redirect'].sudo()
|
||||||
if document_sudo := Redirect._get_redirection(access_token):
|
if document_sudo := Redirect._get_redirection(access_token):
|
||||||
return request.redirect(
|
return request.redirect(
|
||||||
f'/odoo/documents/{document_sudo.access_token}',
|
f'/odoo/documents/{document_sudo.access_token}',
|
||||||
HTTPStatus.MOVED_PERMANENTLY,
|
HTTPStatus.MOVED_PERMANENTLY,
|
||||||
)
|
)
|
||||||
|
|
||||||
# We want (1) the webclient renders the webclient template and load
|
# We want (1) the webclient renders the webclient template and load
|
||||||
# the document action. We also want (2) the router rewrites
|
# the document action. We also want (2) the router rewrites
|
||||||
# /odoo/documents/<id> to /odoo/documents/<access-token> in the
|
# /odoo/documents/<id> to /odoo/documents/<access-token> in the
|
||||||
# URL.
|
# URL.
|
||||||
# We redirect on /web so this override does kicks in again,
|
# We redirect on /web so this override does kicks in again,
|
||||||
# super() is loaded and renders the normal home template. We add
|
# super() is loaded and renders the normal home template. We add
|
||||||
# custom fragments so we can load them inside the router and
|
# custom fragments so we can load them inside the router and
|
||||||
# rewrite the URL.
|
# rewrite the URL.
|
||||||
query = {}
|
query = {}
|
||||||
if request.session.debug:
|
if request.session.debug:
|
||||||
query['debug'] = request.session.debug
|
query['debug'] = request.session.debug
|
||||||
fragment = {
|
fragment = {
|
||||||
'action': request.env.ref("documents.document_action").id,
|
'action': request.env.ref("documents.document_action").id,
|
||||||
'menu_id': request.env.ref('documents.menu_root').id,
|
'menu_id': request.env.ref('documents.menu_root').id,
|
||||||
'model': 'documents.document',
|
'model': 'documents.document',
|
||||||
}
|
}
|
||||||
if document_sudo:
|
if document_sudo:
|
||||||
fragment.update({
|
fragment.update({
|
||||||
f'documents_init_{key}': value
|
f'documents_init_{key}': value
|
||||||
for key, value
|
for key, value
|
||||||
in ShareRoute._documents_get_init_data(document_sudo, request.env.user).items()
|
in ShareRoute._documents_get_init_data(document_sudo, request.env.user).items()
|
||||||
})
|
})
|
||||||
return request.redirect(f'/web?{urlencode(query)}#{urlencode(fragment)}')
|
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