[Odoo 16] Technical Documentation -Part 11-

Alhamdulillah akhirnya kita berada di penghujung tutorial ini. Serial technical documentation ini insya Allah akan saya akhiri pada pertemuan kali ini dengan pembahasan reporting pada Odoo.

Odoo menggunakan beberapa report engine untuk menghasilkan report default seperti QWeb Templates, Twitter Bootstrap dan Wkhtmltopdf. Ketiga engine tersebut dapat menghasilkan report dengan jenis PDF, HTML dan Text (Label, Barcode, dll).

Selain itu, Odoo juga menggunakan library python xlrd untuk membuat file excel. Python banyak menyediakan library/engine untuk membuat spreadsheet seperti openpyxl, xlrd, xlsxwriter, xlwt, xlutils, dll.

Banyak sekali referensi dokumentasi yang disediakan Odoo pada situs resmisnya, temen-temen bisa membacanya sendiri pada akhir tulisan ini, karna jika kami tuangkan dalam tulisan, mungkin bisa membuat serial tutorial tersendiri 😀

InsyaAllah pada kesempatan kali ini, kita akan membahas pembuatan 2 report yaitu PDF dan Excel. Kita akan menggunakan engine dari luar untuk pembuatan excelnya seperti yang telah tersedia di apps yaitu xlsxwriter. Silahkan di download dan diinstal modulnya. Dan pastikan sudah install library xlsxwriter dengan perintah :

pip3 install xlsxwriter

A. PDF

Reports ini dibuat dalam format HTML/Qweb yang menjadi standard web page Odoo sebagaimana website view Odoo. Kita bisa menggunakan Qweb template untuk membuat report. Sedangkan untuk mencetak format PDF, kita membutuhkan engine wkhtmltopdf (seperti namanya).

Selain template, kita juga bisa mengatur beberapa hal dalam report seperti custom fonts, format kertas, barcodes, dll. Pada Odoo, sebuah report merupakan kombinasi dari 2 hal yaitu :

1. Report Actions

Action sebagaimana yang umum pada Odoo dengan perbedaan beberapa parameter field dan modelnya. Jika action biasa maka modelnya adalah ir.actions.act_window maka untuk action report kita membutuhkan model ir.actions.report

<record id="account_invoices" model="ir.actions.report">
    <field name="name">Invoices</field>
    <field name="model">account.invoice</field>
    <field name="report_type">qweb-pdf</field>
    <field name="report_name">account.report_invoice</field>
    <field name="report_file">account.report_invoice</field>
    <field name="paperformat_id" ref="base.paperformat_us"/>
    <field name="attachment_use" eval="True"/>
    <field name="attachment">(object.state in ('open','paid')) and
        ('INV'+(object.number or '').replace('/','')+'.pdf')</field>
    <field name="binding_model_id" ref="model_account_invoice"/>
    <field name="binding_type">report</field>
</record>

Hampir sama seperti action wizard yang memiliki field binding_model_id dan binding_type yang berfungsi membuat item dibawah button report. Sedangkan field attachment berfungsi untuk memberikan nama file report ketika digenerate. Dan field attachment_use berfungsi untuk menyimpan report ke database ketika digenerate. Kita juga bisa mengatur format kertas sesuai kebutuhan dengan field paperformat_id.

<record id="paperformat_A4" model="report.paperformat">
    <field name="name">A4</field>
    <field name="default" eval="True" />
    <field name="format">A4</field>
    <field name="page_height">0</field>
    <field name="page_width">0</field>
    <field name="orientation">Portrait</field>
    <field name="margin_top">40</field>
    <field name="margin_bottom">32</field>
    <field name="margin_left">7</field>
    <field name="margin_right">7</field>
    <field name="header_line" eval="False" />
    <field name="header_spacing">35</field>
    <field name="dpi">90</field>
</record>

2. Reports Templates (QWeb)

Qweb adalah template engine utama yang digunakan Odoo dalam hal mencetak report. Qweb juga salah satu engine asli buatan Odoo selain OWL (Odoo Web Library). Disana juga ada XML template engine yang sering digunakan Odoo dalam hal membuat halaman dan bagian HTML.

Ada banyak element yang dimiliki Qweb, diantaranya :

1. Data Output

<p><t t-esc="value"/></p>

