Compare commits

...

149 Commits

Author SHA1 Message Date
pranay c047dcb8c3 Merge pull request 'project internal users and roles changes' (#27) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/27
2026-07-20 18:12:33 +05:30
pranaysaidurga 5bce5cca2b project internal users and roles changes 2026-07-20 18:11:06 +05:30
pranay db11c27970 Merge pull request 'login page custom' (#26) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/26
2026-07-20 15:30:58 +05:30
pranaysaidurga 1d0833faf8 login page custom 2026-07-16 15:34:09 +05:30
pranay 3cfba75a0a Merge pull request 'feature/share_module' (#25) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/25
2026-07-16 10:05:05 +05:30
seshikanth dfca6bf90b #fix: logo 2026-07-15 18:33:50 +05:30
seshikanth 524bc488a7 #fix: contract 2026-07-15 18:00:48 +05:30
pranay 4a8452ae5f Merge pull request 'feature/share_module' (#24) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/24
2026-07-14 18:33:19 +05:30
pranaysaidurga e5e876ecb4 project task timesheets sprints and kanban buttons 2026-07-14 18:30:46 +05:30
pranaysaidurga fd866fb866 project task timesheets related roles update 2026-07-14 16:11:54 +05:30
pranaysaidurga 1335c60b76 project lead changes and multi company dependencies 2026-07-14 15:22:48 +05:30
karuna a003e4d566 updation on project lead from single to many users 2026-07-14 15:19:59 +05:30
pranay fa78117a1d Merge pull request 'feature/share_module' (#23) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/23
2026-07-14 13:17:21 +05:30
pranaysaidurga c52ff00374 work mode updated in the grace period 2026-07-14 11:34:00 +05:30
karuna d7dadb5974 enhancements of Reuqesttimelines 2026-07-13 18:14:19 +05:30
pranay ebe6146e15 Merge pull request 'feature/share_module' (#22) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/22
2026-07-13 17:00:38 +05:30
pranaysaidurga 923f2beab4 project Record rule 2026-07-13 16:58:23 +05:30
karuna 9d6234cc8e enhancements of Reuqesttimelines 2026-07-13 16:56:07 +05:30
karuna f529865975 Implemented Roles Management and Weekly Timesheet enhancements 2026-07-13 15:59:43 +05:30
seshikanth 778a34be12 #fix: Baseview 2026-07-13 15:19:54 +05:30
deepak a9b2e067d4 Update Dockerfile
added firebase-admin
2026-07-13 12:28:54 +05:30
deepak 0af6fea5dd Update requirements.txt
added firebase-admin
2026-07-13 12:28:12 +05:30
deepak eec20ae335 Update requirements.txt
pyzk added in requirement.txt file
2026-07-13 10:51:50 +05:30
administrator 94aec2f277 Docker file edit 2026-07-13 10:32:34 +05:30
karuna 59285440a5 Implemented Roles Management and Weekly Timesheet enhancements 2026-07-10 16:34:39 +05:30
seshikanth 92a45a847b #fix: webpage 2026-07-10 09:44:53 +05:30
seshikanth febfa6bb98 #fix: webpage 2026-07-10 09:44:33 +05:30
pranay d69e51e21c Merge pull request '#fix: webpage' (#16) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/16
2026-07-10 09:43:41 +05:30
seshikanth 12af7850e1 #fix: webpage 2026-07-10 09:09:11 +05:30
pranay e5300ec792 Merge pull request 'hrms emp dashboard changes' (#14) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/14
2026-07-09 22:22:25 +05:30
pranaysaidurga e93480648a hrms emp dashboard changes 2026-07-09 22:21:11 +05:30
pranay 9f4f3f385d Merge pull request 'feature/share_module' (#13) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/13
2026-07-09 20:48:36 +05:30
pranaysaidurga a707df36cf Employee dashboards 2026-07-09 20:10:34 +05:30
seshikanth 0219b31b79 #fix: webpage 2026-07-09 19:34:06 +05:30
pranay 7de546b752 Merge pull request '#fix: webpage' (#12) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/12
2026-07-09 19:24:23 +05:30
seshikanth 65ed31b3c9 #fix: webpage 2026-07-09 19:20:46 +05:30
pranay f0d4c61fe9 Merge pull request 'fix: recruitment user issue' (#11) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/11
2026-07-09 16:52:20 +05:30
pranaysaidurga 9f97aee937 fix: recruitment user issue 2026-07-09 16:46:49 +05:30
administrator 37579dbca8 Docker improvements, websocket support, CI/CD preparation 2026-07-09 15:11:05 +05:30
administrator 54885316af Docker improvements, websocket support, CI/CD preparation 2026-07-09 14:57:06 +05:30
pranay f76fb9fac2 Merge pull request 'fix: employee, payroll, resignation, recruitment' (#10) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/10
2026-07-09 14:45:10 +05:30
pranaysaidurga 23e8323d6c fix: employee, payroll, resignation, recruitment 2026-07-09 14:43:41 +05:30
pranay ab0843c9df Merge pull request 'feature/share_module' (#9) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/9
2026-07-08 18:32:43 +05:30
pranaysaidurga 42e022bb7f fixes related to company debranding 2026-07-08 18:30:54 +05:30
seshikanth eff39e4662 #fix: JoinformChanges 2026-07-08 18:02:34 +05:30
pranay d9dae88d30 Merge pull request 'fix multiple applicants mismatch' (#8) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/8
2026-07-08 15:40:03 +05:30
pranaysaidurga 59c504e06c fix multiple applicants mismatch 2026-07-08 15:38:55 +05:30
pranay c08f41be29 Merge pull request 'employee jod changes' (#7) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/7
2026-07-08 12:21:44 +05:30
pranaysaidurga ad3dffc6fa employee jod changes 2026-07-08 12:20:31 +05:30
pranay 058781dd90 Merge pull request 'feature/share_module' (#6) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/6
2026-07-07 18:19:32 +05:30
pranaysaidurga 87578af4c3 menu contrel center user access 2026-07-07 18:17:51 +05:30
pranaysaidurga 84c8e601e9 fix emp jod 2026-07-07 14:03:24 +05:30
pranay 0bc6e860db Merge pull request 'feature/share_module' (#5) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/5
2026-07-07 13:44:02 +05:30
pranaysaidurga 5e4c332ff1 Project & menu control center visibility fix 2026-07-07 13:41:53 +05:30
karuna b44c591082 Role Based Group Management 2026-07-07 11:56:58 +05:30
seshikanth d0ed9e4e9d #fix:Leave Screen 2026-07-07 11:41:17 +05:30
pranay 665768cf00 Merge pull request 'feature/share_module' (#4) from feature/share_module into srivyn_test
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/4
2026-07-06 19:10:17 +05:30
pranaysaidurga 445bc8e6d4 payroll dependencies issues 2026-07-06 18:56:00 +05:30
pranaysaidurga 391f270ff3 payroll fixes 2026-07-06 17:59:31 +05:30
seshikanth 06338ce4f0 #fix:Added Disciplinary 2026-07-06 17:54:17 +05:30
pranaysaidurga d913750402 payroll and recruitment multi company bug issues 2026-07-06 15:01:57 +05:30
pranaysaidurga 1696e0ded1 recruitment multi company issues 2026-07-03 12:17:14 +05:30
seshikanth ea58c69ed8 #fix:PERformancemanagement 2026-07-03 10:54:28 +05:30
pranaysaidurga 5e617d3ff8 payroll changes 2026-07-01 17:22:14 +05:30
seshikanth 625bd67064 #fix:HRMSdashboard 2026-06-30 16:17:45 +05:30
seshikanth 3de90ee7ce #fix:Payrollstatement 2026-06-30 12:24:02 +05:30
seshikanth 257e05cc34 #fix:payrolland leave 2026-06-29 12:48:07 +05:30
Bhagya-K 80e90ca88a Merge remote-tracking branch 'origin/feature/share_module' into feature/share_module 2026-06-29 11:50:43 +05:30
Bhagya-K a1320afd5b DEV: Probation Tracking 2026-06-29 11:50:02 +05:30
pranaysaidurga cf0b469b21 recruitment mail issue fix 2026-06-26 17:52:45 +05:30
pranaysaidurga 1b34ae2c5c recruitment bug fixes 2026-06-26 17:26:33 +05:30
pranaysaidurga 08569e0ae0 recruitment fixes 2026-06-25 17:05:21 +05:30
pranaysaidurga 77827a188f payroll fixes 2026-06-25 13:22:45 +05:30
pranaysaidurga aecdbab7c2 helpdesk issue 2026-06-25 13:13:45 +05:30
seshikanth f40d78f5dc #fix:Discplinary few HRMS bugs 2026-06-24 15:33:37 +05:30
seshikanth 3ff97c7c8a #fix: Employee Performance Management Module and few HRMS bugs 2026-06-24 15:23:48 +05:30
seshikanth adc4733e15 #fix: Employee Performance Management Module and few HRMS bugs 2026-06-24 12:20:06 +05:30
Bhagya-K a8570dea30 DEV: Probation Tracking 2026-06-22 15:04:56 +05:30
Bhagya-K 7f8a627a8c DEV: Probation Tracking 2026-06-22 15:02:55 +05:30
Bhagya-K 19669a7a3a Merge remote-tracking branch 'origin/feature/share_module' into feature/share_module 2026-06-22 15:01:22 +05:30
Bhagya-K d448713449 DEV: Probation Tracking 2026-06-22 15:01:12 +05:30
pranaysaidurga a9a9f08ff9 recritment bug fixes 2026-06-19 14:05:09 +05:30
pranaysaidurga 7960d1926b recruitment bug fixes 2026-06-17 15:51:56 +05:30
Bhagya-K e2c8a25c7b DEV: Probation Tracking 2026-06-16 17:05:42 +05:30
Bhagya-K 9ce8c135d0 Merge remote-tracking branch 'origin/feature/share_module' into feature/share_module 2026-06-15 10:59:53 +05:30
Bhagya-K 8877a4ebc4 DEV: Sequence genartion for Employee Code 2026-06-15 10:59:42 +05:30
pranaysaidurga 12ec001b38 business travel expenses 2026-06-12 15:00:28 +05:30
pranaysaidurga 064bd90c58 ALL updates regrading the hrms, ats and pmt 2026-06-12 12:17:49 +05:30
raman 29af1ebf29 Merge pull request 'docker files added' (#3) from feature/odoo18_docker into feature/share_module
Reviewed-on: https://gitea.ftprotech.in/administrator/odoo18/pulls/3
2026-06-10 17:35:22 +05:30
Deepak 41f493840b docker files added 2026-06-10 16:53:14 +05:30
Bhagya-K 54541998c5 Merge remote-tracking branch 'origin/feature/share_module' into feature/share_module 2026-06-10 12:00:05 +05:30
Bhagya-K 68e62956b7 resolved installation issue of grace period module 2026-06-10 11:59:42 +05:30
seshikanth 14e725be4f #fix: Employee Performance Management Module 2026-06-09 13:31:02 +05:30
Bhagya-K adfe801d8e Roster management and grace period changes 2026-06-08 17:01:09 +05:30
Bhagya-K 5c6341d8b7 Merge remote-tracking branch 'origin/feature/share_module' into feature/share_module
# Conflicts:
#	addons_extensions/grace_period/models/__init__.py
2026-06-08 16:33:01 +05:30
Bhagya-K 71f416e8e8 grace Period 2026-06-08 16:30:00 +05:30
pranaysaidurga b5f643fb18 attendance, leaves, weekly timesheets enhancements 2026-06-05 16:42:23 +05:30
karuna 3092032fee enhancements of PMT 2026-06-05 15:16:49 +05:30
karuna f118600ab6 enhancements of PMT 2026-06-04 15:36:32 +05:30
karuna 05bdddc472 enhancements of PMT 2026-06-04 11:06:43 +05:30
pranaysaidurga 582225e11e employee issue 2026-06-04 10:51:17 +05:30
pranaysaidurga 478c1708fb Auto doc get default recruitment 2026-06-04 10:43:54 +05:30
pranaysaidurga 945aedc0b4 Recruitement Dashboards 2026-06-03 14:23:00 +05:30
pranaysaidurga eb17d717dd Project Changes 2026-06-03 10:55:50 +05:30
pranaysaidurga 604d556501 Recruitment changes 2026-06-03 10:48:17 +05:30
karuna 90211776a1 small enhancements 2026-05-28 12:20:45 +05:30
karuna 923304f759 project enhancements 2026-05-26 15:03:48 +05:30
pranaysaidurga 96493be796 Leaves Timesheets Management 2026-05-25 16:38:40 +05:30
pranaysaidurga 5460f6c207 Employee it declaration 2026-05-25 16:10:45 +05:30
pranaysaidurga 4db7e5ade2 Document parser upload 2026-05-20 18:59:16 +05:30
pranaysaidurga f2788e025d bench management system changes 2026-05-19 16:10:05 +05:30
pranaysaidurga 0e51ac85e9 user_ids in project tasks bug 2026-05-19 13:48:45 +05:30
pranaysaidurga 19e5f1db80 Project, recruitment, payroll, bench management changes and updates 2026-05-19 13:36:24 +05:30
karuna c2e33753bb project modifications 2026-05-19 12:08:41 +05:30
pranaysaidurga f6bfd46f2c my tasks issue fix 2026-05-14 12:23:28 +05:30
pranaysaidurga c9746456b8 equipment extended 2026-05-13 11:28:50 +05:30
pranaysaidurga b0ec5ee508 Menu Control Center Functionality change 2026-05-12 11:37:01 +05:30
pranaysaidurga e2d6a8c417 user own estimations 2026-05-07 14:14:41 +05:30
pranaysaidurga 9c33507a45 multi company changes 2026-05-06 18:17:04 +05:30
pranaysaidurga a9a6c683d7 new timeline feature and bug fixes 2026-05-06 17:26:15 +05:30
pranaysaidurga 723dcbe225 portfolio company rules 2026-05-06 11:51:28 +05:30
pranaysaidurga 4139e5fa33 company_id constrain fix 2026-05-06 10:56:13 +05:30
pranaysaidurga ce93d9601c project updates and changes 2026-05-05 11:55:32 +05:30
pranaysaidurga 1b21175e75 accounting kit 2026-05-04 10:56:10 +05:30
pranaysaidurga 74526cc1a2 project task issue fix and changes 2026-04-27 16:14:25 +05:30
pranaysaidurga 6a32ac3f37 project task creation issue 2026-04-27 15:27:27 +05:30
pranaysaidurga fa3833bac3 Project Employee view 2026-04-27 12:37:57 +05:30
pranay 73a27d8921 master selector fix 2026-04-14 11:45:11 +05:30
pranay 26923e20b9 bug fix 2026-04-13 18:21:55 +05:30
karuna ff806505e2 PMT bug fix 2026-04-13 17:56:48 +05:30
karuna b5b276f552 Module Master Selector 2026-03-10 10:45:00 +05:30
Pranay 5d6c2c09aa list view scrolling issue 2026-02-05 12:23:55 +05:30
Pranay 4b85cc0f59 Recruitment changes, Menu Contral Center xml change, attachment preview changes 2026-02-04 11:57:31 +05:30
Pranay ad5967d420 ica web responsive theme 2025-12-24 17:50:22 +05:30
Pranay 53f90a7834 packages list 2025-12-24 17:29:23 +05:30
Pranay 87824199d0 project management system Commit 2025-12-24 12:50:57 +05:30
karuna 92543295d6 adding page visibility based on stages 2025-12-15 18:07:11 +05:30
Pranay 7b6d108ace one2many search widget and project task module updates 2025-12-15 11:25:18 +05:30
Pranay 6f77059f85 PMT, UNIVERSAL ATTACHMENT PREVIEW, MENU CONTROL CENTER INTEGRATION 2025-12-10 10:09:23 +05:30
Pranay c93d208990 Document Preview 2025-12-03 10:19:21 +05:30
Pranay bfd7890cbc Documents preview, binary field widget, menu control center changes 2025-12-03 10:16:19 +05:30
karuna d362ef87aa kudos changes commit 2025-12-03 10:11:32 +05:30
Pranay 20d22c1f04 Project timesheet updates 2025-11-25 16:45:09 +05:30
pranay 44e5ee7e2f ALL MODULE CHANGES 2025-11-20 10:11:59 +05:30
pranay 0fa84c6d43 PMS Updates 2025-11-20 10:06:10 +05:30
karuna 4ce02e58fa Rewards Module for PMS 2025-11-13 12:43:31 +05:30
pranay 66077d1819 Project Task Extended Changes 2025-11-13 12:36:54 +05:30
karuna 2dbdb58127 internal team model update 2025-11-10 10:14:09 +05:30
pranay 5f267e96da PROJECT MODULE AND THEME ADDED IN SHARED MODULES 2025-11-04 11:57:04 +05:30
3586 changed files with 1446543 additions and 1264924 deletions

151
Dockerfile Normal file
View File

@ -0,0 +1,151 @@
FROM python:3.12-bookworm
# ------------------------------------------------------------------
# Environment
# ------------------------------------------------------------------
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
LANG=C.UTF-8 \
TZ=Asia/Kolkata
# ------------------------------------------------------------------
# Install PostgreSQL Repository & System Packages
# ------------------------------------------------------------------
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
gcc \
g++ \
git \
curl \
npm \
wget \
tini \
gnupg \
ca-certificates \
lsb-release && \
mkdir -p /etc/apt/keyrings && \
wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | \
gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list && \
apt-get update && \
apt-get install -y --no-install-recommends \
postgresql-client-16 \
libpq-dev \
libldap2-dev \
libsasl2-dev \
libxml2-dev \
libxslt1-dev \
libjpeg62-turbo-dev \
zlib1g-dev \
libffi-dev \
libssl-dev \
liblcms2-dev \
libblas-dev \
libatlas-base-dev \
libyaml-dev \
libevent-dev \
fonts-dejavu \
iproute2 \
net-tools \
wkhtmltopdf && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# ------------------------------------------------------------------
# Install LESS Compiler (Required by Odoo)
# ------------------------------------------------------------------
RUN npm install -g less less-plugin-clean-css
# ------------------------------------------------------------------
# Create Odoo User
# ------------------------------------------------------------------
RUN useradd \
--system \
--create-home \
--home-dir /opt/odoo \
--shell /bin/bash \
odoo
# ------------------------------------------------------------------
# Working Directory
# ------------------------------------------------------------------
WORKDIR /opt/odoo/source
# ------------------------------------------------------------------
# Upgrade pip
# ------------------------------------------------------------------
RUN pip install --no-cache-dir --upgrade \
pip \
setuptools \
wheel
# ------------------------------------------------------------------
# Copy Application Source
# ------------------------------------------------------------------
COPY . .
# ------------------------------------------------------------------
# Install Python Dependencies
# ------------------------------------------------------------------
RUN if [ -f requirements.txt ]; then \
pip install --no-cache-dir -r requirements.txt; \
fi && \
pip install --no-cache-dir \
psycopg2-binary \
pandas \
qifparse \
pypdf \
phonenumbers \
python-docx \
pyzk \
firebase-admin
# ------------------------------------------------------------------
# Create Required Directories
# ------------------------------------------------------------------
RUN mkdir -p \
/var/lib/odoo \
/var/log/odoo \
/mnt/extra-addons && \
chown -R odoo:odoo \
/opt/odoo \
/var/lib/odoo \
/var/log/odoo \
/mnt/extra-addons
USER odoo
# ------------------------------------------------------------------
# Expose Odoo Port
# ------------------------------------------------------------------
EXPOSE 8069
# ------------------------------------------------------------------
# Health Check
# ------------------------------------------------------------------
HEALTHCHECK --interval=30s \
--timeout=10s \
--start-period=60s \
--retries=5 \
CMD curl -fs http://127.0.0.1:8069/web/login || exit 1
# ------------------------------------------------------------------
# Init
# ------------------------------------------------------------------
ENTRYPOINT ["/usr/bin/tini","--"]
CMD ["python3", "odoo-bin", "-c", "/etc/odoo/odoo.conf"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
{
'name': 'Base Changes',
'category': 'Base',
'version': '1.1',
'author': 'Seshi Kanth',
'summary': 'Changes of the form',
'description': 'This module contains form changes.',
'depends': [
'base',
'web',
],
'data': [
# 'views/user_menu.xml',
],
'assets': {
'web.assets_backend': [
'base_custom/static/src/css/backend.css',
'base_custom/static/src/js/user_menu_patch.js',
'base_custom/static/src/js/hide_user_menu.js',
'base_custom/static/src/xml/user_menu.xml',
],
},
'installable': True,
'application': True,
'license': 'LGPL-3',
}

View File

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

View File

@ -0,0 +1,17 @@
from odoo import models
class IrHttp(models.AbstractModel):
_inherit = "ir.http"
def session_info(self):
session_info = super().session_info()
employee = self.env.user.employee_id
session_info.update({
"employee_name": employee.name or self.env.user.name,
"employee_designation": employee.job_id.name if employee.job_id else "",
})
return session_info

View File

@ -0,0 +1,23 @@
.o_switch_company_menu .oe_topbar_name{
display: none !important;
}
.oe_topbar_name{
display:flex;
flex-direction:column;
line-height:1.2;
}
.oe_topbar_name .fw-bold{
font-size:13px;
font-weight:600;
}
.oe_topbar_name .designation{
font-size:11px;
color:#7b7b7b;
}
.text-muted {
color: #000000 !important;
}

View File

@ -0,0 +1,18 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
const userMenuRegistry = registry.category("user_menuitems");
const itemsToHide = [
"documentation",
"support",
"shortcuts",
"odoo_account",
];
for (const item of itemsToHide) {
if (userMenuRegistry.contains(item)) {
userMenuRegistry.remove(item);
}
}

View File

@ -0,0 +1,23 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { UserMenu } from "@web/webclient/user_menu/user_menu";
import { session } from "@web/session";
patch(UserMenu.prototype, {
setup() {
super.setup();
this.employeeName = session.employee_name;
this.employeeDesignation = session.employee_designation;
},
get employeeInfo() {
return {
employeeName: this.employeeName,
employeeDesignation: this.employeeDesignation,
};
},
});

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="web.UserMenu"
t-inherit-mode="extension">
<xpath expr="//small[contains(@class,'oe_topbar_name')]"
position="replace">
<small class="oe_topbar_name d-none d-lg-inline-block ms-2 text-start"
style="max-width:220px">
<div class="fw-bold text-truncate">
<t t-esc="employeeInfo.employeeName"/>
</div>
<div class="text-muted text-truncate designation">
<t t-esc="employeeInfo.employeeDesignation"/>
</div>
</small>
</xpath>
</t>
</templates>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,21 @@
{ {
'name': 'Consolidated Payslip Grid (OWL)', '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,
} }

View File

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

View File

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

1 id name model_id:id group_id perm_read perm_write perm_create perm_unlink

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,36 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?> <?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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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