fix(fusion_clock): kiosk photo now shows on clock + profile (right image fields)

Root-caused on live entech (not guessed):
- The kiosk runs as a non-HR operator (uid 141) who gets AccessError reading
  hr.employee images, so /web/image served a placeholder. Point the result-card
  avatar at hr.employee.public/avatar_128 — verified readable as the operator,
  returns the real photo. (Odoo's own UI uses .public for employee images.)
- The Odoo profile/preferences avatar is res.users → res.partner.image_1920,
  which the capture never wrote. Propagate the captured photo to the linked
  user's partner image so the profile updates too.
- Enlarge the capture oval (it was small): stage 62vh/520px, guide width 64%.

Live as 19.0.3.11.4. Also backfilled the existing test photo to the user's
partner image so the profile shows it without re-capturing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-05-30 18:29:46 -04:00
parent e26a7cd9e8
commit 3fd074ff6d
3 changed files with 14 additions and 5 deletions

View File

@@ -5,7 +5,7 @@
{ {
'name': 'Fusion Clock', 'name': 'Fusion Clock',
'version': '19.0.3.11.3', 'version': '19.0.3.11.4',
'category': 'Human Resources/Attendances', 'category': 'Human Resources/Attendances',
'summary': 'Complete Employee T&A with Geofencing, Shifts, Penalties, Overtime, Kiosk, Dashboard & Payroll Export', 'summary': 'Complete Employee T&A with Geofencing, Shifts, Penalties, Overtime, Kiosk, Dashboard & Payroll Export',
'description': """ 'description': """

View File

@@ -256,6 +256,11 @@ class FusionClockNfcKiosk(http.Controller):
if not emp.exists(): if not emp.exists():
return {'error': 'employee_not_found'} return {'error': 'employee_not_found'}
emp.image_1920 = photo emp.image_1920 = photo
# Also push to the linked user's partner image, which is the image Odoo
# shows on the user's profile/preferences avatar (res.users delegates
# image_1920 to res.partner). Employees with no user are HR-only photos.
if emp.user_id and emp.user_id.partner_id:
emp.user_id.partner_id.sudo().write({'image_1920': photo})
return {'success': True} return {'success': True}
@http.route('/fusion_clock/kiosk/nfc/verify_pin', type='jsonrpc', auth='user', methods=['POST']) @http.route('/fusion_clock/kiosk/nfc/verify_pin', type='jsonrpc', auth='user', methods=['POST'])
@@ -311,7 +316,11 @@ class FusionClockNfcKiosk(http.Controller):
# freshly-saved profile photo never shows. write_date bumps on every # freshly-saved profile photo never shows. write_date bumps on every
# write (incl. saving image_1920), so it refreshes exactly when needed. # write (incl. saving image_1920), so it refreshes exactly when needed.
avatar_unique = employee.write_date.strftime('%Y%m%d%H%M%S') if employee.write_date else '' avatar_unique = employee.write_date.strftime('%Y%m%d%H%M%S') if employee.write_date else ''
avatar_url = f'/web/image/hr.employee/{employee.id}/avatar_128?unique={avatar_unique}' # PUBLIC model: the kiosk runs as a non-HR operator who can't read
# hr.employee images (ACL) — /web/image would serve a placeholder.
# hr.employee.public exposes the same avatar to any internal user
# (verified readable as the kiosk operator, uid 141).
avatar_url = f'/web/image/hr.employee.public/{employee.id}/avatar_128?unique={avatar_unique}'
now = fields.Datetime.now() now = fields.Datetime.now()
today = get_local_today(request.env, employee) today = get_local_today(request.env, employee)
day_plan = employee._get_fclk_day_plan(today) day_plan = employee._get_fclk_day_plan(today)

View File

@@ -684,8 +684,8 @@ html:has(#nfc_kiosk_root) {
.nfc-photo-stage { .nfc-photo-stage {
position: relative; position: relative;
aspect-ratio: 3 / 4; // portrait — width follows the (height-driven) box aspect-ratio: 3 / 4; // portrait — width follows the (height-driven) box
height: 56vh; height: 62vh;
max-height: 440px; max-height: 520px;
max-width: 100%; max-width: 100%;
margin: 0 auto; margin: 0 auto;
border-radius: 1rem; border-radius: 1rem;
@@ -706,7 +706,7 @@ html:has(#nfc_kiosk_root) {
position: absolute; position: absolute;
top: 47%; top: 47%;
left: 50%; left: 50%;
width: 54%; width: 64%;
aspect-ratio: 3 / 4; // VERTICAL oval — a face is taller than it is wide aspect-ratio: 3 / 4; // VERTICAL oval — a face is taller than it is wide
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
border: 3px dashed rgba(255, 255, 255, 0.92); border: 3px dashed rgba(255, 255, 255, 0.92);