2. Conditionals

<div>
    <p t-if="user.birthday == today()">Salam !</p>
    <p t-elif="user.login == 'root'">Welcome master !</p>
    <p t-else="">Welcome!</p>
</div>

3. Loops

<t t-foreach="[1, 2, 3]" t-as="i">
    <p><t t-esc="i"/></p>
</t>

4. Attributes

<div t-att-a="42"/>

5. Setting Variables

<t t-set="foo" t-value="2 + 1"/>
<t t-esc="foo"/>

6. Calling Sub-Templates

<t t-call="other-template">
    <t t-set="var" t-value="1"/>
</t>

Standard minimal template untuk tampilan report seperti ini :

<template id="report_invoice">
    <t t-call="web.html_container">
        <t t-foreach="docs" t-as="o">
            <t t-call="web.external_layout">
                <div class="page">
                    <h2>Report title</h2>
                    <p>This object's name is <span t-field="o.name"/></p>
                </div>
            </t>
        </t>
    </t>
</template>

Element docs diatas sama seperti fungsi self di dalam method yang suka kita looping dan mengandung arti record pada model terkait. Sedangkan web.external_layout berfungsi untuk memberikan default header dan footer pada report kita.

Karna report adalah standard halaman web Odoo (HTML), maka report bisa juga kita akses melalui URL seperti contoh jika kita sedang login di suatu database yang memiliki data di sale order dengan link ini :

http://localhost:8069/report/html/sale.report_saleorder/1

Dan bisa juga kita akses dalam format PDF nya dengan link :

http://localhost:8069/report/pdf/sale.report_saleorder/1

Karna sumber utama report berasal dari HTML (web), maka jika PDF yang dihasilkan berbeda style/layout nya (bahkan hilang), kemungkinan berasal dari kesalahan wkhtmltopdf nya yang tidak bisa melakukan koneksi dengan web server Odoo kita. Solusinya dengan mensetting report.url dan web.base.url.freeze seperti disini

Selanjutnya kita akan membuat report Qweb pada model Training Session. Ikuti langkah-langkah berikut ini :

A. Buat button baru di view form Session seperti berikut :


        <!-- Membuat View Form Session -->

        <record id="training_session_view_form" model="ir.ui.view">
            <field name="name">training.session.form</field>
            <field name="model">training.session</field>
            <field name="arch" type="xml">
                <form string="Session Form">
                    <header>
                        <button name="action_print_session" type="object" string="Print Session" states="open" class="oe_highlight"/>
                        <button name="action_confirm" type="object" string="Confirm" states="draft" groups="training_odoo.group_training_manager" class="btn-primary"/>
                        <button name="action_cancel" type="object" string="Cancel" states="open"/>
                        <button name="action_close" type="object" string="Close" groups="training_odoo.group_training_manager" states="open" class="oe_highlight"/>
                        <field name="state" widget="statusbar" statusbar_visible="draft,open,done"/>
                    </header>
                    <sheet>
                        <group>
                            <group string="Informasi">
                                <field name="course_id" options="{'no_create': True, 'no_edit': True, 'no_open': True}"/>
                                <field name="name" placeholder="Contoh: Introduction"/>
                                <field name="partner_id"/>
                            </group>
                            <group string="Jadwal">
                                <field name="start_date" string="Tanggal Mulai"/>
                                <field name="end_date"/>
                                <field name="duration"/>
                            </group>
                        </group>
                        <group string="">
                            <group>
                                <field name="seats"/>
                                <field name="taken_seats" widget="progressbar"/>
                            </group>
                            <group>
                                <field name="attendees_count"/>
                            </group>
                        </group>
                        <group string="Peserta">
                            <field name="attendee_ids" nolabel="1" colspan="2"/>
                        </group>
                    </sheet>
                </form>
            </field>
        </record>

B. Buat method dari button report diatas (karna typenya object) dengan nama yang sama dari attribute name :

class TrainingSession(models.Model):
    _name = 'training.session'
    
    ...
    ...
    
    def action_print_session(self):
        return self.env.ref('training_odoo.report_training_session_action').report_action(self)   

C. Buat folder baru bernama report di dalam modul training_odoo

D. Buat file xml baru dengan nama report_action.xml didalam folder report yang isinya adalah script action dengan id xml nya sesuai point B dan isinya seperti berikut ini :

