Cara Menggunakan Module safe_eval Bawaan Odoo

Beberapa waktu yang lalu, saya telah menulis function/module bawaan odoo untuk mengolah tipe data float. Kali ini saya akan membahas module bawaan odoo yang lain, yaitu safe_eval.

Apasih kegunaan module safe_eval ini ?

Jika kita baca source code odoo, sebenarnya safe_eval adalah implementasi dari function eval bawaan python. Tentu saja dengan beberapa modifikasi.

Jika Anda belum tahu, module eval digunakan untuk mengolah ekspresi yang bertipe data string agar bisa dibaca sebagai operasi matematika atau bahkan kode python. Sebagai contoh, katakanlah kita memiliki sebuah variabel bertipe data string seperti pada kode di bawah ini.

my_string = '100 + 5'

Dengan memanfaatkan module eval kita bisa mengolah variabel my_string yang bertipe data string di atas, agar diolah sebagai operasi matematika yang valid. Silakan Anda coba kode di bawah ini.

my_string = '100 + 5'

value = eval(my_string)
print(value)

Variabel value harusnya sekarang bernilai 105 hasil dari operasi 100 + 5.

Module eval juga bisa mengolah ekspresi yang tidak memiliki nilai secara explisit. Seperti pada kode di bawah ini.

a = 100
b = 5
my_string = 'a + b'

value = eval(my_string)

Secara otomatis eval akan mencari variabel dengan nama a dan b di global scope, kemudian dia akan mengolah ekspresi yang di masukkan oleh user. Jika kita tidak mau menulis variable di global scope, kita juga bisa memasukkan daftar nama variabel dalam sebuah dictionary, seperti pada kode di bawah ini.

my_string = 'a + b'

value = eval(my_string, {'a': 100, 'b': 5})

Seperti yang saya tulis di awal-awal, module safe_eval sebenarnya adalah implementasi dari module eval bawaan python, dengan beberapa modifikasi. Oleh karena itu kode di atas juga bisa diulis menggunakan module safe_eval, seperti pada kode di bawah ini.

from odoo.tools.safe_eval import safe_eval

my_string = 'a + b'

value = safe_eval(my_string, {'a': 100, 'b': 5})

Lalu apa yang membedakan module eval bawaan python dengan module safe_eval milik odoo ?

Module safe_eval memblacklist beberapa ekspresi yang kalau di eksekusi dengan eval tidak menyebabkan masalah. Silakan lihat daftar ekspresi yang di blacklist oleh safe_eval di source code odoo ini. Ekpresi pertama yang diblacklist oleh safe_eval adalah ekspresi import. Misal kita punya ekspresi seperti ini.

my_string = "__import__('odoo').tools.float_round(a/b,pricision)"

Ekspresi di atas, jika dilewatkan pada module eval tidak akan menyebabkan error, dan akan return nilai yang benar. Silakan coba kode di bawah ini.

eval_value = eval(my_string, {'a': 15, 'b': 2, 'pricision': 0})

Tetapi jika digunakan pada module safe_eval

safe_eval_value = safe_eval(my_string, {'a': 15, 'b': 2, 'pricision': 0})

kode di atas akan menyebabkan error seperti pada gambar di bawah ini.

Pesan error saat menggunakan safe_eval odoo

Selain mencegah ekspresi yang menggunakan kode import, jika dilihat dari source code odoo, sebenarnya safe_eval juga mencegah ekspresi yang lain. Tetapi saya belum menemukan contoh kodenya, jadi saya tidak bisa membahasnya saat ini. Jika suatu saat saya menemukan contoh kodenya, saya akan mengupdate artikel ini.

Selain itu kita juga bisa mengganti mode dari safe_eval. Secara default saat kita memanggil safe_eval odoo akan mengeksekusi module eval bawaan python. Tetapi kita bisa mengganti eval dengan module yang lain, misal exec yang juga bawaan python. Perhatikan kode di bawah ini.

my_string = "c = a + b"
my_value = {'c': 1}
print('my_value==before==', my_value)
# my_value==before== {'c': 1}

safe_eval(my_string, {'a': 4, 'b': 7}, my_value, mode="exec", nocopy=True)

print('my_value==after==', my_value)
# my_value==after== {'c': 11}

Jika Anda belum tahu, jika kita melewatkan ekspresi c = a + b kedalam module eval, ekspresi itu akan mentrigger error, karena c = a + b bukanlah sebuah ekspresi yang valid bagi eval, kalau dari forum-forum sih c = a + b bukanlah sebuah ekspresi, tetapi sebuah statement. Tetapi jika statement c = a + b itu dilewatkan pada module exec, dia akan diolah tanpa menyebabkan error. Tetapi module exec tidak return value, oleh karena itu kita perlu memasukkan argument ketiga pada safe_eval, yaitu argument locals_dict dimana pada contoh di atas nilainya adalah variabel my_value, untuk menampung nilai hasil perhitungan. Perlu anda ingat saat kita memanggil module safe_eval dengan mode exec kita harus mengatur nilai dari argument nocopy agar bernilai True, jika tidak nilai dari variabel my_value tidak akan berubah.

