Thừa kế model trong Odoo

  Apr 25, 2020      2m
   

Tut 6: Thừa kế model trong Odoo

Thừa kế model trong Odoo

Trước khi xem bài viết này, bạn vui lòng hoàn thành hướng dẫn ở Tut 5 hoặc download mã nguồn ở cuối bài Tut 5. Bài viết này sẽ hướng dẫn bạn cách thừa kế một model trong Odoo.

Mã nguồn hoàn chỉnh cho bài viết Tut 6 này được đính kèm ở cuối bài viết.

Các loại thừa kế model trong Odoo

Thừa kế model trong Odoo

  • Traditional Inheritance
    • Class Inheritance: thêm field, chỉnh sửa field, chỉnh sửa phương thức trong model đã có; tổ chức database lưu trữ chung một bảng với model gốc.
    • Prototype Inheritance: copy field, copy phương thức trong model đã có; tổ chức database lưu trữ trong bảng mới khác với bảng của model gốc.
  • Delegation Inheritance: cho phép đa thừa kế; tổ chức database lưu trữ trong bảng mới khác với bảng của model gốc; model mới có field tham khảo đến model gốc.

Tạo module mypet+

Mình sẽ khởi tạo module mypet+ sẽ thừa kế và mở rộng phần model của module mypetTut 5. Mã nguồn chương trình của module mypet bạn có thể download tại đây:

https://bit.ly/odoo-2019-tut-5

Cấu trúc thư mục mypet_plus sẽ như sau:

models
    |- __init__.py       # `from . import my_pet_plus`
    |- my_pet_plus.py
security
    |- ir.model.access.csv # note: update later
static
    |- description
        |- icon.png
views
    |- my_pet_plus_views.xml # update later
__init__.py              # `from . import models`
__manifest__.py

Ghi chú: bỏ thư mục controllers và cập nhật tương ứng cho file __init__.py

__manifest__.py

# -*- coding: utf-8 -*-
{
    'name': "My pet (+) - minhng.info",
    'summary': """My pet plus""",
    'description': """Managing pet information""",
    'author': "minhng.info",
    'website': "https://minhng.info",
    'category': 'Uncategorized',
    'version': '0.1',
    'depends': [
        'mypet',
    ],
    'data': [
        'security/ir.model.access.csv',
        'views/my_pet_plus_views.xml',
    ],
    # 'qweb': ['static/src/xml/*.xml'],
    'installable': True,
    'application': True,
}

Mình kế thừa từ model mypet, do đó nhớ cập nhật chỗ depends!

Hiện thực thừa kế Class trong Odoo (Class Inheritance)

Hiện thực thừa kế kiểu Class cho phép ta thêm các thuộc tính mới vào model cũ, hoặc chỉnh sửa các trường thuộc tính cũ. View của model thừa kế và model gốc là dùng chung.

Trong thư mục model của mypet_plus, mình sẽ hiện thực thừa kế:

  • Thêm 1 field mới là đồ chơi cho pet (toy).
  • Sửa tuổi mặc định lúc tạo mới record pet lên bằng 2 thay vì 1 (bạn xem hiện thực ở module mypet sẽ thấy default đang set bằng 1).
  • Thêm một lựa chọn giới tính mới cho pet (selection).

models/my_pet_plus.py

# -*- coding: utf-8 -*-
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError, ValidationError

class MyPetPlus(models.Model):
    _name = "my.pet"
    _inherit = "my.pet"
    _description = "Extend mypet model"

    # add new field
    toy = fields.Char('Pet Toy', required=False)
    
    # modify old fields
    age = fields.Integer('Pet Age', default=2) # change default age from 1 to 2
    gender = fields.Selection(selection_add=[('sterilization', 'Sterilization')]) # add one more selection

Nhìn file model trong module thừa kế, bạn sẽ thấy rất gọn. Vì nó sẽ thừa hưởng toàn bộ field và method của model gốc, ta chỉ việc đặc tả các điểm thay đổi.