<odoo>


    <!-- Membuat Action/Event Report Session -->

    <record id="report_training_session_action" model="ir.actions.report">
        <field name="name">Training Session (PDF)</field>
        <field name="model">training.session</field>
        <field name="report_type">qweb-html</field>
        <field name="report_name">training_odoo.report_session</field>
        <field name="report_file">training_odoo.report_training_session</field>
        <field name="print_report_name">'Session - %s' % (object.name)</field>
        <field name="binding_model_id" ref="model_training_session"/>
        <field name="binding_view_types">form</field>
    </record>


</odoo>   

Nanti setelah kita daftarkan file xml baru kita di __manifest__.py, maka kita bisa mengecek hasil script diatas pada menu Settings > Technical > (Actions) > Reports

E. Buat file template report Qweb didalam folder report dengan nama report_training_session.xml yang isinya :

<odoo>


    <!-- Report Template Header - Session -->

    <template id="header_session">
        <t t-if="o and 'company_id' in o">
            <t t-set="company" t-value="o.company_id"></t>
        </t>
        <t t-if="not o or not 'company_id' in o">
            <t t-set="company" t-value="res_company"></t>
        </t>
        <div class="header">
            <div class="row mt32 mb32">
                <div class="col-6">
                    <img t-if="company.logo" t-att-src="image_data_uri(company.logo)" style="max-height: 100px;" />
                </div>
                <div class="col-6 text-right" style="font:15px lucida-console,sans-serif !important;background-color:#960380;padding:10px">
                    <span t-field="company.partner_id" style="font-size: 20px;color:white;font-weight: bold;"/>
                    <br/>
                    <span t-field="company.partner_id.street" style="color:white;font-weight: bold;"/>
                    <br/>
                    <span t-field="company.partner_id.city" style="color:white;font-weight: bold;"/>
                    <br/>
                    <span t-field="company.partner_id.country_id" style="color:white;font-weight: bold;"/>
                    <br/>
                    <span t-field="company.partner_id.vat" style="color:white;"/>
                    <br/>
                </div>
            </div>
        </div>
    </template>


    <!-- Report Template Body - Session -->

    <template id="body_session">
        <div class="page">
            <div class="d-flex justify-content-center">
                <table class="table table-bordered" style="width:50%">
                    <tbody>
                        <tr>
                            <td style="width:25%; background-color:#960380; padding:5px;color:white;font-weight: bold;">Course</td>
                            <td style="width:75%;padding:5px;">
                                <t t-esc="o.course_id.name"/>
                            </td>
                        </tr>
                        <tr>
                            <td style="width:25%; background-color:#960380; padding:5px;color:white;font-weight: bold;">Session</td>
                            <td style="width:75%;padding:5px;">
                                <t t-esc="o.name"/>
                            </td>
                        </tr>
                        <tr>
                            <td style="width:25%; background-color:#960380; padding:5px;color:white;font-weight: bold;">Instructor</td>
                            <td style="width:75%;padding:5px;">
                                <t t-esc="o.partner_id.name"/>
                            </td>
                        </tr>
                        <tr>
                            <td style="width:25%; background-color:#960380; padding:5px;color:white;font-weight: bold;">Seats</td>
                            <td style="width:75%;padding:5px;">
                                <t t-esc="o.seats"/>
                            </td>
                        </tr>
                        <tr>
                            <td style="width:25%; background-color:#960380; padding:5px;color:white;font-weight: bold;">Taken Seats</td>
                            <td style="width:75%;padding:5px;">
                                <t t-esc="o.taken_seats"/>%
                            </td>
                        </tr>
                    </tbody>
                </table>
                <table class="table table-bordered" style="width:50%">
                    <tbody>
                        <tr>
                            <td style="width:25%; background-color:#960380; padding:5px;color:white;font-weight: bold;">Start Date</td>
                            <td style="width:75%;padding:5px;">
                                <t t-if="o.start_date">
                                    <t t-esc="o.start_date.strftime('%d/%m/%Y')"/>
                                </t>
                            </td>
                        </tr>
                        <tr>
                            <td style="width:25%; background-color:#960380; padding:5px;color:white;font-weight: bold;">End Date</td>
                            <td style="width:75%;padding:5px;">
                                <t t-if="o.end_date">
                                    <t t-esc="o.end_date.strftime('%d/%m/%Y')"/>
                                </t>
                            </td>
                        </tr>
                        <tr>
                            <td style="width:25%; background-color:#960380; padding:5px;color:white;font-weight: bold;">Duration</td>
                            <td style="width:75%;padding:5px;">
                                <t t-esc="o.duration"/>
                            </td>
                        </tr>
                        <tr>
                            <td style="width:25%; background-color:#960380; padding:5px;color:white;font-weight: bold;">Attendees</td>
                            <td style="width:75%;padding:5px;">
                                <t t-esc="o.attendees_count"/>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
            <table class="table table-bordered mt-4">
                <thead style="background-color:#960380;color:white;font-weight: bold;font-weight: bold;">
                    <tr>
                        <td class="text-center">Name</td>
                        <td class="text-center">Email</td>
                        <td class="text-center">Kelamin</td>
                    </tr>
                </thead>
                <tbody>
                    <t t-foreach="o.attendee_ids" t-as="attendee">
                        <t t-set="gender" t-value="dict([('male','Pria'),('female','Wanita'), (False, '')])"/>
                        <tr>
                            <td>
                                <t t-esc="attendee.name"/>
                            </td>
                            <td>
                                <t t-esc="attendee.email"/>
                            </td>
                            <td>
                                <t t-esc="gender[attendee.sex]"/>
                            </td>
                        </tr>
                    </t>
                </tbody>
            </table>
        </div>
    </template>


    <!-- Report Template - Session -->

    <template id="report_session">
        <t t-call="web.html_container">
            <t t-foreach="docs" t-as="o">
                <div t-if="report_type == 'html'">
                    <div>
                        <t t-call="training_odoo.header_session"/>
                    </div>
                    <div style="margin-top:150px">
                        <t t-call="training_odoo.body_session"/>
                    </div>
                </div>
                <div t-else="">
                    <t t-call="training_odoo.header_session"/>
                    <t t-call="web.external_layout">
                        <t t-call="training_odoo.body_session"/>
                    </t>
                </div>
            </t>
        </t>
    </template>


