實踐環境
Odoo 14.0-20221212 (Community Edition)
程式碼實現
模組檔案組織結構
說明:為了更好的表達本文主題,一些和主題無關的檔案、程式碼已略去
odoo14\custom\estate
│ __init__.py
│ __manifest__.py
│
├─models
│ estate_customer.py
│ __init__.py
│
├─security
│ ir.model.access.csv
│
├─static
│ ├─img
│ │ icon.png
│ │
│ └─src
│ ├─js
│ │ estate_customer_tree_upload.js
│ │
│ └─xml
│ estate_customer_tree_view_buttons.xml
│
└─views
estate_customer_views.xml
estate_menus.xml
webclient_templates.xml
測試模型定義
odoo14\custom\estate\models\estate_customer.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import base64
import openpyxl
from odoo.exceptions import UserError
from odoo import models, fields, _ # _ = GettextAlias()
from tempfile import TemporaryFile
class EstateCustomer(models.Model):
_name = 'estate.customer'
_description = 'estate customer'
name = fields.Char(required=True)
age = fields.Integer()
description = fields.Text()
def create_customer_from_attachment(self, attachment_ids=None):
"""
:param attachment_ids: 上傳的資料檔案ID列表
"""
attachments = self.env['ir.attachment'].browse(attachment_ids)
if not attachments:
raise UserError(_("未找到上傳的檔案"))
for attachment in attachments:
file_name_suffix = attachment.name.split('.')[-1]
# 針對文字檔案,暫時不實現資料儲存,僅演示如何處理文字檔案
if file_name_suffix in ['txt', 'html']: # 文字檔案
lines = base64.decodebytes(attachment.datas).decode('utf-8').split('\n')
for line in lines:
print(line)
elif file_name_suffix in ['xlsx', 'xls']: # excel檔案
file_obj = TemporaryFile('w+b')
file_obj.write(base64.decodebytes(attachment.datas))
book = openpyxl.load_workbook(file_obj, read_only=False)
sheets = book.worksheets
for sheet in sheets:
rows = sheet.iter_rows(min_row=2, max_col=3) # 從第二行開始讀取,每行讀取3列
for row in rows:
name_cell, age_cell, description_cell = row
self.create({'name': name_cell.value, 'age': age_cell.value, 'description': description_cell.value})
else:
raise UserError(_("不支援的檔案型別,暫時僅支援.txt,.html,.xlsx,.xls檔案"))
return {
'action_type': 'reload', # 匯入成功後,希望前端執行的動作型別, reload-重新整理tree列表, do_action-執行action
}
說明:
- 函式返回值,具體需要返回啥,實際取決於下文js實現(上傳成功後需要執行的操作),這裡結合實際可能的需求,額外提供另外幾種返回值供參考:
形式1:實現替換當前頁面的效果
return {
'action_type': 'do_action',
'action': {
'name': _('匯入資料'),
'res_model': 'estate.customer',
'views': [[False, "tree"]],
'view_mode': 'tree',
'type': 'ir.actions.act_window',
'context': self._context,
'target': 'main'
}
}
形式2:彈出對話方塊效果
return {
'action_type': 'do_action',
'action': {
'name': _('匯入成功'),
'res_model': 'estate.customer.wizard',
'views': [[False, "form"]],
'view_mode': 'form',
'type': 'ir.actions.act_window',
'context': self._context,
'target': 'new'
}
}
說明:開啟estate.customer.wizard
預設form
檢視
形式3:實現類似瀏覽器重新整理當前頁面效果
return {
'action_type': 'do_action',
'action': {
'type': 'ir.actions.client',
'tag': 'reload' # 或者替換成 'tag': 'reload_context',
}
}
odoo14\custom\estate\models\__init__.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from . import estate_customer
測試資料檔案
mydata.xlsx
姓名 | 年齡 | 備註 |
---|---|---|
張三 | 30 | 喜好運動 |
李四 | 28 | 喜歡美食 |
王五 | 23 |
測試模型檢視定義
odoo14\custom\estate\views\estate_customer_views.xml
<?xml version="1.0"?>
<odoo>
<record id="link_estate_customer_action" model="ir.actions.act_window">
<field name="name">顧客資訊</field>
<field name="res_model">estate.customer</field>
<field name="view_mode">tree,form</field>
</record>
<record id="estate_customer_view_tree" model="ir.ui.view">
<field name="name">estate.customer.tree</field>
<field name="model">estate.customer</field>
<field name="arch" type="xml">
<tree js_class="estate_customer_tree" limit="15">
<field name="name" string="Title"/>
<field name="age" string="Age"/>
<field name="description" string="Remark"/>
</tree>
</field>
</record>
<record id="estate_customer_view_form" model="ir.ui.view">
<field name="name">estate.customer.form</field>
<field name="model">estate.customer</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<field name="name" />
<field name="age"/>
<field name="description"/>
</group>
</sheet>
</form>
</field>
</record>
</odoo>
說明:<tree js_class="estate_customer_tree" limit="15">
,其中estate_customer_tree
為下文javascript中定義的元件,實現新增自定義按鈕;limit
設定列表檢視每頁最大顯示記錄數
選單定義
odoo14\custom\estate\views\estate_menus.xml
<?xml version="1.0"?>
<odoo>
<menuitem id="test_menu_root" name="Real Estate" web_icon="estate,static/img/icon.png">
<menuitem id="data_import_testing" name="資料匯入測試" action="link_estate_customer_action"/>
</menuitem>
</odoo>
estate_customer_tree
元件定義
js實現
為列表檢視新增自定義上傳資料檔案按鈕
odoo14\custom\estate\static\src\js\estate_customer_tree_upload.js
odoo.define('estate.upload.customer.mixin', function (require) {
"use strict";
var core = require('web.core');
var _t = core._t;
var qweb = core.qweb;
var UploadAttachmentMixin = {
start: function () {
// 定義一個唯一的fileUploadID(形如 my_file_upload_upload737)和一個回撥方法
this.fileUploadID = _.uniqueId('my_file_upload');
$(window).on(this.fileUploadID, this._onFileUploaded.bind(this));
return this._super.apply(this, arguments);
},
_onAddAttachment: function (ev) {
// 一旦選擇了附件,自動提交表單(關閉上傳對話方塊)
var $input = $(ev.currentTarget).find('input.o_input_file');
if ($input.val() !== '') {
// o_estate_customer_upload定義在對應的QWeb模版中
var $binaryForm = this.$('.o_estate_customer_upload form.o_form_binary_form');
$binaryForm.submit();
}
},
_onFileUploaded: function () {
// 建立附件後的回撥,根據附件ID執行相關處程式
var self = this;
var attachments = Array.prototype.slice.call(arguments, 1);
// 獲取附件ID
var attachent_ids = attachments.reduce(function(filtered, record) {
if (record.id) {
filtered.push(record.id);
}
return filtered;
}, []);
// 請求模型方法
return this._rpc({
model: 'estate.customer', //模型名稱
method: 'create_customer_from_attachment', // 模型方法
args: ["", attachent_ids],
context: this.initialState.context,
}).then(function(result) { // result為一個字典
if (result.action_type == 'reload') {
self.trigger_up('reload'); // 實現在不重新整理頁面的情況下,重新整理列表檢視// 此處換成 self.reload(); 發現效果也是一樣的
} else if (result.action_type == 'do_action') {
self.do_action(result.action); // 執行action動作
} else { // 啥也不做
}
// 重置 file input, 如果需要,可以再次選擇相同的檔案,如果不新增以下這行程式碼,不重新整理當前頁面的情況下,無法重複匯入相同的檔案
self.$('.o_estate_customer_upload .o_input_file').val('');
}).catch(function () {
self.$('.o_estate_customer_upload .o_input_file').val('');
});
},
_onUpload: function (event) {
var self = this;
// 如果隱藏的上傳表單不存在則建立
var $formContainer = this.$('.o_content').find('.o_estate_customer_upload');
if (!$formContainer.length) {
// estate.CustomerHiddenUploadForm定義在對應的QWeb模版中
$formContainer = $(qweb.render('estate.CustomerHiddenUploadForm', {widget: this}));
$formContainer.appendTo(this.$('.o_content'));
}
// 觸發input選取檔案
this.$('.o_estate_customer_upload .o_input_file').click();
},
}
return UploadAttachmentMixin;
});
odoo.define('estate.customer.tree', function (require) {
"use strict";
var core = require('web.core');
var ListController = require('web.ListController');
var ListView = require('web.ListView');
var UploadAttachmentMixin = require('estate.upload.customer.mixin');
var viewRegistry = require('web.view_registry');
var CustomListController = ListController.extend(UploadAttachmentMixin, {
buttons_template: 'EstateCustomerListView.buttons',
events: _.extend({}, ListController.prototype.events, {
'click .o_button_upload_estate_customer': '_onUpload',
'change .o_estate_customer_upload .o_form_binary_form': '_onAddAttachment',
}),
});
var CustomListView = ListView.extend({
config: _.extend({}, ListView.prototype.config, {
Controller: CustomListController,
}),
});
viewRegistry.add('estate_customer_tree', CustomListView);
});
說明:如果其它模組的列表檢視也需要實現類似功能,想複用上述js,需要替換js中以下內容:
-
修改
estate.upload.customer.mixin
為其它自定義全域性唯一值 -
替換
o_estate_customer_upload
為在對應按鈕檢視模板中定義的對應class屬性值 -
替換
estate.CustomerHiddenUploadForm
為在對應按鈕檢視模板中定義的隱藏表單模版名稱 -
替換
EstateCustomerListView.buttons
為對應按鈕檢視模板中定義的按鈕模版名稱 -
根據需要替換
this._rpc
函式中的model
引數值("estate.customer"),method
引數值("create_customer_from_attachment"),必要的話,修改then
函式實現。 -
替換
estate_customer_tree
為自定義全域性唯一值 -
do_action
為Widget()
的快捷方式(定義在odoo14\odoo\addons\web\static\src\js\core\service_mixins.js
中),用於查詢當前action管理器並執行action --do_action
函式的第一個引數,格式如下:{ 'type': 'ir.actions.act_window', 'name': _('匯入資料'), 'res_model': 'estate.customer', 'views': [[False, "tree"], [False, "form"]], 'view_mode': 'tree', 'context': self._context, 'target': 'current' }
載入js指令碼xml檔案定義
odoo14\custom\estate\views\webclient_templates.xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_common" inherit_id="web.assets_common" name="Backend Assets (used in backend interface)">
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/estate/static/src/js/estate_customer_tree_upload.js"></script>
</xpath>
</template>
</odoo>
按鈕檢視模板定義
odoo14\custom\estate\static\src\xml\estate_customer_tree_view_buttons.xml
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="estate.CustomerHiddenUploadForm">
<div class="d-none o_estate_customer_upload">
<t t-call="HiddenInputFile">
<t t-set="multi_upload" t-value="true"/>
<t t-set="fileupload_id" t-value="widget.fileUploadID"/>
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
<input type="hidden" name="model" value=""/>
<input type="hidden" name="id" value="0"/>
</t>
</div>
</t>
<t t-name="EstateCustomerListView.buttons" t-extend="ListView.buttons">
<t t-jquery="button.o_list_button_add" t-operation="after">
<!--btn表示按鈕類
按鈕顏色:btn-primary--主要按鈕,btn-secondary次要按鈕
按鈕大小:btn-sm小按鈕,btn-lg大按鈕
預設按鈕:btn-default-->
<button type="button" class="btn btn-primary o_button_upload_estate_customer">Upload</button>
</t>
</t>
</templates>
說明:
t-name
:定義模版名稱
t-extend
:定義需要繼承的模板。
t-jquery
:接收一個CSS 選擇器,用於查詢上下文中,同CSS選擇器匹配的元素節點(為了方便描述,暫且稱之為上下文節點)
t-operation
:設定需要對上下文節點執行的操作(為了方便描述,暫且將t-operation
屬性所在元素稱為模板元素),可選值如下:
-
append
將模板元素內容(body)追加到上下文節點的最後一個子元素後面。
-
prepend
將模板元素內容插入到上下文節點的第一個子元素之前。
-
before
將模板元素內容插入到上下文節點之前。
-
after
將模板元素內容插入到上下文節點之後。
-
inner
將模板元素內容替換上下文節點元素內容(所有子節點)
-
replace
將模板元素內容替換上下文節點
-
attributes
模版元素內容應該是任意數量的屬性元素,每個元素都有一個名稱屬性和一些文字內容,上下文節點的命名屬性將被設定為屬性元素的值(如果已經存在則替換,如果不存在則新增)
注意:參考官方文件,t-extend
這種繼承方式為舊的繼承方式,已廢棄,筆者實踐了最新繼承方式,如下
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-name="DataImportTestingListView.buttons" t-inherit="ListView.buttons" t-inherit-mode="primary">
<xpath expr="//button[@calss='btn btn-primary o_list_button_add']" position="after">
<button type="button" class="btn btn-primary o_button_upload_estate_customer">Upload</button>
</xpath>
</t>
</templates>
發現會報錯:
ValueError: Module ListView not loaded or inexistent, or templates of addon being loaded (estate) are misordered
參考連線:https://www.odoo.com/documentation/14.0/zh_CN/developer/reference/javascript/qweb.html
模型訪問許可權配置
odoo14\custom\estate\security\ir.model.access.csv
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_estate_customer_all_perm,access_estate_customer_all_perm,model_estate_customer,base.group_user,1,1,1,1
模組其它配置
odoo14\custom\estate\__init__.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from . import models
odoo14\custom\estate\__manifest__.py
#!/usr/bin/env python
# -*- coding:utf-8 -*-
{
'name': 'estate',
'depends': ['base'],
'data':[
'security/ir.model.access.csv',
'views/webclient_templates.xml',
'views/estate_customer_views.xml',
'views/estate_menus.xml'
],
'qweb':[# templates定義檔案不能放data列表中,提示不符合shema,因為未使用<odoo>元素進行“包裹”
'static/src/xml/estate_customer_tree_view_buttons.xml',
]
}