Ir al contenido

¿CÓMO GENERAR INFORMES XLSX USANDO UN CONTROLADOR EN ODOO 19?

¿CÓMO GENERAR INFORMES XLSX USANDO UN CONTROLADOR EN ODOO 19?

Generar informes de Excel es un requisito común en las aplicaciones empresariales. Odoo 19 proporciona herramientas potentes para crear informes XLSX dinámicos usando controladores. Con los controladores, puedes manejar solicitudes directamente, generar informes y devolverlos al usuario para su descarga. Esta guía lo guía a través del proceso de creación de un informe XLSX completamente funcional en Odoo 19, utilizando un ejemplo del mundo real de Informes de asistencia de empleados.

Paso 1: Configuración del controlador

Los controladores en Odoo manejan solicitudes HTTP. Para los informes XLSX, crearemos un controlador que recibe parámetros del frontend, genera un archivo Excel y lo devuelve como una respuesta descargable.

Crea un nuevo archivo en tu módulo: controllers/xlsx_report_controller.py

Python

import json
from odoo import http
from odoo.http import content_disposition, request, serialize_exception
from odoo.tools import html_escape

class XLSXReportController(http.Controller):
    """Xlsx Report Controller"""

    @http.route('/xlsx_reports', type='http', auth='user', methods=['POST'], csrf=False)
    def get_report_xlsx(self, model, options, output_format, report_name):
        """xlsx report"""
        report_obj = request.env[model].with_user(request.session.uid)
        options = json.loads(options)
        token = 'dummy-because-api-expects-one'
        try:
            if output_format == 'xlsx':
                response = request.make_response(
                    None,
                    headers=[
                        ('Content-Type', 'application/vnd.ms-excel'),
                        ('Content-Disposition', content_disposition(report_name + '.xlsx'))
                    ]
                )
                report_obj.get_xlsx_report(options, response)
            response.set_cookie('fileToken', token)
            return response
        except Exception as e:
            se = serialize_exception(e)
            error = {
                'code': 200,
                'message': 'Odoo Server Error',
                'data': se
            }
            return request.make_response(html_escape(json.dumps(error)))
  • csrf=False: Es importante para permitir descargas activadas por JS en ciertos contextos, aunque en entornos modernos de Odoo se recomienda manejar el token CSRF si es posible.
  • content_disposition: Garantiza que el archivo se descargue en lugar de mostrarse en el navegador.
  • Todo el trabajo pesado de la creación de Excel se delega al método del modelo (get_xlsx_report).

Paso 2: Creación de la interfaz frontend

Para permitir que los usuarios activen informes, crearemos un asistente (wizard) con un formulario sencillo. Este asistente permite a los usuarios seleccionar filtros (como empleado y rango de fechas) y hacer clic en “Imprimir” para generar el informe XLSX.

Crea un archivo de vista: views/report_wizard.xml

XML

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <record id="employee_attendance_report_view_form" model="ir.ui.view">
        <field name="name">employee.attendance.report.view.form</field>
        <field name="model">employee.attendance.report</field>
        <field name="arch" type="xml">
            <form string="Informe de Asistencia">
                <group>
                    <group>
                        <field name="from_date"/>
                        <field name="to_date"/>
                    </group>
                    <group>
                        <field name="employee_ids" widget="many2many_tags"/>
                    </group>
                </group>
                <footer>
                    <button name="action_print_xlsx" string="Print" type="object" class="btn-primary"/>
                    <button string="Cancel" class="btn-secondary" special="cancel"/>
                </footer>
            </form>
        </field>
    </record>

    <record id="action_employee_attendance_report" model="ir.actions.act_window">
        <field name="name">Attendance Report</field>
        <field name="type">ir.actions.act_window</field>
        <field name="res_model">employee.attendance.report</field>
        <field name="view_mode">form</field>
        <field name="view_id" ref="employee_attendance_report_view_form"/>
        <field name="target">new</field>
    </record>

    <menuitem id="employee_attendance_report_menu"
              name="Reports"
              parent="hr_attendance.menu_hr_attendance_root"/>

    <menuitem id="employee_attendance_report_menu_action"
              name="Attendance Report"
              parent="employee_attendance_report_menu"
              action="action_employee_attendance_report"/>