</odoo>

F. Update file __manifest__.py (Tambahkan 2 file xml yang baru di folder report):

# always loaded
'data': [
    'report/report_training_session.xml',
    'report/report_action.xml',
    'security/security.xml',
    'security/ir.model.access.csv',
    'views/sequence_data.xml',
    'views/scheduler_data.xml',
    'views/views.xml',
    'views/partner_views.xml',
    'wizard/training_wizard_views.xml',
    'views/menuitem_views.xml',
    'views/templates.xml',
],

Lakukan restart dan upgrade modul lalu masuk ke view form session yang berstatus ‘Open’ dan klik tombol ‘Print Session’ sehingga menghasilkan report seperti berikut :

Jika kita ingin saat klik ‘Print Session’ langsung generate PDF tanpa preview di HTML dulu, maka kita bisa mengganti script action pada field atau parameter report_type seperti ini :

Sebelumnya :

<field name="report_type">qweb-html</field>

Sesudahnya :

<field name="report_type">qweb-pdf</field>

B. EXCEL

XlsxWriter adalah sebuah modul/library/engine python yang berguna untuk membuat sebuah spreadsheet seperti excel yang berisi text, angka, image, hyperlinks, formatting, dll. XlsxWriter juga memiliki banyak fungsi dan mendukung beberapa hal seperti compatible dengan microsoft excel, integration dengan pandas, menampung file yang besar, data validasi, dll.

Sama seperti report Qweb yang membutuhkan Report Action dan Report Template. Begitu juga dengan report excel yang menggunakan engine xlsxwriter ini juga membutuhkan kedua hal tersebut. Bedanya jika Report Template Qweb menggunakan syntax gabungan antara HTML & XML. Sedangkan xlsxwriter murni menggunakan full python code. Contoh sederhananya seperti ini :

from odoo import models

class PartnerXlsx(models.AbstractModel):
    _name = 'report.module_name.report_name'
    _inherit = 'report.report_xlsx.abstract'
    def generate_xlsx_report(self, workbook, data, partners):
        for obj in partners:
            report_name = obj.name
            # One sheet by partner
            sheet = workbook.add_worksheet(report_name[:31])
            bold = workbook.add_format({'bold': True})
            sheet.write(0, 0, obj.name, bold)

