192 lines
8.6 KiB
Python
192 lines
8.6 KiB
Python
# -*- 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/<string:token>', 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/<string:token>', 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/<string:token>', 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/<string:token>', 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/<string:token>', 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/<string:token>', 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/<string:token>', 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)}
|