Tulisan ini adalah bagian ke tiga dari seri tutorial membuat controller pada odoo. Pada tutorial ini saya akan membahas bagaimana membuat form upload dan bagaimana meng-override controller yang sudah ada. Bagian download file sudah saya tulis dalam tulisan ini.
Pertama mari kita buat template atau view untuk untuk menerima input user
<template id="upload_form" name="Upload File Form"> <!-- panggil view website.layout agar navbar dll tampil --> <t t-call="website.layout"> <!-- tambah judul halaman --> <t t-set="additional_title" t-value="'Upload Sale Payment'" /> <div id="wrap"> <div class="container"> <div class="row"> <div class="col-12"> <h2 class="sale-title">Upload Sale Payment</h2> <form action="/process/upload" method="post" enctype="multipart/form-data"> <!-- set csrf token, agar lebih secure, jika tidak diset, pastikan pada controller paremeter csrf diset False --> <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/> <div class="row"> <div class="col-md-12"> <div class="form-group"> <label class="col-form-label">Your Sale Order Number</label> <input type="text" name="so_id" class="form-control" /> </div> </div> </div> <div class="row"> <div class="col-md-12"> <div class="form-group"> <label class="col-form-label">Your Payment Document</label> <input type="file" name="so_file" class="form-control" accept="image/*"/> </div> </div> </div> <div class="row"> <div class="col-12"> <button type="submit" class="btn btn-success">Upload</button> </div> </div> </form> </div> </div> </div> </div> </t> </template>
Form diatas cukup sederhana, yaitu hanya menerima input berupa text yang akan kita jadikan sebagai acuan untuk mencari nomor sale order dan sebuah file input tempat user memilih file untuk diupload. Selanjutnya kita buat controller untuk merender template diatas.
# -*- coding: utf-8 -*- import odoo from odoo import http, models, fields, _ from odoo.http import request import json import unicodedata import base64 class LatihanControllerTiga(http.Controller): @http.route('/upload', type='http', website=True) def render_upload_form(self, **kwargs): return request.render('tutorial_controller.upload_form')
Saat kita memasukkan alamat http://localhost:8069/upload pada web browser template tutorial_controller.upload_form akan dirender seperti pada gambar dibawah ini.
Saat kita klik tombol upload data akan dikirim ke controller atau route /process/upload oleh karena itu mari kita buat controllernya.
@http.route('/process/upload', type='http', website=True) def process_upload_form(self, **kwargs): data = { 'message': 'Thank you !! We will prosess your Sale Order soon' } # mendapatkan sale order, berdasarkan input user dengan input name so_id order = request.env['sale.order'].sudo().search([('name','=', kwargs['so_id'])], limit=1) # mendapatkan daftar file yang diupload oleh user dengan input name so_file files = request.httprequest.files.getlist('so_file') # jika sale order tidak ada karena user memasukkan nomor dokumen yang salah tampilkan pesan if not order: data['message'] = 'Please input your valid Sale Order number !!!' # jika user tidak upload file tampilkan pesan if not files: data['message'] = 'Please upload your payment form' # jika sale order valid dan ada file, simpan file tersebut kedalam model attachment # jika res_model dan res_id di isi file yang baru diupload akan tampil di form yang sesuai if order and files: for ufile in files: filename = ufile.filename if request.httprequest.user_agent.browser == 'safari': filename = unicodedata.normalize('NFD', ufile.filename) try: attachment = request.env['ir.attachment'].sudo().create({ 'name': filename, 'datas': base64.encodestring(ufile.read()), 'datas_fname': filename, 'res_model': 'sale.order', 'res_id': order.id }) except Exception: data['message'] = 'Sorry something bad happen. Please try again !!!' return request.render('tutorial_controller.upload_message', data)
Kemudian kita buat template untuk memberikan feedback pada user bahwa file yang diupload berhasi disimpan atau tidak.
<template id="upload_message" name="Upload Form Message"> <!-- panggil view website.layout agar navbar dll tampil --> <t t-call="website.layout"> <!-- tambah judul halaman --> <t t-set="additional_title" t-value="'Upload Message'" /> <div id="wrap"> <div class="container"> <div class="row"> <div class="col-12"> <h1> <span t-esc="message" /> </h1> <a href="/upload" class="btn btn-primary">Back</a> </div> </div> </div> </div> </t> </template>
File pada odoo umumnya disimpan di model ir.attachment dalam format base64, oleh karena itu kita memanggil method create di model ini. Perlu anda ketahui file tersebut akan disimpan di database. Jadi semakin besar dan banyak file yang diupload ukuran database anda akan semakin besar juga.
Untuk melihat file yang sudah berhasil disimpan buka menu Setting >> Technical >> Database Structure >> Attachments
Atau jika anda ingin file tersebut ditampilkan di model terkait (dalam tutorial ini model Sale Order) pastikan anda isi field res_model dengan nama model dimana file akan ditampilkan dan field res_id dengan id atau primary key model tersebut. Dalam tutorial ini res_id diisi secara otomatis berdasarkan nomor dokumen yang diinput oleh user. Jika user input nomor dokument SO001 pada Sale Order dengan nomor tersebut akan tampil file yang diupload oleh user seperti pada gambar dibawah ini.
Jika file tidak tampil cari dan klik tombol dengan icon paper clip di form tersebut.
Selanjutnya kita akan membahas bagaimana meng-override controller yang sudah ada. Sebagai contoh kasus kita akan meng-override controller /shop/product/ seperti pada gambar dibawah ini untuk menampilkan jumlah stok yang tersedia
Pertama kita perlu tahu controller atau route /shop/product/ ini di source code odoo ditulis di module apa. Jika anda menggunakan operating system linux, masuk folder addons odoo kemudian gunakan perintah grep untuk mencari lokasi controller atau route /shop/product/ seperti dibawah ini.
grep -Rl '/shop/product/'
Pada komputer saya hasilnya adalah seperti ini
Pada gambar diatas terlihat bahwa controller atau route /shop/product/ ditulis di module website_sale lebih tepatnya di file website_sale/controllers/main.py. Buka file tersebut kemudian lihat nama class dimana controller atau route /shop/product/ ini ditulis. Kemudian import class tersebut pada module kita seperti pada kode dibawah ini
from odoo.addons.website_sale.controllers.main import WebsiteSale
Kemudian buat class yang inherit ke class tersebut dan override controller atau route /shop/product/ seperti dibawah ini.
class WebsiteSaleInherit(WebsiteSale): @http.route(['/shop/product/<model("product.template"):product>'], type='http', auth="public", website=True) def product(self, product, category='', search='', **kwargs): if not product.can_access_from_current_website(): raise NotFound() product_context = dict(request.env.context, active_id=product.id, partner=request.env.user.partner_id) ProductCategory = request.env['product.public.category'] if category: category = ProductCategory.browse(int(category)).exists() attrib_list = request.httprequest.args.getlist('attrib') attrib_values = [[int(x) for x in v.split("-")] for v in attrib_list if v] attrib_set = {v[1] for v in attrib_values} keep = QueryURL('/shop', category=category and category.id, search=search, attrib=attrib_list) categs = ProductCategory.search([('parent_id', '=', False)]) pricelist = request.website.get_current_pricelist() from_currency = request.env.user.company_id.currency_id to_currency = pricelist.currency_id compute_currency = lambda price: from_currency._convert(price, to_currency, request.env.user.company_id, fields.Date.today()) if not product_context.get('pricelist'): product_context['pricelist'] = pricelist.id product = product.with_context(product_context) values = { 'search': search, 'category': category, 'pricelist': pricelist, 'attrib_values': attrib_values, 'compute_currency': compute_currency, 'attrib_set': attrib_set, 'keep': keep, 'categories': categs, 'main_object': product, 'product': product, 'optional_product_ids': [p.with_context({'active_id': p.id}) for p in product.optional_product_ids], 'get_attribute_exclusions': self._get_attribute_exclusions, 'available_qty': product.sudo().qty_available, # tambahan tidak ada di source code asli odoo 'product_uom_name': product.sudo().uom_id.display_name # tambahan tidak ada di source code asli odoo } return request.render("website_sale.product", values)
Kemudian cari template website_sale.product lalu override seperti pada kode dibawah ini
<!-- jangan lupa untuk menulis nama module yang akan kita override sebelum external id sebagai tanda bawah kita override template pada module tersebut --> <template id="website_sale.product" name="Product"> <t t-call="website.layout"> <t t-set="additional_title" t-value="product.name" /> <div itemscope="itemscope" itemtype="http://schema.org/Product" id="wrap" class="js_sale"> <section t-attf-class="container py-2 oe_website_sale #{(compute_currency(product.lst_price) - product.website_price) > 0.01 and website.get_current_pricelist().discount_policy == 'without_discount' and 'discount'}" id="product_detail"> <div class="row"> <div class="col-md-4"> <ol class="breadcrumb"> <li class="breadcrumb-item"> <a t-att-href="keep(category=0)">Products</a> </li> <li t-if="category" class="breadcrumb-item"> <a t-att-href="keep('/shop/category/%s' % slug(category), category=0)" t-field="category.name" /> </li> <li class="breadcrumb-item active"> <span t-field="product.name" /> </li> </ol> </div> <div class="col-md-8"> <div class="form-inline justify-content-end"> <t t-call="website_sale.search"/> <t t-call="website_sale.pricelist_list"> <t t-set="_classes">ml-2</t> </t> </div> </div> </div> <div class="row"> <div class="col-md-6"> <t t-set="variant_img" t-value="any(product.mapped('product_variant_ids.image_variant'))"/> <t t-set="image_ids" t-value="product.product_image_ids"/> <div id="o-carousel-product" class="carousel slide" data-ride="carousel" data-interval="0"> <div class="carousel-outer"> <div class="carousel-inner"> <div t-if="variant_img" class="carousel-item active" itemprop="image" t-field="product[:1].product_variant_id.image" t-options="{'widget': 'image', 'class': 'product_detail_img js_variant_img', 'alt-field': 'name', 'zoom': 'image', 'unique': str(product['__last_update']) + (str(product.product_variant_id['__last_update']) or '')}"/> <div t-attf-class="carousel-item#{'' if variant_img else ' active'}" itemprop="image" t-field="product.image" t-options="{'widget': 'image', 'class': 'product_detail_img', 'alt-field': 'name', 'zoom': 'image', 'unique': product['__last_update']}"/> <t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg"> <div class="carousel-item" t-field="pimg.image" t-options='{"widget": "image", "class": "product_detail_img", "alt-field": "name", "zoom": "image" }'/> </t> </div> <t t-if="len(image_ids) or variant_img"> <a class="carousel-control-prev" href="#o-carousel-product" role="button" data-slide="prev" > <span class="fa fa-chevron-left" role="img" aria-label="Previous" title="Previous"/> </a> <a class="carousel-control-next" href="#o-carousel-product" role="button" data-slide="next"> <span class="fa fa-chevron-right" role="img" aria-label="Next" title="Next"/> </a> </t> </div> <ol class="carousel-indicators" t-if="len(image_ids) or variant_img"> <li t-if="variant_img" data-target="#o-carousel-product" data-slide-to="0" class="active"> <img class="img img-fluid js_variant_img_small" t-attf-src="/website/image/product.product/{{product.product_variant_id.id}}/image/90x90" t-att-alt="product.name"/> </li> <li data-target="#o-carousel-product" t-att-data-slide-to="1 if variant_img else '0'" t-att-class="'' if variant_img else 'active'"> <img class="img img-fluid" t-attf-src="/website/image/product.template/{{product.id}}/image/90x90" t-att-alt="product.name"/> </li> <t t-if="len(image_ids)" t-foreach="image_ids" t-as="pimg"> <li data-target="#o-carousel-product" t-att-data-slide-to="pimg_index + (variant_img and 2 or 1)"> <img class="img img-fluid" t-attf-src="/website/image/product.image/{{pimg.id}}/image/90x90" t-att-alt="pimg.name"/> </li> </t> </ol> </div> </div> <div class="col-md-6 col-xl-4 offset-xl-2" id="product_details"> <h1 itemprop="name" t-field="product.name">Product Name</h1> <span itemprop="url" style="display:none;" t-esc="'%sshop/product/%s' % (request.httprequest.url_root, slug(product))"/> <form action="/shop/cart/update" method="POST"> <input type="hidden" name="csrf_token" t-att-value="request.csrf_token()" /> <div class="js_product js_main_product"> <t t-placeholder="select"> <input type="hidden" class="product_id" name="product_id" t-att-value="product.product_variant_id.id if len(product.product_variant_ids) == 1 else '0'" /> <input type="hidden" class="product_template_id" name="product_template_id" t-att-value="product.id" /> <t t-call="sale.variants"> <t t-set="ul_class" t-value="'flex-column'" /> </t> </t> <t t-call="website_sale.product_price" /> <p t-if="product.has_dynamic_attributes() or len(product.product_variant_ids) > 1" class="css_not_available_msg bg-danger" style="padding: 15px;">This combination does not exist.</p> <a role="button" id="add_to_cart" class="btn btn-primary btn-lg mt8 js_check_product a-submit" href="#">Add to Cart</a> </div> </form> <!-- tambahan tidak ada di source code asli odoo - start --> <hr /> <p> Available stock is <span t-esc="available_qty" /> <span t-esc="product_uom_name"/> </p> <!-- tambahan tidak ada di source code asli odoo - end --> <hr t-if="product.description_sale" /> <div class="o_not_editable"> <p t-field="product.description_sale" class="text-muted" /> </div> <hr /> <p class="text-muted"> 30-day money-back guarantee<br /> Free Shipping in U.S.<br /> Buy now, get in 2 days </p> </div> </div> </section> <div itemprop="description" t-field="product.website_description" class="oe_structure mt16" id="product_full_description" /> </div> </t> </template>
Dari kedua kode diatas perhatikan bagian yang ada komentar tambahan, dibagian tersebutlah kita melakukan perubahan pada source code asli odoo.
Setelah module selesai diupgrade tampilannya akan jadi seperti ini