Ghi chú: Thừa kế kiểu class này ta sẽ dùng rất thường xuyên trong quá trình dev / customization module cho khách hàng / nhu cầu người dùng!

Phần view lúc này ta tạm thời để trống, vì thừa kế view cũ để mở rộng hiển thị thêm field toy sẽ được trình bày ở Tut sau.

views/my_pet_plus_views.xml

<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <data>
    </data>
</odoo>

Tiến hành Update App list và cài module mới mà ta vừa thừa kế.

my pet plus module

Sau khi cài đặt, vào xem form mypet ta sẽ thấy 2 điểm thay đổi rõ rệt:

  • Tạo mới record, Pet Age để giá trị mặc định là 2.
  • Field Gender đã có thêm lựa chọn mới ta vừa thêm.

Hiện thực thừa kế Prototype trong Odoo (Prototype Inheritance)

Hiện thực thừa kế kiểu Prototype này cho phép ta clone một model ra và thêm các thuộc tính mới. Model mới này độc lập model cũ do Database bên dưới lưu ở hai bảng khác nhau. Thừa kế này cũng không làm ảnh hưởng các field trong view cũ. Ta cũng cần phải tạo View mới tương ứng cho model mới này để hiển thị cho người dùng.

Mình sẽ tiến hành clone model mypet thành một model gọi là superpet (siêu nhân thú cưng), nó sẽ có những thuộc tính mới không tưởng dành cho thú cưng bình thường như:

  • Thêm field is_super_strength: siêu sức mạnh hay không
  • Thêm field is_fly: bay được không
  • Thêm field planet: đến từ hành tinh nào

models/super_pet.py

# -*- coding: utf-8 -*-
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError, ValidationError

class SuperPet(models.Model):
    _name = "super.pet" # <- new model name
    _inherit = "my.pet" # <- inherit fields and methods from model "my.pet"
    _description = "Prototype inheritance"

    # add new field
    is_super_strength = fields.Boolean("Is Super Strength", default=False)
    is_fly = fields.Boolean("Is Super Strength", default=False)
    planet = fields.Char("Planet")
    
    # avoid error: TypeError: Many2many fields super.pet.product_ids and my.pet.product_ids use the same table and columns
    product_ids = fields.Many2many(comodel_name='product.product', 
                                string="Related Products", 
                                relation='super_pet_product_rel', # <- change this relation name!
                                column1='col_pet_id',
                                column2='col_product_id')

LƯU Í: nhớ cập nhật file models/__init__.py tương ứng mỗi khi thêm một model mới (file python). Các code Python sau mình sẽ không nhắc vẫn đề này nữa. Mặc định bạn theo dõi Tutorial đến đây đã phải nắm rõ phần này!

models/__init__.py

from . import my_pet_plus
from . import super_pet # <- add this

Ta cần phải tạo view mới riêng cho model super.pet:

views/my_pet_plus_views.xml