</odoo>

Paso 3: Creación del modelo del asistente

El modelo de asistente maneja la entrada del usuario, valida datos y delega la generación de informes.

Crear models/xlsx_report_wizard.py:

Python

import io
import json
from datetime import datetime, date
from dateutil.rrule import rrule, DAILY
from odoo import fields, models, _
from odoo.exceptions import ValidationError
from odoo.tools import date_utils, json_default

try:
    from odoo.tools.misc import xlsxwriter
except ImportError:
    import xlsxwriter

class EmployeeAttendanceReport(models.TransientModel):
    """ Wizard for Employee Attendance Report """
    _name = 'employee.attendance.report'
    _description = 'Employee Attendance Report Wizard'

    from_date = fields.Date('From Date', help="Start date of the report")
    to_date = fields.Date('To Date', help="End date of the report")
    employee_ids = fields.Many2many('hr.employee', string='Employee', help='Employee Name')

    def action_print_xlsx(self):
        """
        Return the report action for XLSX Attendance Report
        Raises: ValidationError: if from date > to date
        Raises: ValidationError: if no attendance records
        Returns: dict: the XLSX report action
        """
        if not self.from_date:
            self.from_date = date.today()
        if not self.to_date:
            self.to_date = date.today()
        
        if self.from_date > self.to_date:
            raise ValidationError(_('Start Date must be less than End Date.'))

        attendances = self.env['hr.attendance'].search([
            ('employee_id', 'in', self.employee_ids.ids)
        ])
        
        data = {
            'from_date': self.from_date,
            'to_date': self.to_date,
            'employee_ids': self.employee_ids.ids
        }

        if self.employee_ids and not attendances:
            raise ValidationError(_("No Employee Attendance Records Found"))

        if self.from_date and self.to_date:
            return {
                'type': 'ir.actions.report',
                'data': {
                    'model': 'employee.attendance.report',
                    'options': json.dumps(data, default=json_default),
                    'output_format': 'xlsx',
                    'report_name': 'Attendance Report',
                },
                'report_type': 'xlsx',
            }

    def get_xlsx_report(self, data, response):
        """
        Print the XLSX Report
        Returns: None
        """
        query = """
            SELECT hr_e.name, date(hr_at.check_in), SUM(hr_at.worked_hours) 
            FROM hr_attendance hr_at 
            LEFT JOIN hr_employee hr_e ON hr_at.employee_id = hr_e.id
        """
        
        if not data['employee_ids']:
            query += """ GROUP BY date(check_in), hr_e.name"""
        else:
            query += """ WHERE hr_e.id in (%s) GROUP BY date(check_in), hr_e.name""" % (
                ', '.join(str(employee_id) for employee_id in data['employee_ids'])
            )

        self.env.cr.execute(query)
        docs = self.env.cr.dictfetchall()

        output = io.BytesIO()
        workbook = xlsxwriter.Workbook(output, {'in_memory': True})
        sheet = workbook.add_worksheet('docs')

        start_date = datetime.strptime(str(data['from_date']), '%Y-%m-%d').date()
        end_date = datetime.strptime(str(data['to_date']), '%Y-%m-%d').date()
        date_range = rrule(DAILY, dtstart=start_date, until=end_date)

        sheet.set_column(1, 1, 15)
        sheet.set_column(2, 2, 15)

        border = workbook.add_format({'border': 1})
        green = workbook.add_format({'bg_color': '#28A828', 'border': 1})
        red = workbook.add_format({'bg_color': '#ff3333', 'border': 1})
        rose = workbook.add_format({'bg_color': '#DA70D6', 'border': 1})
        
        head = workbook.add_format({'bold': True, 'font_size': 30, 'align': 'center'})
        date_size = workbook.add_format({'font_size': 12, 'bold': True, 'align': 'center'})

        sheet.merge_range('C3:K6', 'Attendance Report', head)
        sheet.merge_range('B8:C9', 'From Date: ' + str(data['from_date']), date_size)
        sheet.merge_range('B10:C11', 'To Date: ' + str(data['to_date']), date_size)

        sheet.write(2, 12, '', green)
        sheet.write(2, 13, 'Present')
        sheet.write(4, 12, '', red)
        sheet.write(4, 13, 'Absent')
        sheet.write(6, 12, '', rose)
        sheet.write(6, 13, 'Half Day')

        sheet.merge_range('B16:B17', 'Sl.No', border)
        sheet.merge_range('C16:C17', 'Employee', border)

        row = 15
        col = 2
        for date_data in date_range:
            col += 1
            sheet.write(row, col, date_data.strftime('%Y-%m-%d'), border)

        row = 16
        col = 2
        for date_data in date_range:
            col += 1
            sheet.write(row, col, date_data.strftime('%a'), border)

        employee_names = []
        attendance_list = []

        for doc in docs:
            if doc['name'] not in employee_names:
                date_sum_list = []
                employee_names.append(doc['name'])
                
                for date_data in date_range:
                    date_out = date_data.strftime('%Y-%m-%d')
                    record_list = list(filter(lambda x: x['name'] == doc['name'] and str(x['date']) == date_out, docs))
                    
                    if record_list:
                        date_sum_list.append(record_list[0])
                    else:
                        date_sum_list.append({'name': '', 'date': '', 'sum': 0})
                
                attendance_list.append({'name': doc['name'], 'items': date_sum_list})

        work = self.env.ref('resource.resource_calendar_std')
        row = 17
        i = 0

        for rec in attendance_list:
            row += 1
            col = 1
            i += 1
            sheet.write(row, col, i, border)
            col += 1
            sheet.write(row, col, rec['name'], border)

            for item in rec['items']:
                col += 1
                if item['sum'] >= work.hours_per_day:
                    sheet.write(row, col, item['sum'], green)
                elif 1 <= item['sum'] <= 4 or 4 <= item['sum'] <= work.hours_per_day:
                    sheet.write(row, col, item['sum'], rose)
                else:
                    sheet.write(row, col, item['sum'], red)

        workbook.close()
        output.seek(0)
        response.stream.write(output.read())
        output.close()

