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命名,增強程式碼可