Sekarang kita akan membuat report excel pada model Training Course. Silahkan ikut langkah-langkah seperti berikut :

A. Buat button baru di view form Course seperti berikut :


        <!-- Membuat View Form Course -->

        <record id="training_course_view_form" model="ir.ui.view">
            <field name="name">training.course.form</field>
            <field name="model">training.course</field>
            <field name="arch" type="xml">
                <form string="Course Form">
                    <header>
                        <button name="action_print_course" type="object" string="Print Course" class="oe_highlight"/>
                    </header>
                    <sheet>
                        <div class="oe_title">
                            <h1>
                                <field name="ref"/>
                            </h1>
                        </div>
                        <group string="Informasi">
                            <group>
                                <field name="name"/>
                                <field name="level" widget="radio"/>
                            </group>
                            <group>
                                <field name="color"/>
                            </group>
                        </group>
                        <group>
                            <separator string="Koordinator"/>
                            <field name="user_id"/>
                            <separator string="Login"/>
                            <field name="email" password="1"/>
                        </group>
                        <notebook>
                            <page string="Sesi">
                                <group>
                                    <field name="session_line" mode="tree,kanban" nolabel="1" colspan="2">
                                        <tree string="Sesi" editable="top">
                                            <field name="name"/>
                                            <field name="partner_id"/>
                                            <field name="start_date"/>
                                            <field name="duration"/>
                                            <field name="seats"/>
                                            <field name="state"/>
                                        </tree>
                                        <form string='Sesi'>
                                            <group>
                                                <field name="name"/>
                                                <field name="partner_id"/>
                                                <field name="start_date"/>
                                                <field name="duration"/>
                                                <field name="seats"/>
                                            </group>
                                        </form>
                                    </field>
                                </group>
                            </page>
                            <page string="Cendera Mata">
                                <group>
                                    <field name="product_ids" nolabel="1" colspan="2"/>
                                </group>
                            </page>
                            <page string="Keterangan">
                                <group>
                                    <field name="description"/>
                                </group>
                            </page>
                        </notebook>
                    </sheet>
                    <div class="oe_chatter">
                        <field name="message_follower_ids"/>
                        <field name="activity_ids"/>
                        <field name="message_ids"/>
                    </div>
                </form>
            </field>
        </record>

B. Buat method dari button report diatas (karna typenya object) dengan nama yang sama dari attribute name :

class TrainingCourse(models.Model):
    _name = 'training.course'
    
    ...
    ...
    
    def action_print_course(self):
        return self.env.ref('training_odoo.report_training_course_action').report_action(self)  

C. Tambahkan script action pada file report_action.xml yang id xml nya sesuai point B dan isinya seperti ini (paste di paling bawah) :

    <!-- Membuat Action/Event Report Course -->

    <record id="report_training_course_action" model="ir.actions.report">
        <field name="name">Training Course (XLSX)</field>
        <field name="model">training.course</field>
        <field name="report_type">xlsx</field>
        <field name="report_name">training_odoo.report_course</field>
        <field name="report_file">training_odoo.report_training_course</field>
        <field name="print_report_name">'Course - %s' % (object.name)</field>
        <field name="binding_model_id" ref="model_training_course"/>
        <field name="binding_view_types">form</field>
    </record>

D. Lalu buat file __init__.py di dalam folder report dengan isi :

from . import report_training_course

E. Buat file python baru dengan nama report_training_course.py sesuai file yang kita import pada point D yang isinya :

from odoo import api, fields, models

