From 7c51fdcfa2fcdec0fae4852c0f88703e4501e286 Mon Sep 17 00:00:00 2001
From: James Angi <jangi@atggroup.us>
Date: Tue, 16 Dec 2025 16:40:44 +0000
Subject: [PATCH] Custom Attendance Tray

---
 addons/hr_attendance/controllers/main.py      | 10 +++-
 addons/hr_attendance/models/hr_attendance.py  |  9 +++
 addons/hr_attendance/models/hr_employee.py    | 19 +++++-
 .../attendance_menu/attendance_menu.js        | 33 +++++++++--
 .../attendance_menu/attendance_menu.xml       | 58 +++++++++++++++----
 5 files changed, 110 insertions(+), 19 deletions(-)

diff --git a/addons/hr_attendance/controllers/main.py b/addons/hr_attendance/controllers/main.py
index ecbc766d2ca..a8c4ecfee3a 100644
--- a/addons/hr_attendance/controllers/main.py
+++ b/addons/hr_attendance/controllers/main.py
@@ -8,6 +8,7 @@ from odoo.tools import float_round, py_to_js_locale, SQL
 from odoo.tools.image import image_data_uri
 
 import datetime
+import logging
 
 class HrAttendance(http.Controller):
     @staticmethod
@@ -27,6 +28,9 @@ class HrAttendance(http.Controller):
                 'last_check_in': employee.last_check_in,
                 'attendance_state': employee.attendance_state,
                 'display_systray': employee.company_id.attendance_from_systray,
+                'last_customer_id': employee.last_customer.id,
+                'travel_time': request.env['hr.attendance'].sudo().search([
+                    ('employee_id', '=', employee.id),('check_out', '=', False)], limit=1).total_travel_time or 0.0,
             }
         return response
 
@@ -46,7 +50,7 @@ class HrAttendance(http.Controller):
                     ('employee_id', '=', employee.id), ('date', '=', datetime.date.today()),
                     ('adjustment', '=', False)]).duration or 0,
                 'use_pin': employee.company_id.attendance_kiosk_use_pin,
-                'display_overtime': employee.company_id.hr_attendance_display_overtime
+                'display_overtime': employee.company_id.hr_attendance_display_overtime,
             }
         return response
 
@@ -169,12 +173,12 @@ class HrAttendance(http.Controller):
         return []
 
     @http.route('/hr_attendance/systray_check_in_out', type="json", auth="user")
-    def systray_attendance(self, latitude=False, longitude=False):
+    def systray_attendance(self, latitude=False, longitude=False, customer=0, travel_time=0.0):
         employee = request.env.user.employee_id
         geo_ip_response = self._get_geoip_response(mode='systray',
                                                   latitude=latitude,
                                                   longitude=longitude)
-        employee._attendance_action_change(geo_ip_response)
+        employee._attendance_action_change(geo_ip_response,customer,travel_time)
         return self._get_employee_info_response(employee)
 
     @http.route('/hr_attendance/attendance_user_data', type="json", auth="user", readonly=True)
diff --git a/addons/hr_attendance/models/hr_attendance.py b/addons/hr_attendance/models/hr_attendance.py
index 705a13783c6..ef0789e814c 100644
--- a/addons/hr_attendance/models/hr_attendance.py
+++ b/addons/hr_attendance/models/hr_attendance.py
@@ -1,6 +1,7 @@
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
 import pytz
+import logging
 
 from calendar import monthrange
 from collections import defaultdict
@@ -32,6 +33,9 @@ class HrAttendance(models.Model):
     def _default_employee(self):
         return self.env.user.employee_id
 
