odoo12從零開始:三、2)odoo模型層


前言

  上一篇文章(創建你的第一個應用模塊(module))已經大致描述了odoo的模型層(model)和視圖層(view),這一篇文章,我們將系統地介紹有關於model的知識,其中包括:

1、模型的類型:Model、TransientModel、AbstractModel
2、模型的屬性:_name,_description,_table,_order等
3、模型的字段類型:Char、Boolean、Selection、Binary、Integer、Float、Date、Datetime、Html、Text、Many2one、One2many等
4、模型的字段屬性:string,default,help,index,copy,readonly,required,groups,states,translate,compute,store,domain,related等 
5、模型的自帶字段:create_uid,create_date,write_uid,write_date
6、模型的修飾器:@api.multi,@api.model,@api.constrains,@api.onchange,@api.depends等
7、模型的生命周期方法:create、write、unlink、
default_get、name_get等

 模型的類型

      odoo的模型是系統的數據中心,所有的數據都通過odoo類的ORM(對象關系映射)映射到數據庫的表,所有的數據操作除了直接通過sql查詢外,都通過odoo類進行操作。odoo類通過python繼承models.Model、models.TransientModel、models.AbstractModel實現,其中:系統會為Model, TransientModel的所有字段建立數據庫字段,不會為AbstractModel建立任何數據庫字段。

Tips:
1、Odoo的命名遵循大駝峰的命名方式(eg. EmployeeSalary)
2、Odoo通過python類繼承實現模型定義(eg. Class Employee(models.Model))

1、Model

      Model是存儲數據記錄的最主要手段,它是持久化地對數據記錄(record)進行存儲,直至對其進行刪除。例如我們在上一節建立的員工模塊,它繼承的就是models.Model,它將會存儲所有的員工檔案信息,這也是我們想要的。

2、TransientModel

  TransientModel我們稱之為"瞬時模型",數據庫也會為瞬時模型存儲數據,但是Odoo會有專門的定時任務對瞬時模型進行清空,這將會大大節省了數據的存儲空間。它的優點在於可以使用Odoo正常的功能函數,但是不會對數據庫造成數據負擔,主要的用途就是向導(wizard)。例如:res.config.settings模型使用的就是瞬時模型,它在專門的地方對其他模型的數據值進行配置,而不產生多余存儲空間。我們在odoo12之應用:一、雙因子驗證(Two-factor authentication, 2FA)一節中使用"導出翻譯"功能界面就是一個由瞬時模型寫的向導界面:

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import base64
import contextlib
import io

from odoo import api, fields, models, tools, _

NEW_LANG_KEY = '__new__'

class BaseLanguageExport(models.TransientModel):
    _name = "base.language.export"
    _description = 'Language Export'

    @api.model
    def _get_languages(self):
        langs = self.env['res.lang'].search([('translatable', '=', True)])
        return [(NEW_LANG_KEY, _('New Language (Empty translation template)'))] + \
               [(lang.code, lang.name) for lang in langs]
   
    name = fields.Char('File Name', readonly=True)
    lang = fields.Selection(_get_languages, string='Language', required=True, default=NEW_LANG_KEY)
    format = fields.Selection([('csv','CSV File'), ('po','PO File'), ('tgz', 'TGZ Archive')],
                              string='File Format', required=True, default='csv')
    modules = fields.Many2many('ir.module.module', 'rel_modules_langexport', 'wiz_id', 'module_id',
                               string='Apps To Export', domain=[('state','=','installed')])
    data = fields.Binary('File', readonly=True)
    state = fields.Selection([('choose', 'choose'), ('get', 'get')], # choose language or get the file
                             default='choose')

    @api.multi
    def act_getfile(self):
        this = self[0]
        lang = this.lang if this.lang != NEW_LANG_KEY else False
        mods = sorted(this.mapped('modules.name')) or ['all']

        with contextlib.closing(io.BytesIO()) as buf:
            tools.trans_export(lang, mods, buf, this.format, self._cr)
            out = base64.encodestring(buf.getvalue())

        filename = 'new'
        if lang:
            filename = tools.get_iso_codes(lang)
        elif len(mods) == 1:
            filename = mods[0]
        extension = this.format
        if not lang and extension == 'po':
            extension = 'pot'
        name = "%s.%s" % (filename, extension)
        this.write({'state': 'get', 'data': out, 'name': name})
        return { 'type': 'ir.actions.act_window', 'res_model': 'base.language.export', 'view_mode': 'form', 'view_type': 'form', 'res_id': this.id, 'views': [(False, 'form')], 'target': 'new', }

 它通過在view中"導出"按鈕實現調用act_getfile方法,實現導出功能,並return回到base.language.export頁面中。