<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <data>
        <record id="super_pet_form_view" model="ir.ui.view">
            <field name="name">super.pet.form.view</field>
            <field name="model">super.pet</field>
            <field name="arch" type="xml">
                <form>
                    <sheet>
                        <div class="oe_title">
                            <label for="name" string="Pet Name" class="oe_edit_only"/>
                            <h1><field name="name" placeholder="e.g. Kittie"/></h1>
                            <label for="owner_id"/>
                            <h3><field name="owner_id"/></h3>
                        </div>
                        <group>
                            <group>
                                <field name="is_super_strength"/>
                                <field name="is_fly"/>
                            </group>
                            <group>
                                <field name="planet"/>
                            </group>
                        </group>
                        <group name="images">
                            <group>
                                <field name="age"/>
                                <field name="weight"/>
                            </group>
                            <group>
                                <field name="dob"/>
                                <field name="gender"/>
                            </group>                            
                        </group>
                        <notebook>
                            <page name="general_information" string="General Information">
								<group>
									<group>
										<field name="nickname"/>
									</group>
									<group>
										<label for="description" colspan="2"/>
										<field name="description" colspan="2" nolabel="1"/>
									</group>
								</group>
							</page>
							<page name="additional_information" string="Additional Information">
								<group>
									<group string="Images">
										<field name="pet_image" string="Pet's Image" widget="image"/>
									</group>
									<group string="Products">
										<field name="product_ids" widget="many2many_tags"/>
									</group>
								</group>
							</page>							
						</notebook>
                    </sheet>
                </form>
            </field>
        </record>

        <record id="super_pet_tree_view" model="ir.ui.view">
            <field name="name">super.pet.tree.view</field>
            <field name="model">super.pet</field>
            <field name="arch" type="xml">
                <tree string="Super Pets" default_order="create_date desc">
                    <field name="name"/>
                    <field name="nickname"/>
                    <field name="age"/>
                    <field name="weight"/>                    
                    <field name="dob"/>
                    <field name="gender"/>
                    <field name="owner_id"/>
                </tree>
            </field>
        </record>

        <record id="action_super_pet" model="ir.actions.act_window">
            <field name="name">Super Pet</field>
            <field name="type">ir.actions.act_window</field>
            <field name="res_model">super.pet</field>
            <!-- <field name="view_type">form</field> --> <!-- Odoo 13 has removed this field -->
            <field name="view_ids" eval="[(5, 0, 0),
                (0, 0, {'view_mode': 'tree', 'view_id': ref('super_pet_tree_view')}),
                (0, 0, {'view_mode': 'form', 'view_id': ref('super_pet_form_view')})]"/>
        </record>

        <menuitem id="menu_super_pet"
            name="Super Pet"
            action="action_super_pet"
            sequence="10"
            groups="base.group_user"/>
    </data>
</odoo>

Cập nhật phân quyền security cho model mới super.pet:

security/ir.model.access.csv

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_super_pet,access_super_pet,model_super_pet,base.group_user,1,1,1,1

Do mình vừa thêm file Python và chỉnh sửa view xml, ta cần restart server Odoo và upgrade module.

Xem thành quả:

odoo tutorial super pet form

Bạn sẽ thấy rằng các record của super.petmy.pet được lưu trữ độc lập. Khi tạo mới record bên này sẽ không liên quan hay ảnh hưởng bên kia. Đơn thuần Super Pet là model nâng cấp mở rộng của My Pet.

Hiện thực thừa kế Delegation trong Odoo (Delegation Inheritance)

Thừa kế Delegation có các đặc tính sau:

  • Cho phép mình link record của model mới đến record của model gốc.
  • Điểm lợi của cách làm này đó là khi bạn thay đổi giá trị của một field ở record của model gốc thì phần thừa kế trong model mới sẽ được đồng bộ thay đổi theo.
  • Model mới lưu trữ trên bảng riêng và có view riêng.

Trong Odoo, bạn sẽ bắt gặp cách thường kế này model product.templateproduct.product. Ta có thể hiểu nôm na rằng product.template là một template cho product ta sẽ bán (ví dụ: cây son). Còn product.product sẽ là các variant của product template (ví dụ: cây son màu đỏ, màu xanh, màu hồng, …). Các record trong lớp con sẽ link đến record (cây son) trong lớp cha (gốc).

Sau đây mình sẽ tiến hành hiện thực minh họa kiểu thừa kế này trên module Thú Cưng. Ngữ cảnh là sau khi nuôi thú cưng chán chê, ví dụ như chó, mèo, heo, … Mình quyết định kinh doanh bán chúng đi, nhưng mà nói đến mèo thì cũng có "mèo this", "mèo that" nên đâu thể bán đồng giá?! Giống như mèo mun sẽ bán giá $100, mèo tam thể quý hiếm bán giá $175, còn mèo cute bán giá $999 chẳng hạn. Nhưng rõ ràng các "biến thể" (variant) của mèo đều được úp từ một khuôn là con mèo ra (my.pet)