+    total_travel_time = fields.Float(string='Total Travel Time', store=True)
+    customer_id = fields.Many2one('res.partner', string='Customer', store=True, domain=[('customer_rank', '!=', 0)])
+
     employee_id = fields.Many2one('hr.employee', string="Employee", default=_default_employee, required=True,
         ondelete='cascade', index=True, group_expand='_read_group_employee_id')
     department_id = fields.Many2one('hr.department', string="Department", related="employee_id.department_id",
@@ -471,6 +475,8 @@ class HrAttendance(models.Model):
 
     @api.model_create_multi
     def create(self, vals_list):
+        _logger = logging.getLogger(__name__)
+        _logger.info(f"Vals to be written: {vals_list}")
         res = super().create(vals_list)
         res._update_overtime()
         no_validation = res.filtered(lambda att: att.employee_id.company_id.attendance_overtime_validation == 'no_validation')
@@ -478,6 +484,8 @@ class HrAttendance(models.Model):
         return res
 
     def write(self, vals):
+        _logger = logging.getLogger(__name__)
+        _logger.info(f"Vals to be written: {vals}")
         if vals.get('employee_id') and \
             vals['employee_id'] not in self.env.user.employee_ids.ids and \
             not self.env.user.has_group('hr_attendance.group_hr_attendance_officer'):
@@ -489,6 +497,7 @@ class HrAttendance(models.Model):
         if vals.get('check_in') and self.in_mode == 'technical':
             vals.update({'in_mode': 'manual'})
         result = super(HrAttendance, self).write(vals)
+        _logger.info(f"Result: {result}")
         if any(field in vals for field in ['employee_id', 'check_in', 'check_out']):
             # Merge attendance dates before and after write to recompute the
             # overtime if the attendances have been moved to another day
diff --git a/addons/hr_attendance/models/hr_employee.py b/addons/hr_attendance/models/hr_employee.py
index 5bdd3a62680..3f029b831fe 100644
--- a/addons/hr_attendance/models/hr_employee.py
+++ b/addons/hr_attendance/models/hr_employee.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
 
+import logging
 import pytz
 from dateutil.relativedelta import relativedelta
 
@@ -48,6 +49,8 @@ class HrEmployee(models.Model):
         'hr.attendance.overtime', 'employee_id', groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user")
     total_overtime = fields.Float(
         compute='_compute_total_overtime', compute_sudo=True, groups="hr_attendance.group_hr_attendance_officer,hr.group_hr_user")
+    last_customer = fields.Many2one(
+        'res.partner', store=True)
 
     @api.model_create_multi
     def create(self, vals_list):
@@ -158,25 +161,33 @@ class HrEmployee(models.Model):
             att = employee.last_attendance_id.sudo()
             employee.attendance_state = att and not att.check_out and 'checked_in' or 'checked_out'
 
-    def _attendance_action_change(self, geo_information=None):
+    def _attendance_action_change(self, geo_information=None, customer=0, travel_time=0.0):
         """ Check In/Check Out action
             Check In: create a new attendance record
             Check Out: modify check_out field of appropriate attendance record
         """
+        _logger = logging.getLogger(__name__)
+
         self.ensure_one()
         action_date = fields.Datetime.now()
 
+        self.last_customer = customer
+
         if self.attendance_state != 'checked_in':
             if geo_information:
                 vals = {
                     'employee_id': self.id,
                     'check_in': action_date,
+                    'customer_id': customer,
+                    'total_travel_time': travel_time,
                     **{'in_%s' % key: geo_information[key] for key in geo_information}
                 }
             else:
                 vals = {
                     'employee_id': self.id,
                     'check_in': action_date,
+                    'customer_id': customer,
+                    'total_travel_time': travel_time,
                 }
             return self.env['hr.attendance'].create(vals)
         attendance = self.env['hr.attendance'].search([('employee_id', '=', self.id), ('check_out', '=', False)], limit=1)
@@ -184,11 +195,15 @@ class HrEmployee(models.Model):
             if geo_information:
                 attendance.write({
                     'check_out': action_date,
+                    'customer_id': customer,
+                    'total_travel_time': travel_time,
                     **{'out_%s' % key: geo_information[key] for key in geo_information}
                 })
             else:
                 attendance.write({
-                    'check_out': action_date
+                    'check_out': action_date,
+                    'customer_id': customer,
+                    'total_travel_time': travel_time,
                 })
         else:
             raise exceptions.UserError(_(
diff --git a/addons/hr_attendance/static/src/components/attendance_menu/attendance_menu.js b/addons/hr_attendance/static/src/components/attendance_menu/attendance_menu.js
index 98a00972f87..2abdc4cf8fc 100644
--- a/addons/hr_attendance/static/src/components/attendance_menu/attendance_menu.js
+++ b/addons/hr_attendance/static/src/components/attendance_menu/attendance_menu.js
@@ -1,7 +1,7 @@
 /* @odoo-module */
 
 
-import { Component, useState } from "@odoo/owl";
+import { Component, useState,onWillStart } from "@odoo/owl";
 import { Dropdown } from "@web/core/dropdown/dropdown";
 import { DropdownItem } from "@web/core/dropdown/dropdown_item";
 import { useDropdownState } from "@web/core/dropdown/dropdown_hooks";
@@ -19,6 +19,15 @@ export class ActivityMenu extends Component {
 
     setup() {
         this.ui = useState(useService("ui"));
+this.orm = useService("orm");
+this.partners = [];
+onWillStart(async () => {
+this.partners = await this.orm.searchRead(
+'res.partner', // Model name
+[['customer_rank','!=',0]], // Search domain (fetch all partners)
+['name', 'email'] // Fields to read
+);
+});
         this.employee = false;
         this.state = useState({
             checkedIn: false,
@@ -48,24 +57,38 @@ export class ActivityMenu extends Component {
             this.state.checkedIn = this.employee.attendance_state === "checked_in";
             this.isFirstAttendance = this.employee.hours_previously_today === 0;
             this.state.isDisplayed = this.employee.display_systray
+            this.travel_time = this.employee.travel_time
         } else {
             this.state.isDisplayed = false
         }
     }
 
     async signInOut() {
+       const dropdownElement = document.getElementById('customer');
+       const customer = Number(dropdownElement.value);
+        const travelTimeElement = document.getElementById('travel_time');
+        var travel_time = 0.0;
+        if (travelTimeElement) {
+            travel_time = parseFloat(travelTimeElement.value);
+        }
+
         this.dropdown.close();
         if (!isIosApp()) { // iOS app lacks permissions to call `getCurrentPosition`
             navigator.geolocation.getCurrentPosition(
                 async ({coords: {latitude, longitude}}) => {
                     await rpc("/hr_attendance/systray_check_in_out", {
                         latitude,
-                        longitude
+                        longitude,
+                        customer,
+                        travel_time
                     })
                     await this.searchReadEmployee()
                 },
                 async err => {
-                    await rpc("/hr_attendance/systray_check_in_out")
+                    await rpc("/hr_attendance/systray_check_in_out", {
+                    customer,
+                    travel_time
+                       })
                     await this.searchReadEmployee()
                 },
                 {
@@ -73,7 +96,9 @@ export class ActivityMenu extends Component {
                 }
             )
         } else {
-            await rpc("/hr_attendance/systray_check_in_out")
+            await rpc("/hr_attendance/systray_check_in_out", {
+            customer
+               })
             await this.searchReadEmployee()
         }
     }
diff --git a/addons/hr_attendance/static/src/components/attendance_menu/attendance_menu.xml b/addons/hr_attendance/static/src/components/attendance_menu/attendance_menu.xml
index 6a07afa50a6..067f91b3827 100644
--- a/addons/hr_attendance/static/src/components/attendance_menu/attendance_menu.xml
+++ b/addons/hr_attendance/static/src/components/attendance_menu/attendance_menu.xml
@@ -27,18 +27,56 @@
                                 <small class="text-muted">Total today</small>
                                 <h2 t-esc="this.hoursToday" class="mb-0 fs-2"/>
                             </div>
-                            <button t-on-click="() => this.signInOut()" class="flex-basis-100 mt-3" t-attf-class="btn btn-{{ this.state.checkedIn ? 'warning' : 'success' }}">
-                                <span t-if="!this.state.checkedIn">Check in</span>
-                                <span t-else="">Check out</span>
-                                <i t-attf-class="fa fa-sign-{{ this.state.checkedIn ? 'out' : 'in' }} ms-1"/>
-                            </button>
+                            <t t-if="!this.state.checkedIn">
+                                <button t-on-click="() => this.signInOut()" class="flex-basis-100 mt-3" t-attf-class="btn btn-{{ this.state.checkedIn ? 'warning' : 'success' }}">
+                                    <span>Check in</span>
+                                </button>
+                            </t>
+                            <t t-else="">
+                                <div>
+                                    <small class="text-muted">Travel Time:</small>
+                                    <input id="travel_time" type="number" t-att-value='this.travel_time'/>
+                                </div>
+                                <button t-on-click="() => this.signInOut()" class="flex-basis-100 mt-3" t-attf-class="btn btn-{{ this.state.checkedIn ? 'warning' : 'success' }}">
+                                    <span>Check out</span>
+                                    <i t-attf-class="fa fa-sign-{{ this.state.checkedIn ? 'out' : 'in' }} ms-1"/>
+                                </button>
+                            </t>
                         </div>
                     </div>
-                    <button t-if="this.isFirstAttendance" t-on-click="() => this.signInOut()" t-attf-class="btn btn-{{ this.state.checkedIn ? 'warning' : 'success' }}">
-                        <span t-if="!this.state.checkedIn">Check in</span>
-                        <span t-else="">Check out</span>
-                        <i t-attf-class="fa fa-sign-{{ this.state.checkedIn ? 'out' : 'in' }} ms-1"/>
-                    </button>
+                    <div>
+                        <small class="text-muted">Customer:</small>
+                        <select id='customer'>
+                        <option value=''>None</option>
+                        <t t-foreach='this.partners' t-as='partner' t-key='partner.id'>
+                            <t t-if="partner.id == this.employee.last_customer_id">
+                                <option t-att-value='partner.id' selected="selected">
+                                    <t t-esc='partner.name'/>
+                                </option>
+                            </t>
+                            <t t-else="">
+                                <option t-att-value='partner.id'>
+                                    <t t-esc='partner.name'/>
+                                </option>
+                            </t>
+                        </t>
+                        </select>
+                    </div>
+                    <t t-if="!this.state.checkedIn">
+                        <button t-if="this.isFirstAttendance" t-on-click="() => this.signInOut()" t-attf-class="btn btn-{{ this.state.checkedIn ? 'warning' : 'success' }}">
+                            <span>Check in</span>
+                        </button>
+                    </t>
+                    <t t-else="">
+                        <div>
+                            <small class="text-muted">Travel Time:</small>
+                            <input id="travel_time" type="number" t-att-value='this.travel_time'/>
+                        </div>
+                        <button t-if="this.isFirstAttendance" t-on-click="() => this.signInOut()" t-attf-class="btn btn-{{ this.state.checkedIn ? 'warning' : 'success' }}">
+                            <span>Check out</span>
+                            <i t-attf-class="fa fa-sign-{{ this.state.checkedIn ? 'out' : 'in' }} ms-1"/>
+                        </button>
+                    </t>
                 </div>
             </t>
         </Dropdown>
-- 
2.43.0