Selanjutnya, pada module-module odoo, biasanya safe_eval digunakan untuk apa sih ?

Yang pertama, safe_eval biasanya digunakan untuk menguji domain. Misal pada module pos_loyalty di odoo 14 enterprise. Atau pada module dynamic_print_access_right yang saya buat untuk membatasi hak akses tombol print secara dinamis.

Pada module dynamic_print_access_right saya memiliki field dengan nama condition yang bertipe data Char, dimana pada viewnya field tersebut saya render dengan widget turunan dari widget domain bawaan odoo. Widget ini memungkinkan user untuk menulis domain dengan mudah, dimana domain yang ditulis user nantinya akan tetap disimpan sebagai Char.

Tampilan widget domain pada odoo

Karena domain disimpan sebagai Char/String di database, tentu saja kita tidak bisa langsung melewatkannya pada method search atau method lain bawaan model. Kita harus mengubahnya menjadi domain yang valid yang bertipe data List, nah salah satu caranya adalah dengan menggunakan module safe_eval, seperti yang saya gunakan di module dynamic_print_access_right atau pada module pos_loyalty di odoo 14 enterprise. Odoo sendiri tidak konsisten dalam hal mengubah string menjadi domain yang valid ini, misal pada module coupon, mereka menggunakan module ast yang juga bawaan python.

Penggunaan safe_eval yang lain pada module odoo adalah saat mereka menggunakannya untuk kalkulasi rumus laporan Profit and Loss atau Balance Sheet di odoo enterprise. Perhatikan rumus blok Income di menu Accounting >> Configuration >> Financial Reports >> Profit and Loss berikut ini.

Rumus income di profit and loss odoo

Perhatikan field Formulas pada gambar di atas yang bernilai OPINC + OIN. Jika Anda punya akses ke odoo enterprise, silakan lihat file formula.py di module account_reports. Di file tersebut ekspresi OPINC + OIN akan diolah dengan safe_eval, tetapi cara odoo menulis kode di module ini menurut saya cukup menarik. Karena saat odoo memangil safe_eval nilai OPINC dan OIN belum ada, tidak seperti kode di bawah ini.

safe_eval('OPINC + OIN', {'OPINC': 100, 'OIN': 200})

Tetapi nilai kedua variabel itu akan diolah secara dinamis. Sebagai gambaran, anggaplah kita memiliki ekspresi seperti ini.

my_string = 'S00006 + S00007'

Anggap saja S00006 dan S00007 adalah nomor Sales Order yang ada di database kita. Dengan menggunakan safe_eval kita akan membuat nilai dari kedua variable itu bisa dicari dan dihitung secara dinamis, sehingga jika kita mengganti ekpresi menjadi P00006 * P00008 dan ingin data diambil dari Purchase Order, kita tidak perlu mengubah kode kita.

Pertama, mari kita buat sebuah class yang inherit ke Dictionary.

class DataSet(dict):

    def __init__(self, model, field_to_search, field_to_calculate):
        super().__init__()
        self.model = model
        self.field_to_search = field_to_search
        self.field_to_calculate = field_to_calculate

    def __getitem__(self, item):
        record = self.model.search([(self.field_to_search,'=',item)],limit=1)
        if record:
            return getattr(record, self.field_to_calculate)
        else:
            return 0

Kemudian saat memanggil safe_eval kita bisa menggunakan class DataSet di atas sebagai argument, seperti pada kode di bawah ini.

my_string = 'S00006 + S00007'

value = safe_eval(my_string,DataSet(self.env['sale.order'],'name','amount_total'), nocopy=True)

Saat safe_eval berusaha untuk mendapatkan nilai dari S00006, safe_eval akan mentrigger method __getitem__ bawaan Dictionary. Oleh karena itulah kita meng-override method ini agar bisa return value yang dinamis. Jika kita ingin mengunakan safe_eval untuk keperluan yang lain, misal untuk mendapatkan nilai subtotal dari Purchase Order, kita bisa mengubahnya dengan mudah, seperti pada kode di bawah ini.

my_string = 'P00006 + P00007'

value = safe_eval(my_string,DataSet(self.env['purchase.order'],'name','amount_untaxed'), nocopy=True)

Perlu kita ingat, saat kita menggunakan cara yang dinamis ini pada safe_eval, kita harus mengatur nilai dari argument nocopy agar bernilai True. Tetapi jika menggunakan eval tidak perlu.

Demikian yang bisa saya tulis mengenai module safe_eval bawaan odoo. Sebenarnya saya belum menemukan contoh kode yang membuat safe_eval lebih baik dan harus menjadi pilihan utama dibandingkan eval. Tetapi saya berusaha untuk selalu menggunakan safe_eval untuk menguji ekspresi yang bisa diinput secara bebas oleh user. Dengan embel-embel kata safe dan terbukti safe_eval memblacklist beberapa kode, misal import seperti di atas, saya harap dengan menggunakan safe_eval aplikasi yang saya buat bisa lebih secure.

Tulisan Serupa

Leave a Reply

Your email address will not be published. Required fields are marked *