Biết được ngữ cảnh bài toán, ta tiến hành các bước sau:

  • Sửa code gốc model my.pet bổ sung thêm 1 field basic_price, đồng thời sửa view cho hiển thị field này, mục đích giá cơ bản cho loại pet là MÈO. Mình chưa hướng dẫn bạn cách dùng thừa kế, vì thừa kế ở view sẽ được trình bày ở Tut 7!
  • Biến thể của mèo cần có những field đặc thù cho nó: pet_type (mô tả loại pet từ cơ bản đến cao cấp chẳng hạn), pet_color (màu thú), bonus_price (giá cộng thêm ứng với loại và màu sắc của thú cưng này) và cuối cùng là final_price được tính bằng công thức: final_price = basic_price + bonus_price
  • Hiện thực view tương ứng cho model thừa kế
  • Bước cuối cùng là hưởng thụ thành quả ^^

Thêm giá cả cho Thú Cưng

mypet/models/my_pet.py

... # như code cũ
    basic_price = fields.Float('Basic Price', default=0)

mypet/views/my_pet_views.xml

...
    <group>
        <field name="basic_price"/>
        <field name="age"/>
        <field name="weight"/>
    </group>
...

Upgrade module mypet bạn sẽ thấy có thêm field "Basic Price" trên form.

Thừa kế delegation trong Odoo

Nhảy qua module mypet_plus ta tiến hành thừa kế model. Thêm các field:

  • pet_type: selection. Ghi chú: bạn có thể dùng Many2one để linh động thêm mới. Mình không có thời gian nên dùng tạm selection cho đơn giản.
  • pet_color: selection. Ghi chú: có thể thay bằng Many2one.
  • bonus_price: float
  • final_price: compute

mypet_plus/models/product_pet.py

# -*- coding: utf-8 -*-
from odoo import api, fields, models, tools, _
from odoo.exceptions import UserError, ValidationError

class ProductPet(models.Model):
    _name = "product.pet"
    _inherits = {'my.pet': 'my_pet_id'}
    _description = "Product Pet"

    my_pet_id = fields.Many2one(
        'my.pet', 'My Pet',
        auto_join=True, index=True, ondelete="cascade", required=True)
    
    pet_type = fields.Selection([
        ('basic', 'Basic'),
        ('intermediate', 'Intermediate'),
        ('vip', 'VIP'),
        ('cute', 'Cute'),
    ], string='Pet Type', default='basic')
    
    pet_color = fields.Selection([
        ('white', 'White'),
        ('black', 'Black'),
        ('grey', 'Grey'),
        ('yellow', 'Yellow'),
    ], string='Pet Color', default='white')
    
    bonus_price = fields.Float("Bonus Price", default=0)
    
    final_price = fields.Float("Final Price", compute='_compute_final_price')
    
    def _compute_final_price(self):
        for record in self:
            record.final_price = record.basic_price + record.bonus_price

security/ir.model.access.csv: cập nhật permission

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_super_pet,access_super_pet,model_super_pet,base.group_user,1,1,1,1
access_product_pet,access_product_pet,model_product_pet,base.group_user,1,1,1,1

Thêm view cho model vừa thừa kế

Thêm view XML tương ứng cho model product.pet, bạn nhớ cập nhật file __manifest__.py thêm đường dẫn đến view mới vừa thêm nha.

mypet_plus/views/product_pet_views.xml