<footer states="choose">
    <button name="act_getfile" string="Export" type="object" class="btn-primary"/>
    <button special="cancel" string="Cancel" type="object" class="btn-secondary"/>
</footer>

3、AbstractModel

      AbstractModel(抽象類模型)和我們平時理解的面向對象語言中的抽象類是類似的功能,在抽象類中定義一些通用的字段和方法,在子類中進行繼承或者重寫,可以理解為它是沒有"多態"功能的抽象類。比如我們使用的所有字段類: Integer、Float等,還有MailThread,都是抽象模型,為子類實現部分功能。

class Float(models.AbstractModel):
    _name = 'ir.qweb.field.float'
    _description = 'Qweb Field Float'
    _inherit = 'ir.qweb.field.float'

    @api.model
    def from_html(self, model, field, element):
        lang = self.user_lang()
        value = element.text_content().strip()
        return float(value.replace(lang.thousands_sep, '')
                          .replace(lang.decimal_point, '.'))

模型的屬性

 我們主要介紹幾個常用的屬性:

_name: 必填屬性,odoo類的唯一標識,全局不能重復。
_description: 描述屬性,只在查看模型界面的時候作為展示使用,沒有實際用戶,可選不填,但好的編碼習慣我們應該書寫盡量詳盡的描述。
_table:對應的數據庫表名,可選,默認為模型的_name替換.為_,實際上為了方便和統一,我們在一般的情況下不修改數據庫表名。
_order: 數據視圖的排列順序,實用功能,方便tree視圖的查看,例如我們使用_order = 'sequence,id'表示根據單號和記錄id排序, "create_date desc":根據最新創建時間排序。
Tip:為了代碼的可讀性以及數據可維護性,筆者建議不要使用_table和_order功能。

視圖排序功能可以通過在tree/kanban視圖中使用default_order實現:

<tree string="xxx" default_order="create_date desc"></tree>

模型的字段類型

Char: 單行文本
Boolean: 邏輯字段,True/False
Selection: 列表選擇字段,第一個參數為元組列表,表示可選列表,
如:
GENDER = [
    ('male', u''),
    ('female', u''),
    ('other', u'其他')
]
gender = fields.Selection(GENDER, string=u'性別')
Binary: 二進制字段,通常用於圖片、附件等文件讀寫
Integer: 整型字段
Float: 浮點型字段,可以指定位數digits,使用元組(a,b),其中a是總位數,b 是保留小數位
Date: 日期對象,精確到天
Datetime: 日期對象,精確到秒
Html: 界面展示HTML內容,帶有富文本編輯器
Text: 多行文本,表現為textarea
Many2one: 多對一關系字段,如:
company_id = fields.Many2one('res.company', string=u'公司')
表現為多個員工可以對應同一個公司,'res.company'是odoo內置公司模型
One2many:一對多關系字段,如:
subordinate_ids = fields.One2many('ml.employee', 'leader_id', string=u'下屬')
表示一個員工可以有多個下屬
_sql_constraints: 為數據庫添加約束,例如:
_sql_constraints = [
('attendance_name_uniq', 'unique (name)', u'編碼不能重復!'),
]

模型的字段屬性