Paso 4: Acceso de seguridad

Agregar derechos de acceso en security/ir.model.access.csv:

Fragmento de código

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_employee_attendance_report,employee.attendance.report.access,model_employee_attendance_report,base.group_user,1,1,1,1

Paso 5: Integración de Frontend JS

Para gestionar las descargas de informes XLSX, Odoo utiliza controladores de acciones en el lado del cliente (JavaScript). Agregue este código en static/src/js/action_manager.js:

JavaScript

/** @odoo-module **/

import { registry } from "@web/core/registry";
import { BlockUI } from "@web/core/ui/block_ui";
import { download } from "@web/core/network/download";

/**
 * XLSX Handler
 * This handler is responsible for generating XLSX reports.
 * It sends a request to the server to generate the report in XLSX format
 * and downloads the generated file.
 */

registry.category("ir.actions.report handlers").add("xlsx", async function (action) {
    if (action.report_type === 'xlsx') {
        BlockUI;
        await download({
            url: '/xlsx_reports',
            data: action.data,
            complete: () => unblockUI,
            error: (error) => self.call('crash_manager', 'rpc_error', error),
        });
    }
});

Conclusión

Siguiendo estos pasos, puedes crear informes XLSX totalmente dinámicos en Odoo 19:

  1. El controlador maneja las solicitudes HTTP y sirve el archivo Excel.
  2. El asistente (Wizard) recopila información y filtros del usuario.
  3. El modelo obtiene los datos SQL, los formatea y genera el libro de Excel usando xlsxwriter.
  4. El controlador JS activa la descarga sin problemas en el frontend.