class CourseXlsx(models.AbstractModel):
    _name = 'report.training_odoo.report_course'
    _inherit = 'report.report_xlsx.abstract'

    def generate_xlsx_report(self, workbook, data, obj):
        sheet = workbook.add_worksheet('Course %s' % obj.name)
        text_top_style = workbook.add_format({'font_size': 12, 'bold': True ,'font_color' : 'white', 'bg_color': '#b904bf', 'valign': 'vcenter', 'text_wrap': True})
        text_header_style = workbook.add_format({'font_size': 12, 'bold': True ,'font_color' : 'white', 'bg_color': '#b904bf', 'valign': 'vcenter', 'text_wrap': True, 'align': 'center'})
        text_style = workbook.add_format({'font_size': 12, 'valign': 'vcenter', 'text_wrap': True, 'align': 'center'})
        number_style = workbook.add_format({'num_format': '#,##0', 'font_size': 12, 'align': 'right', 'valign': 'vcenter', 'text_wrap': True})

        sheet.merge_range(0, 0, 0, 1, "Reference", text_top_style)
        sheet.write(0, 2, obj.ref)
        sheet.merge_range(1, 0, 1, 1, "Course Title", text_top_style)
        sheet.write(1, 2, obj.name)
        sheet.merge_range(2, 0, 2, 1, "Level", text_top_style)
        sheet.write(2, 2, obj.level.capitalize() if obj.level else '')
        sheet.merge_range(3, 0, 3, 1, "Responsible", text_top_style)
        sheet.write(3, 2, obj.user_id.name)

        row = 5
        sheet.freeze_panes(6, 10)
        sheet.set_column(0, 0, 5)
        sheet.set_column(1, 9, 15)
        header = ['No', 'Session', 'Instructor', 'Start Date', 'End Date','Duration', 'Seats', 'Attendees','Taken Seats(%)','Status']
        sheet.write_row(row, 0, header, text_header_style)
        
        no_list = []
        session = []
        partner = []
        start_date = []
        end_date = []
        duration = []
        seats = []
        attendees = []
        taken_seats = []
        status = []

        no = 1 
        for x in obj.session_line:
            no_list.append(no)
            session.append(x.name or '')
            partner.append(x.partner_id.name if x.partner_id and x.partner_id.name else '')
            start_date.append(x.start_date.strftime('%d-%m-%Y') if x.start_date else '')
            end_date.append(x.end_date.strftime('%d-%m-%Y') if x.end_date else '')
            duration.append(x.duration or '')
            seats.append(x.seats or '')
            attendees.append(x.attendees_count)
            taken_seats.append(x.taken_seats)
            status.append(x.state.capitalize())
            no+=1

        row += 1
        sheet.write_column(row, 0, no_list, text_style)
        sheet.write_column(row, 1, session, text_style)
        sheet.write_column(row, 2, partner, text_style)
        sheet.write_column(row, 3, start_date, text_style)
        sheet.write_column(row, 4, end_date, text_style)
        sheet.write_column(row, 5, duration, text_style)
        sheet.write_column(row, 6, seats, number_style)
        sheet.write_column(row, 7, attendees, number_style)
        sheet.write_column(row, 8, taken_seats, number_style)
        sheet.write_column(row, 9, status, text_style)

F. Update file __init__.py di folder training_odoo (yang sejajar dengan file __manifest__.py):

from . import controllers
from . import report
from . import models
from . import wizard

G. Update file __manifest__.py (menambahkan depends modul report_xlsx):

# any module necessary for this one to work correctly
'depends': ['base', 'product', 'mail', 'report_xlsx'],

Lakukan restart dan upgrade modul lalu masuk ke view form course dan klik tombol ‘Print Course sehingga menghasilkan report seperti berikut :

Alhamdulillahilladzi bini’matihi tatimmush shalihat, akhirnya kita telah menyelesaikan serial tutorial technical ini. Bagi yang mengalami kendala, bisa mendownload hasil tulisan ini disini.

Penulis berharap, semoga amal ibadah ini diterima oleh Allah azza wa jalla. Lalu ilmu yang telah disampaikan dapat bermanfaat untuk semuanya. Aamiin.

Yang benar datangnya dari Allah Ta’ala, sedangkan kesalahan dari saya pribadi. Saran dan kritik yang membangun sangat saya harapkan.

Semoga bermanfaat. Wassalamu’alaikum …

Sumber :

https://xlsxwriter.readthedocs.io/
https://www.odoo.com/documentation/16.0/developer/tutorials/getting_started/15_qwebintro.html
https://www.odoo.com/documentation/16.0/developer/tutorials/backend.html#reporting
https://www.odoo.com/documentation/16.0/developer/reference/backend/reports.html
https://www.odoo.com/documentation/16.0/developer/reference/frontend/qweb.html
https://www.odoo.com/documentation/16.0/developer/tutorials/pdf_reports.html

Leave a comment