string: 字段的默認標簽,展示於用戶界面,不聲明的話odoo將會采用字段名。它通常是第一個參數(一對多,多對一,多對多和Selection除外),也可以使用string="xxx"放置於任何位置。在xml視圖中,可以使用<field name="xxx" string="XXX" />替代默認標簽
default: 設置默認值,允許是函數或者匿名函數,例如:
fields.Date(string='XXX', default=fields.Date.context_today)
help: 幫助信息,通常進行描述字段,將鼠標放置於界面字段上將會顯示幫助信息。
index: 會為數據庫字段添加索引,加快數據讀取速度
copy: 復制時是否復制當前字段,除了關聯字段外,默認為True
readonly: 控制字段是否不可編輯。僅對用戶界面生效,對API調用不生效,如:
date = fields.Date(readonly=True, default=fields.Date.context_today)
self.update{
    date: '2019-01-01'
}
依然生效
required: 控制字段是否必填, 會為數據庫添加約束NOT NULL,因此對API調用是生效的
groups: 控制字段權限,為字段分權限組,只有處於該權限組的用戶可見該字段
states: 控制不同狀態下字段的屬性,表現在用戶界面。如:
states={'draft':[('readonly', '=', False), ('invisible', '=', 'False), ('required', '=' True]}
translate: 表示是否對這個字段生成翻譯
store: 是否存儲該字段,除了compute字段和關聯字段,其他字段默認都為True
compute: 計算字段,屬性值為函數名,會為該字段調用對應的函數獲取返回值作為字段的值,擁有該屬性的字段默認readonly為True,store為False
domain: 用於Many2one字段,篩選對應模型的可選記錄值
related: 關聯字段,用於與其他模型字段進行關聯,不會創建數據庫字段,默認只讀,如果設為可寫(readonly=False),字段的修改將會直接影響被關聯的字段。如:
is_open_2fa = fields.Boolean(related='company_id.is_open_2fa', string="Open 2FA", readonly=False)
前提是模型中有company_id這個關聯字段

模型的自帶字段

模型中還自帶有四個默認的字段:create_uid,create_date,write_uid,write_date;

create_uid: 代表記錄的創建用戶

create_date: 代表記錄的創建時間,Datetime類型

write_uid: 代表最近更新記錄的值的用戶

write_date: 代表最近更新記錄的值的時間,Datetime類型

此外,還有一個active字段,代表記錄是否有效

模型的修飾器

@api.multi:對記錄集進行操作的方法需要添加此修飾器,此時self就是要操作的記錄集。所以方法內應該對self進行遍歷,例如:

@api.multi
def xxxxxxx(self):
    for record in self:
        do_something # 對數據集的一些操作

如果方法沒有添加修飾器,默認為@api.multi

@api.model:模型(model)層面的操作需要添加此修飾器,它不針對特定的記錄,也不保留記錄集,self是對模型的引用。相當於類靜態函數。例如create方法,widget的調用方法。

注意:form視圖自帶按鈕的調用應該使用@api.multi,因為它是針對特定記錄的操作,而widget內自定的視圖通過rpc或者call調用方法,應該使用@api.model,因為它是模型層面的調用。

@api.one: 老版本遺留修飾器,不推薦使用,在@api.multi中使用self.ensure_one()來代替

以上是對數據集和模型進行操作的修飾器。此外,還有對字段進行操作的修飾器:

@api.constrains:在界面層面對字段進行約束,對API調用不起效果,例如:

@api.constrains('amount')
def _check_amount(self):
    self.ensure_one()
    if self.amount < 0:
        raise ValidationError(_('The payment amount cannot be negative.'))

@api.onchange:onchange方法只在用戶界面表單視圖中觸發,當用戶修改指定的字段值時,立即執行方法內的業務邏輯,可以用於數據的修改,用戶提示等。

注意1、onchange修改的字段值在保存時會失效,需要在xml字段中使用force_save="1"來存儲,如:
@api.multi @api.onchange(
'a') def _onchange_a(self): for record in self: record.lead_id = 'XXX' <field name="lead_id" readonly="1" force_save="1" />
2、在不同的表單中,可以使用on_change="0"來禁止某個字段的onchange屬性 @api.depends:compute字段所對應的方法需要使用該修飾器,以計算值,例如: # 將二維碼的值賦給otp_qrcode變量 otp_uri = fields.Char(compute='_compute_otp_uri', string="URI") @api.depends('otp_uri') def _compute_otp_qrcode(self): self.ensure_one() self.otp_qrcode = self.create_qr_code(self.otp_uri)

 模型的生命周期方法

create:記錄創建方法,每次記錄的創建都會調用create方法,可以在該方法中添加對數據的校驗,自動生成單號等,例如下面的自動生成單號

@api.model
def create(self, vals):
    if vals.get('name', '/') == '/':
        vals['name'] = self.env['ir.sequence'].next_by_code('picking.batch') or '/'
    return super(StockPickingBatch, self).create(vals)

注意,create方法應該使用@api.model修飾器,我們在任何情況下都應該調用父類的創建方法,以創建記錄,並返回創建的對象:
do something # 創建前的邏輯
rec = super(StockPickingBatch, self).create(vals)
do other things # 創建后的邏輯
return rec

write: 記錄(record)的編輯方法,對已存在的記錄進行編輯,如:

@api.multi
def write(self, values):
    tools.image_resize_images(values)
    return super(Employee, self).write(values)

unlink:記錄的刪除方法,可以在這里對記錄的刪除添加限制,或者在刪除時對其他信息進行清空,如:

@api.multi
def unlink(self):
    if any(line.holiday_id for line in self):
        raise UserError(_('You cannot delete timesheet lines attached to a leaves. Please cancel the leaves instead.'))
    return super(AccountAnalyticLine, self).unlink()
default_get:使用修飾器@api.model包裹,定義數據的默認值,跟字段中的default效果類似,例如:

@api.model
def default_get(self, fields):
    res = super(StockRulesReport, self).default_get(fields)
    product_tmpl_id = False
    if 'product_id' in fields:
        if self.env.context.get('default_product_id'):
            product_id = self.env['product.product'].browse(self.env.context['default_product_id'])
            product_tmpl_id = product_id.product_tmpl_id
            res['product_tmpl_id'] = product_id.product_tmpl_id.id
            res['product_id'] = product_id.id
        elif self.env.context.get('default_product_tmpl_id'):
            product_tmpl_id = self.env['product.template'].browse(self.env.context['default_product_tmpl_id'])
            res['product_tmpl_id'] = product_tmpl_id.id
            res['product_id'] = product_tmpl_id.product_variant_id.id
            if len(product_tmpl_id.product_variant_ids) > 1:
                res['product_has_variants'] = True
    if 'warehouse_ids' in fields:
        warehouse_id = self.env['stock.warehouse'].search([], limit=1).id
        res['warehouse_ids'] = [(6, 0, [warehouse_id])]
    return res

name_get:定義記錄的顯示形式,特別是在Many2one字段中的顯示,比較常用,例如:

@api.multi
@api.depends('employee_id')
def name_get(self):
    """
    名稱顯示格式:[XXX]YYY
    """
    result = []
    for record in self:
        name = '[%s]員工' % (record.employee_id.name)
        result.append((record.id, name))
    return result

那么它的展現形式就會是:[李三]員工

筆者的建議

1、使用onchange + force_save替代compute字段:盡量不要使用compute的字段,在API取值時,每次都會重新觸發一次計算邏輯,重新計算字段的值,這將是一件十分耗時的操作,想象一下,假如你需要將某個模型中的10w條記錄取到,每條記錄中有四到五個compute字段,需要耗時多少?

2、使用default_get代替default,方便默認值的維護。

3、不用在字段屬性中使用readonly,增加代碼的閱讀障礙,無法實現動態控制"是否可寫",而在xml的字段屬性attrs可以動態控制"是否可寫",我們約定所有readonly寫在xml中。

4、確實需要動態控制required的應寫在xml中,不需要的盡量都寫在類字段中,因為API的調用不受到xml中的required的影響。

5、字段的命名:Many2one字段使用xxx_id命名,One2many字段使用xxx_ids命名,增強代碼可讀性

聲明

原文來自於博客園(https://www.cnblogs.com/ljwTiey/p/11492862.html)

轉載請注明文章出處,文章如有任何版權問題,請聯系作者刪除。

合作或問題反饋,聯系郵箱:26476395@qq.com


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM