# -*- coding: utf-8 -*- from odoo import http, fields, _ from odoo.http import request from odoo.tools.image import image_data_uri import base64 import json class HrExpenseKioskController(http.Controller): def _check_token(self, token): """ Verify the kiosk token and return the company. """ company = request.env['res.company'].sudo().search([('expense_kiosk_key', '=', token)], limit=1) if not company: return False return company @http.route('/hr_expense/kiosk/', type='http', auth='public', website=True) def hr_expense_kiosk_mode(self, token, **kwargs): """ Render the main Kiosk UI using the token. """ company = self._check_token(token) if not company: return request.not_found() return request.render('hr_expense_account_split.hr_expense_kiosk_mode_template', { 'session_info': request.env['ir.http'].get_frontend_session_info(), 'kiosk_token': token, 'company_name': company.name, 'json': json, }) @http.route('/hr_expense/kiosk_data/', type='jsonrpc', auth='public') def get_kiosk_data(self, token): """ Get all employees for selection. """ company = self._check_token(token) if not company: return [] employees = request.env['hr.employee'].sudo().search_read( domain=[('company_id', '=', company.id)], fields=['id', 'name', 'job_id', 'avatar_128'] ) # Convert avatar to data URI for emp in employees: emp['avatar_url'] = image_data_uri(emp['avatar_128']) if emp.get('avatar_128') else '/web/static/img/placeholder.png' emp['job_name'] = emp.get('job_id')[1] if emp.get('job_id') else '' # Fetch expense categories (products) products = request.env['product.product'].sudo().search_read( domain=[('can_be_expensed', '=', True), ('company_id', 'in', [company.id, False])], fields=['id', 'display_name', 'image_128'] ) for prod in products: prod['image_url'] = image_data_uri(prod['image_128']) if prod.get('image_128') else '/web/static/img/placeholder.png' return { 'employees': employees, 'categories': products, } @http.route('/hr_expense/kiosk_validate_pin/', type='jsonrpc', auth='public') def validate_pin(self, token, employee_id, pin): """ Validates the 4-digit PIN of the employee. """ if not self._check_token(token): return {'status': 'error', 'message': _("Invalid access token.")} employee = request.env['hr.employee'].sudo().browse(employee_id) if not employee.exists(): return {'status': 'error', 'message': _("Employee not found.")} if employee.pin == pin: return {'status': 'ok'} else: return {'status': 'error', 'message': _("Incorrect PIN.")} @http.route('/hr_expense/kiosk_get_pending/', type='jsonrpc', auth='public') def get_pending(self, token, employee_id): """ Returns pending realizations for the employee. """ if not self._check_token(token): return [] return request.env['hr.expense.realization'].sudo().get_pending_realizations(employee_id) @http.route('/hr_expense/kiosk_get_submitted/', type='jsonrpc', auth='public') def get_submitted(self, token, employee_id): """ Returns submitted expenses for the employee. """ if not self._check_token(token): return [] expenses = request.env['hr.expense'].sudo().search([ ('employee_id', '=', employee_id), ('state', 'not in', ['draft', 'refused']) ], order='date desc, id desc') result = [] state_selection = dict(request.env['hr.expense']._fields['state']._description_selection(request.env)) # Get payment state labels from account.move if possible payment_selection = dict(request.env['account.move']._fields['payment_state']._description_selection(request.env)) for exp in expenses: payment_state = exp.account_move_id.payment_state if exp.account_move_id else 'not_paid' result.append({ 'id': exp.id, 'name': exp.name, 'sequences': exp.sequence_name or '', 'date': exp.date.strftime('%Y-%m-%d') if exp.date else '', 'total_amount': exp.currency_id.symbol + " " + "{:,.2f}".format(exp.total_amount), 'state': state_selection.get(exp.state), 'state_raw': exp.state, 'payment_status': payment_selection.get(payment_state, _("Not Paid")), 'payment_state_raw': payment_state, }) return result @http.route('/hr_expense/kiosk_submit_realization/', type='jsonrpc', auth='public') def submit_realization(self, token, employee_id, expense_id, lines=None): """ Creates a realization report from the kiosk. """ if not self._check_token(token): return {'status': 'error', 'message': _("Invalid access token.")} try: expense = request.env['hr.expense'].sudo().browse(expense_id) if not expense or expense.employee_id.id != employee_id: return {'status': 'error', 'message': _("Invalid expense selection.")} if not lines: return {'status': 'error', 'message': _("No receipt lines provided.")} realization_vals = { 'expense_id': expense_id, 'description': _("Submitted via Kiosk"), 'line_ids': [] } for line in lines: image_base64 = line.get('image') line_vals = { 'description': line.get('description') or _("Receipt for ") + expense.name, 'amount': float(line.get('amount') or 0.0), 'attachment_id': image_base64.split(',')[-1] if image_base64 else False, 'attachment_name': 'receipt.jpg' if image_base64 else False, } realization_vals['line_ids'].append((0, 0, line_vals)) realization = request.env['hr.expense.realization'].sudo().create(realization_vals) realization.action_confirm() return {'status': 'ok', 'res_id': realization.id} except Exception as e: return {'status': 'error', 'message': str(e)} @http.route('/hr_expense/kiosk_submit_new_expense/', type='jsonrpc', auth='public') def submit_new_expense(self, token, employee_id, product_id, amount, description, payment_mode=None, image_base64=None): """ Creates a new expense (e.g. out of pocket) from the kiosk. """ if not self._check_token(token): return {'status': 'error', 'message': _("Invalid access token.")} try: employee = request.env['hr.employee'].sudo().browse(employee_id) # Since product_id could be generic, let's try to find a default expense product if not product_id: product = request.env['product.product'].sudo().search([('can_be_expensed', '=', True)], limit=1) product_id = product.id if product else False if not product_id: return {'status': 'error', 'message': _("No expense product found in the system.")} vals = { 'name': description or _("New Expense"), 'employee_id': employee_id, 'product_id': product_id, 'total_amount': float(amount) if amount else 0.0, 'date': fields.Date.today(), 'payment_mode': payment_mode or 'own_account', 'company_id': employee.company_id.id, 'currency_id': employee.company_id.currency_id.id, } expense = request.env['hr.expense'].sudo().create(vals) if image_base64: request.env['ir.attachment'].sudo().create({ 'name': 'receipt.jpg', 'res_model': 'hr.expense', 'res_id': expense.id, 'datas': image_base64.split(',')[-1], }) # Use sudo to allow the public user to trigger the workflow expense.sudo().action_submit() return {'status': 'ok', 'res_id': expense.id} except Exception as e: return {'status': 'error', 'message': str(e)}