<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <data>
        <record id="product_pet_form_view" model="ir.ui.view">
            <field name="name">product.pet.form.view</field>
            <field name="model">product.pet</field>
            <field name="arch" type="xml">
                <form>
                    <sheet>
                        <div class="oe_title">
                            <label for="name" string="Pet Name" class="oe_edit_only"/>
                            <h1><field name="name" placeholder="e.g. Kittie"/></h1>
                            <label for="owner_id"/>
                            <h3><field name="owner_id"/></h3>
                        </div>
                        <group>
                            <group>
                                <field name="my_pet_id"/>
                                <field name="pet_type"/>
                                <field name="pet_color"/>
                            </group>
                            <group>
                                <field name="basic_price"/>
                                <field name="bonus_price"/>
                                <field name="final_price"/>
                            </group>
                        </group>
                        <group name="images">
                            <group>
                                <field name="age"/>
                                <field name="weight"/>
                            </group>
                            <group>
                                <field name="dob"/>
                                <field name="gender"/>
                            </group>                            
                        </group>
                        <notebook>
                            <page name="general_information" string="General Information">
								<group>
									<group>
										<field name="nickname"/>
									</group>
									<group>
										<label for="description" colspan="2"/>
										<field name="description" colspan="2" nolabel="1"/>
									</group>
								</group>
							</page>
							<page name="additional_information" string="Additional Information">
								<group>
									<group string="Images">
										<field name="pet_image" string="Pet's Image" widget="image"/>
									</group>
									<group string="Products">
										<field name="product_ids" widget="many2many_tags"/>
									</group>
								</group>
							</page>							
						</notebook>
                    </sheet>
                </form>
            </field>
        </record>

        <record id="product_pet_tree_view" model="ir.ui.view">
            <field name="name">product.pet.tree.view</field>
            <field name="model">product.pet</field>
            <field name="arch" type="xml">
                <tree string="Product Pets" default_order="create_date desc">
                    <field name="name"/>
                    <field name="nickname"/>
                    <field name="age"/>
                    <field name="weight"/>
                    <field name="gender"/>
                    <field name="pet_type"/>
                    <field name="pet_color"/>
                    <field name="final_price"/>
                </tree>
            </field>
        </record>

        <record id="action_product_pet" model="ir.actions.act_window">
            <field name="name">Product Pet</field>
            <field name="type">ir.actions.act_window</field>
            <field name="res_model">product.pet</field>
            <!-- <field name="view_type">form</field> --> <!-- Odoo 13 has removed this field -->
            <field name="view_ids" eval="[(5, 0, 0),
                (0, 0, {'view_mode': 'tree', 'view_id': ref('product_pet_tree_view')}),
                (0, 0, {'view_mode': 'form', 'view_id': ref('product_pet_form_view')})]"/>
        </record>

        <menuitem id="menu_product_pet"
            name="Product Pet"
            action="action_product_pet"
            sequence="10"
            groups="base.group_user"/>
    </data>
</odoo>

mypet_plus/__manifest__.py: bổ sung đường dẫn đến view mới cho product.pet

...
    'data': [
        'security/ir.model.access.csv',
        'views/my_pet_plus_views.xml',
        'views/product_pet_views.xml', # <--
    ],
...

Tận hưởng thành quả thừa kế Delegation

Mình vào menu My Pet tạo một record thông tin thú cưng có basic_price = $100

Tiếp theo vào menu Product Pet tạo một record link đến record vừa tạo, bổ sung thông tin như màu sắc, loại và giá cộng thêm (bonus_price).

Cách thừa kế này cho phép ta mô tả các biến thể của một sản phẩm. Việc cập nhật thông tin ở My Pet sẽ đồng bộ đến tất cả các Product Pet mà đang được liên kết đến record bị thay đổi đó. Giả sử giá mèo cơ bản tăng lên thành $120, lúc đó các Pet mèo biến thể khác đồng bộ được cập nhật giá, mà ta không phải mất công chỉnh sửa từng record variant một.

Vì vậy lựa chọn cách thừa kế phù hợp để thiết kế model trong Odoo cũng rất quan trọng.

Bài viết cũng đã khá dài rồi, hẹn gặp lại các bạn ở bài viết tiếp theo nha Tut 7: thừa kế view trong Odoo

Download Odoo - Tut 6

Source code download: Tut 6 - Inheritance in models

Chuyên mục thảo luận cho Tut 6 @ https://www.facebook.com/groups/odoo.dev/learning_content/?filter=226430075118574&post=909726269492410


minhng.info đã chính thức mở khóa học training lập trình Odoo OFFLINE ở TP.HCM, dự kiến khai giảng 05/12/2020.

Thông tin chi tiết khóa học Odoo: Khóa học lập trình Odoo (TP.HCM)

Cài đặt Odoo:

Danh sách bài viết series Odoo:

Tham gia ngay group trên Facebook để cùng thảo luận với đồng bọn nhé: