Skip to content

Latest commit

 

History

History
2148 lines (1452 loc) · 79.1 KB

File metadata and controls

2148 lines (1452 loc) · 79.1 KB

odoo 入門篇

建議觀看影片, 會更清楚:smile:

建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)

odoo 手把手建立第一個 addons

這篇主要介紹 Many2one, Many2many, One2many 這三個東西,

以下將介紹這個 addons 的結構

說明

odoo 手把手教學 - Many2one - part1

先來看 models/models.py

Many2one

......
class DemoExpenseTutorial(models.Model):
    _name = 'demo.expense.tutorial'
    _description = 'Demo Expense Tutorial'

    name = fields.Char('Description', required=True)
    employee_id = fields.Many2one('hr.employee', string="Employee", required=True)
    user_id = fields.Many2one('res.users', default=lambda self: self.env.user)
......

alt tag

一個 hr.employee 可以對到很多個 demo.expense.tutorial,

所以是 多(demo.expense.tutorial) 對 一(hr.employee) 的關係,

來看 db 中的狀況

demo_expense_tutorial 會多出一個欄位 ( 對應 hr_employee 的 id )

alt tag

user_id field 中的 default=lambda self: self.env.user 代表預設的值會設定當前登入的 user

alt tag

因為 One2many 比較特別, 所以我們先介紹 Many2many:laughing:

odoo 手把手教學 - Many2many - part2

Many2many

要建立 Many2many 之前, 一定要先定義一個 model,

先定義 DemoTag (也請記得設定 security/ir.model.access.csv )

models/models.py

......
class DemoTag(models.Model):
    _name = 'demo.tag'
    _description = 'Demo Tags'

    name = fields.Char(string='Tag Name', index=True, required=True)
    active = fields.Boolean(default=True, help="Set active.")
......

然後接著到底下 models/models.py

......
class DemoExpenseTutorial(models.Model):
    _name = 'demo.expense.tutorial'
......
    # https://www.odoo.com/documentation/12.0/reference/orm.html#odoo.fields.Many2many
    # Many2many(comodel_name=<object object>, relation=<object object>, column1=<object object>, column2=<object object>, string=<object object>, **kwargs)
    #
    # relation: database table name
    #

    # By default, the relationship table name is the two table names
    # joined with an underscore and _rel appended at the end.
    # In the case of our books or authors relationship, it should be named demo_expense_tutorial_demo_tag_rel.
### odoo 手把手教學 - Many2many - part2
    tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id', string='Tges')
......

Many2many 比較多欄位, 我來說明一下,

comodel_namedemo.tag (需要對應的 model)

relationdemo_expense_tag (table 名稱),

Many2many 會多出一個 table, 這邊是針對 table 命名,

也就是 db 中的 table 名稱,

alt tag

如果你沒填 relation 這個值, 預設的 table 名稱會是 model名稱 + comodel_name + _rel,

所以也就會是 demo_expense_tutorial_demo_tag_rel.

column1demo_expense_id, demo.expense.tutorial table 中對應的 id.

column2tag_id, demo.tag table 中對應的 id.

繼續看 models/models.py

......
    # Related (Reference) fields (不會存在 db)
    # readonly default 為 True
    # store default 為 False
    gender = fields.Selection('Gender', related='employee_id.gender')
......

fields.Selection 就只是下拉選單而已, 比較特別的是 related 這個,

related='employee_id.gender' 這邊的意思是, 會自己去找 employee_id 中的 gender,

到 employee 中找到 gender 為 Male

alt tag

DemoExpenseTutorial 中的 gender 自然會是 Male,

alt tag

但要注意幾件事情,

related 預設的 field 是不會儲存在 db 中的, store default 為 False,

你在 table 中是找不到 gender field 的 (如下圖),

如果你想要儲存在 db 中的, 請另外設定 store=Ture,

alt tag

然後 readonly default 為 True, 也就是說你是不可以去修改的,

( 如果要修改請去 employee 中找到 gender 修改 )

alt tag

接著來看最後一個

odoo 手把手教學 - One2many - part3

One2many

alt tag

models/models.py

一個 demo.expense.sheet.tutorial 可以對應很多個 demo.expense.tutorial

所以是 一(demo.expense.sheet.tutorial) 對 多(demo.expense.tutorial) 的關係,

......
class DemoExpenseSheetTutorial(models.Model):
    _name = 'demo.expense.sheet.tutorial'
    _description = 'Demo Expense Sheet Tutorial'

    name = fields.Char('Expense Demo Report Summary', required=True)

    # One2many is a virtual relationship, there must be a Many2one field in the other_model,
    # and its name must be related_field
    expense_line_ids = fields.One2many(
        'demo.expense.tutorial', # related model
        'sheet_id', # field for "this" on related model
        string='Expense Lines')
......

說明 expense_line_ids 裡面的參數意義,

demo.expense.tutorial 代表關連的 model (必填)

sheet_id 代表所關連 model 的 field (必填)

也就是說如果你要建立 One2many, 一定也要有一個 Many2one,

但如果建立 Many2one 則不一定要建立 One2many.

One2many 是一個虛擬的欄位, 你在資料庫中是看不到它的存在(如下圖)

alt tag

你只會看到 Many2one 中的 sheet_id

alt tag

models/models.py, demo.expense.tutorial 中的 sheet_id

class DemoExpenseTutorial(models.Model):
    _name = 'demo.expense.tutorial'
    _description = 'Demo Expense Tutorial'
    ......
    sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report")
    ......

記得也要設定對應的 security/ir.model.access.csvsecurity/security.xml.

views/view.xml

......
  <record id="view_form_demo_expense_tutorial" model="ir.ui.view">
    <field name="name">Demo Expense Tutorial Form</field>
    ......
            <!-- <field name="tag_ids"/> -->
            <field name="tag_ids" widget="many2many_tags"/> <!-- widget -->
            <field name="sheet_id"/>
......

在 odoo 中很有多 widget, 大家可以改成其他的 widget 試試看, 像是 many2many_tags 的 widget

alt tag

views/view.xml

......
    <record id="view_form_demo_expense_sheet_tutorial" model="ir.ui.view">
    <field name="name">Demo Expense Sheet Tutorial Form</field>
    <field name="model">demo.expense.sheet.tutorial</field>
    <field name="arch" type="xml">
      <form string="Demo Expense Sheet Tutorial">
        <sheet>
          <group>
            <field name="name"/>
          </group>
          <notebook>
              <page string="Expense">
                <field name="expense_line_ids">
                  <tree>
                    <field name="name"/>
                    <field name="employee_id"/>
                    <field name="tag_ids" widget="many2many_tags"/>
                  </tree>
                </field>
              </page>
          </notebook>
        </sheet>
      </form>
    </field>
  </record>
......

view_form_demo_expense_sheet_tutorial 裡的 One2many 中的 expense_line_ids fields,

就把需要的欄位填進去即可,

alt tag

odoo 手把手教學 - One2many Editable Bottom and Top - part3-1

這邊補充一下 One2many 中的 Editable Bottom 和 Top

views/view.xml

  <record id="view_form_demo_expense_sheet_tutorial" model="ir.ui.view">
    <field name="name">Demo Expense Sheet Tutorial Form</field>
    <field name="model">demo.expense.sheet.tutorial</field>
    <field name="arch" type="xml">
      <form string="Demo Expense Sheet Tutorial">
        <sheet>
          ......
          <notebook>
              <page string="Expense">
                <field name="expense_line_ids" >
                  <tree>
                  <!-- <tree editable="top"> -->   <!-- <<<<<<<<<<<< -->
                  <!-- <tree editable="bottom"> --> <!-- <<<<<<<<<<<< -->
                    <field name="name"/>
                    <field name="employee_id"/>
                    <field name="tag_ids" widget="many2many_tags"/>
                  </tree>
                </field>
              </page>
          </notebook>
        </sheet>
      </form>
    </field>
  </record>

如果你加上 editable 這個參數, 當你新增 record 的時候, 就不會整個跳出視窗, 可以直接在裡面輸入

(或許比較好看:smile:)

alt tag

至於 editable="bottom"editable="top" 的差別如下

editable="top" 一個新增的 record 會顯示在最上面

alt tag

editable="bottom"一個新增的 record 會顯示在最下面

alt tag

odoo 手把手教學 - Search Filters - part4

接著來看 filter 的功能

......
<record id="view_filter_demo_expense_tutorial" model="ir.ui.view">
  <field name="name">Demo Expense Tutorial Filter</field>
  <field name="model">demo.expense.tutorial</field>
  <field name="arch" type="xml">
      <search string="Demo Expense Tutorial Filter">
          <field name="name" string="Name"/>
          <field name="employee_id" filter_domain="['|', ('employee_id', 'ilike', self), ('user_id', 'ilike', self)]" string="User"/>
          <filter name="filter_inactive" domain="[('active','=',False)]" string="Inactive"/>
          <filter name="gender" domain="[('gender','=','male')]" string="Male"/>
          <separator/>
          <filter name="name" domain="[('name', 'ilike', 'a')]" string="Name_2"/>
          <group expand="0" string="Group By">
            <filter string="Sheet" name="sheet" domain="[]" context="{'group_by': 'sheet_id'}"/>
            <filter string="Employee" name="employee" domain="[]" context="{'group_by': 'employee_id'}"/>
          </group>
      </search>
  </field>
</record>
......

主要都是在 tree 中搜尋, 可參考上面的 code 去看對應的邏輯

alt tag

alt tag

<field name="employee_id" filter_domain="['|', ('employee_id', 'ilike', self), ('user_id', 'ilike', self)]" string="User"/>

特別說明一下這個, self 代表使用者輸入的內容.

<separator> 代表 and, 如果沒寫則代表 or.

and

alt tag

or

alt tag

<filter string="Sheet" name="sheet" domain="[]" context="{'group_by': 'sheet_id'}"/>

<filter string="Employee" name="employee" domain="[]" context="{'group_by': 'employee_id'}"/>

依照特定的 fields 分組

alt tag

點選後的狀態

alt tag

odoo 手把手教學 - 說明 noupdate 以及 domain_force - part5

再來看看

security/ir_rule.xml

......
    <data noupdate="1">

        <record id="ir_rule_demo_expense_user" model="ir.rule">
            <field name="name">Demo Expense User</field>
            <field name="model_id" ref="model_demo_expense_tutorial"/>
            <field name="domain_force">[('employee_id.user_id.id', '=', user.id)]</field>
            <field name="groups" eval="[(4, ref('demo_expense_tutorial_group_user'))]"/>
        </record>

        <record id="ir_rule_demo_expense_manager" model="ir.rule">
            <field name="name">Demo Expense Manager</field>
            <field name="model_id" ref="model_demo_expense_tutorial"/>
            <field name="domain_force">[(1, '=', 1)]</field>
            <field name="groups" eval="[(4, ref('demo_expense_tutorial_group_manager'))]"/>
        </record>
    </data>
......

noupdate="1"的意思為當更新 addons 時, 是不是允許重新 import data,

noupdate="1"

假如我們在安裝完 addons 之後, 去刪除 record data, 然後再重新去更新 addons,

你會發現你刪除的 data 並沒有被安裝回來 (只能先移除 addons 再重新安裝).

noupdate="0"

假如我們在安裝完 addons 之後, 去刪除 record data, 然後再重新去更新 addons,

你會發現你刪除的 data 會被安裝回來.

id="ir_rule_demo_expense_user" 第一段為針對 demo_expense_tutorial_group_user

限制 domain_force, 規則很簡單, 這類的 user 只能看到自己的單子, 也就是

[('employee_id.user_id.id', '=', user.id)].

id="ir_rule_demo_expense_manager" 針對 demo_expense_tutorial_group_manager

限制 domain_force, 這邊比較特別 [(1, '=', 1)], 代表沒有限制, 也就是全部的單子都

可以看到.

demo 用戶為 User, 所以只能看到自己的單子

alt tag

Admin 用戶為 Manager, 所以能看到全部的單子

alt tag

接著補充說明一下, 在 security/ir_rule.xml 中可以設定更細的權限管理

<record id="ir_rule_demo_expense_user" model="ir.rule">
    <field name="name">Demo Expense User</field>
    <field name="model_id" ref="model_demo_expense_tutorial"/>
    <field name="domain_force">[('employee_id.user_id.id', '=', user.id)]</field>
    <field name="groups" eval="[(4, ref('demo_expense_tutorial_group_user'))]"/>
    <!-- Groups (no group = global) -->
    <!-- <field name="global" eval="True"/> -->
    <field eval="0" name="perm_unlink"/>
    <field eval="1" name="perm_write"/>
    <field eval="1" name="perm_read"/>
    <field eval="1" name="perm_create"/>
</record>

預設的 rule 如果沒有特別設定權限, CRUD 都會是 true,

但也可以去分別設定 (如上教學),

像這邊給了 read, write, create 的權限 (沒給 delete 權限)

alt tag

<field name="global" eval="True"/> 則代表 global,

基本上, no group = global.

odoo 手把手教學 - 如何透過 button 呼叫 view, form - part6

接下來介紹前面跳過的部份, 也就是透過 button 的方式呼叫 view, form,

models/models.py

class DemoExpenseTutorial(models.Model):
    _name = 'demo.expense.tutorial'
    _description = 'Demo Expense Tutorial'
    ......

    @api.multi
    def button_sheet_id(self):
        return {
            'view_mode': 'form',
            'res_model': 'demo.expense.sheet.tutorial',
            'res_id': self.sheet_id.id,
            'type': 'ir.actions.act_window'
        }

透過前端呼叫 button_sheet_id, 會回傳屬於它的 sheet_id

alt tag

點進去會直接進入 sheet 中的 form

alt tag

views/view.xml

前端的部份就是呼叫 button_sheet_id

<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
  <field name="name">Demo Expense Tutorial Form</field>
  <field name="model">demo.expense.tutorial</field>
  <field name="arch" type="xml">
    <form string="Demo Expense Tutorial">
      <sheet>
        <div class="oe_button_box" name="button_box">
          <button class="oe_stat_button" name="button_sheet_id"
                  string="SHEET ID" type="object"
                  attrs="{'invisible':[('sheet_id','=', False)]}" icon="fa-bars"/>
        </div>
        ......
      </sheet>
    </form>
  </field>
</record>

既然找了 sheet_id, 也來做一個反查回來的, 也就是透過 sheet_id 找到 demo.expense.tutorial,

models/models.py

class DemoExpenseSheetTutorial(models.Model):
    _name = 'demo.expense.sheet.tutorial'
    _description = 'Demo Expense Sheet Tutorial'

    ......

    @api.multi
    def button_line_ids(self):
        return {
            'name': 'Demo Expense Line IDs',
            'view_type': 'form',
            'view_mode': 'tree,form',
            'res_model': 'demo.expense.tutorial',
            'view_id': False,
            'type': 'ir.actions.act_window',
            'domain': [('sheet_id', '=', self.id)],
        }

    ......

res_model 為目標的 model demo.expense.tutorial.

domain 稍微說明一下 [('sheet_id', '=', self.id)],,

sheet_id 是指目標 model demo.expense.tutorial 的 sheet_id,

self.id 是指當下 model demo.expense.sheet.tutorial 的 id.

alt tag

點下去會帶出它的 demo.expense.tutorial

alt tag

views/view.xml 的部份

......
<record id="view_form_demo_expense_sheet_tutorial" model="ir.ui.view">
  <field name="name">Demo Expense Sheet Tutorial Form</field>
  <field name="model">demo.expense.sheet.tutorial</field>
  <field name="arch" type="xml">
    <form string="Demo Expense Sheet Tutorial">
      <sheet>
        <div class="oe_button_box" name="button_box">
          <button class="oe_stat_button" name="button_line_ids"
                  string="SHEET IDs" type="object"
                  attrs="{'invisible':[('expense_line_ids','=', False)]}" icon="fa-bars"/>
        </div>
        ......
      </sheet>
    </form>
  </field>
</record>

odoo 手把手教學 - 說明 name_get 和 _name_search - part7

最後來看 models/models.py 中比較特殊的部份,

分別是 name_get_name_search,

class DemoExpenseSheetTutorial(models.Model):
    _name = 'demo.expense.sheet.tutorial'
    _description = 'Demo Expense Sheet Tutorial'

    ......

    @api.multi
    def name_get(self):
        names = []
        for record in self:
            name = '%s-%s' % (record.create_date.date(), record.name)
            names.append((record.id, name))
        return names

    # odoo12/odoo/odoo/addons/base/models/ir_model.py
    @api.model
    def _name_search(self, name='', args=None, operator='ilike', limit=100):
        if args is None:
            args = []
        domain = args + ['|', ('id', operator, name), ('name', operator, name)]
        # domain = args + [ ('name', operator, name)]
        # domain = args + [ ('id', operator, name)]
        return super(DemoExpenseSheetTutorial, self).search(domain, limit=limit).name_get()

首先是 name_get

這個的功能主要是去修改 name 的名稱, 在這邊我們加上當下的時間

(可以依照自己的需求下去修改)

alt tag

Many2one 時也會看到自己定義的 name_get

注意:exclamation: 這些增加的值是不會儲存進 db 中的, db 中還是儲存的是 name 的內容而已 (概念和 compute field 一樣:smile:)

alt tag

再來要來說明 _name_search,

如果沒有它, 假設我知道某個資料的 id 是 4, 在搜尋的地方打上 id,

你會發現找不到資料:joy:

alt tag

但今天如果有了 _name_search 並實作它,

你會發現這次你打 id 會才成功找到需要的資料:satisfied:

我在 code 中有放幾個範例註解, 大家可以自行玩玩看:smile:

alt tag

odoo 手把手教學 - 使用 python 增加取代 One2many M2X record - part8

參考 models/models.py

這邊只需要注意3個 function,

add_demo_expense_record link_demo_expense_record replace_demo_expense_record

分別對應的 button 為下圖

參考 views/view.xml

alt tag

class DemoExpenseSheetTutorial(models.Model):
    _name = 'demo.expense.sheet.tutorial'
    _description = 'Demo Expense Sheet Tutorial'

    name = fields.Char('Expense Demo Report Summary', required=True)

    # One2many is a virtual relationship, there must be a Many2one field in the other_model,
    # and its name must be related_field
    expense_line_ids = fields.One2many(
        'demo.expense.tutorial', # related model
        'sheet_id', # field for "this" on related model
        string='Expense Lines')

    @api.multi
    def add_demo_expense_record(self):
        # (0, _ , {'field': value}) creates a new record and links it to this one.

        data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')

        tag_data_1 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_1')
        tag_data_2 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_2')

        for record in self:
            # creates a new record
            val = {
                'name': 'test_data',
                'employee_id': data_1.employee_id,
                'tag_ids': [(6, 0, [tag_data_1.id, tag_data_2.id])]
            }

            self.expense_line_ids = [(0, 0, val)]

    @api.multi
    def link_demo_expense_record(self):
        # (4, id, _) links an already existing record.

        data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')

        for record in self:
            # link already existing record
            self.expense_line_ids = [(4, data_1.id, 0)]

    @api.multi
    def replace_demo_expense_record(self):
        # (6, _, [ids]) replaces the list of linked records with the provided list.

        data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
        data_2 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_2')

        for record in self:
            # replace multi record
            self.expense_line_ids = [(6, 0, [data_1.id, data_2.id])]

說明 add_demo_expense_record

......
@api.multi
def add_demo_expense_record(self):
    # (0, _ , {'field': value}) creates a new record and links it to this one.

    data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')

    tag_data_1 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_1')
    tag_data_2 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_2')

    for record in self:
        # creates a new record
        val = {
            'name': 'test_data',
            'employee_id': data_1.employee_id,
            'tag_ids': [(6, 0, [tag_data_1.id, tag_data_2.id])]
        }

        self.expense_line_ids = [(0, 0, val)]
......

(0, _ , {'field': value}) 新建一筆 record 並且連接它.

self.env.ref(......) 這個的用法是去取得既有的資料, 路徑在 data/demo_expense_tutorial_data.xml.

當你點選按鈕, 下面就會一直新增資料

alt tag

說明 link_demo_expense_record

......
@api.multi
def link_demo_expense_record(self):
    # (4, id, _) links an already existing record.

    data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')

    for record in self:
        # link already existing record
        self.expense_line_ids = [(4, data_1.id, 0)]
......

(4, id, _) 連接已經存在的 record.

當你點選按鈕, 下面會直接連接一比資料, 如果已經連接就不會有動作,

alt tag

說明 replace_demo_expense_record

......
@api.multi
def replace_demo_expense_record(self):
    # (6, _, [ids]) replaces the list of linked records with the provided list.

    data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
    data_2 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_2')

    for record in self:
        # replace multi record
        self.expense_line_ids = [(6, 0, [data_1.id, data_2.id])]
......

(6, _, [ids]) 使用 list 取代既有的 records.

當你點選按鈕, 會使用你定義的 list 取代全部的 records.

alt tag

odoo 手把手教學 - tree create delete edit False - part9

通常管理一個使用者可不可以建立 records, 是根據 security 資料夾裡面的檔案,

也就是 security.xml ir_rule.xml ir.model.access.csv.

記住:exclamation: odoo 可以從 model 層(db層) 或權限下手, 也可以從 view 那層下手,

當然, 如果是從安全性的角度來看 從 model 層(db層) 或權限下手 是比較高全的:smile:

今天就是要來介紹 從 view 那層下手,

增加一個 tree views/view.xml

......
<record id="view_tree_demo_expense_tutorial_no_create" model="ir.ui.view">
  <field name="name">Demo Expense Tutorial List No Create</field>
  <field name="model">demo.expense.tutorial</field>
  <field name="arch" type="xml">
    <tree string="no_create_tree" create="0" delete="false" edit="1" editable="top">
      <field name="name"/>
      <field name="employee_id"/>
    </tree>
  </field>
</record>
......

重點在 <tree string="no_create_tree" create="0" delete="false" edit="1" editable="top">

這段, 裡面增加了一下 tag, 允許就是 1True, 不允許就是 0False.

儘管你有權限建立 records, 如果你設定了 create="0", 你還是沒辦法建立 records.

也記得在 views/menu.xml 增加 action,

並且要指定 view_id (也就是剛剛建立出來的那個)

......
<!-- Action to open the demo_expense_tutorial_no_craete -->
<record id="action_expense_tutorial_no_craete" model="ir.actions.act_window">
    <field name="name">Demo Expense Tutorial Action No Craete</field>
    <field name="res_model">demo.expense.tutorial</field>
    <field name="view_type">form</field>
    <field name="view_mode">tree</field>
    <field name="view_id" ref="view_tree_demo_expense_tutorial_no_create"/>
</record>
......

你會發現 create delete 的按鈕都消失了

alt tag

odoo 手把手教學 - 同一個 model 使用不同的 view_ids - part10

一般來說, 在定義一個 model 時, 通常會搭配一個 form 的 view 以及 tree 的 view, 或是特別指定一個 view,

像是前面介紹到的 view_id, 但有時候會有這種情況, 也就是一個 model, 在兩個不同的地方, 分別顯示不同的

form 的 view 以及 tree 的 view, 這時候就要使用 view_ids 分別下去定義.

現在 meun 上會多出 Demo Expense Tutorial View ids, 點下去分別有

Demo Expense Tutorial View id 1 以及 Demo Expense Tutorial View id 2

他們都是屬於 demo.expense.tutorial model, 只不過使用了不同的 view 和 form,

alt tag

為了方便區分不同的 form 和 view, 簡單用 fields 的排序不同

view 1

alt tag

view 2

alt tag

可參考 views/menu.xml

    ......
    <!-- Action to open the menu_expense_tutorial_view_id_1 -->
    <record id="action_expense_tutorial_view_id_1" model="ir.actions.act_window">
        <field name="name">Demo Expense Tutorial Action View id 1</field>
        <field name="res_model">demo.expense.tutorial</field>
        <field name="view_type">form</field>
        <field name="view_mode">tree,form</field>
        <field name="view_ids" eval="[(5, 0, 0),
            (0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.tree_expense_view_id_1')}),
            (0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.form_expense_view_id_1')})]"/>
    </record>

    ......

    <!-- Action to open the menu_expense_tutorial_view_id_2 -->
    <record id="action_expense_tutorial_view_id_2" model="ir.actions.act_window">
        <field name="name">Demo Expense Tutorial Action View id 2</field>
        <field name="res_model">demo.expense.tutorial</field>
        <field name="view_type">form</field>
        <field name="view_mode">tree,form</field>
        <field name="view_ids" eval="[(5, 0, 0),
            (0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.tree_expense_view_id_2')}),
            (0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.form_expense_view_id_2')})]"/>
    </record>

    ......

看起來雖然很複雜, 但其實不難, 設定都和之前的一樣, 只是將 view_id 換成了 view_ids, 然後分別設定不同的 view_id,

這邊只有分別設定 tree 和 form, 如果你想要定義新的 kanban 或其他的 view_mode 也都是可以的.

eval="[(5, 0, 0) 的意思是清除所有和它有關的 record (因為我們重新定義了需要的 view),

相關說明可參考

(0, 0,  { values })    link to a new record that needs to be created with the given values dictionary
(1, ID, { values })    update the linked record with id = ID (write *values* on it)
(2, ID)                remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
(3, ID)                cut the link to the linked record with id = ID (delete the relationship between the two objects but does not delete the target object itself)
(4, ID)                link to existing record with id = ID (adds a relationship)
(5)                    unlink all (like using (3,ID) for all linked records)
(6, 0, [IDs])          replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)

你也可以在 Technical -> Actions -> Window Actions 看到你所設定的 view_ids

alt tag

這些就是剛剛的設定

alt tag

這邊補充一個小技巧, 在定義 view 時, 有一個參數是 <field name="priority" eval="1"/>, 如果你只有一個 view 不需要特別設定,

但如果你有很多個, 你可以透過這個 priority 去決定顯示 view 的優先權.

odoo 手把手教學 - widget 介紹 handle 和 many2onebutton - part11

在 odoo 中很非常多的 widget 可以使用, 除了像前面介紹的 widget="many2many_tags" 之外, 這邊再介紹另外兩個,

首先是 handle widget,

這個比較常和 sequence 搭配一起使用,

可參考 models/models.py

class DemoExpenseTutorial(models.Model):
    _name = 'demo.expense.tutorial'
    _description = 'Demo Expense Tutorial'
    _order = "sequence, id desc"

    ......

    sequence = fields.Integer(index=True, help="Gives the sequence order", default=1)

定義了一個 sequence fields, 然後排序使用 sequence.

在 tree view 中加入 widget="handle",

可參考 views/view.xml

  <record id="view_tree_demo_expense_tutorial" model="ir.ui.view">
    <field name="name">Demo Expense Tutorial List</field>
    <field name="model">demo.expense.tutorial</field>
    <field name="priority" eval="1"/>
    <field name="arch" type="xml">
      <tree>
      <!-- <tree default_order="sequence, id desc"> -->
        <field name="sequence" widget="handle"/>
        ......
      </tree>
    </field>
  </record>

這邊補充一下, 除了在 model 中定義 order 之外, 也可以在 tree, kanban 上定義,

像是 <tree default_order="sequence, id desc">.

這樣就完成了, 你會發現 tree 可以排序了:smile:

alt tag

再來是 many2onebutton widget,

通常如果一個 tree view 上有 many2one 的 fields, 如果想看這個 fields 的資料,

必須要點進去 form, 再點進去 many2one 的 fields 才看的到資料,

如果在 tree 的 many2one fields 上加入 widget="many2onebutton",

就可以直接點進去觀看該 fields 的資料.

可參考 views/view.xml

<record id="view_tree_demo_expense_tutorial" model="ir.ui.view">
    <field name="name">Demo Expense Tutorial List</field>
    <field name="model">demo.expense.tutorial</field>
    <field name="priority" eval="1"/>
    <field name="arch" type="xml">
      <tree>
        ......
        <field name="sheet_id" widget="many2onebutton"/>
      </tree>
    </field>
  </record>

你會發現 many2one fields 變藍色的了, 直接點選即可.

alt tag

odoo 手把手教學 - view 搭配 context - part12

這部份將介紹 view 搭配 context 的使用,

可參考 views/menu.xml

<!-- Action to open the demo_expense_tutorial_context -->
    <record id="action_expense_tutorial_context" model="ir.actions.act_window">
        <field name="name">Demo Expense Tutorial Action Context</field>
        <field name="res_model">demo.expense.tutorial</field>
        <field name="view_type">form</field>
        <field name="view_mode">tree,form</field>
        <field name="domain">[]</field>

        <!-- init search default -->
        <field name="context">{'search_default_name': 'test123'}</field>

        <!-- init create default name 'test123'-->
        <!-- <field name="context">{'default_name': 'test123'}</field> -->
    </record>

來說明一下 <field name="context">{'search_default_name': 'test123'}</field>

這段程式碼, name 是我定義的 fields, 格式是 search_default + fields,

也就是進入這個 view 的時候, 預設會幫你搜尋 name 吻合 test123.

alt tag

接著來看另一個, <field name="context">{'default_name': 'test123'}</field>

這段程式碼, 格式是 default + fields, 注意哦, 這次沒有 search,

那這個和剛剛的有什麼不同呢:question:

當你建立一個 records 的時候, 他預設會幫你的 name fields 自動帶入 test123.

alt tag

context 也可以在 developer mode 中的 Edit Action 看到,

alt tag

odoo 手把手教學 - view 搭配 active_test context - part13

這部份延續上一次的介紹, 來看看 active_test 這個東西,

這部份建議大家看影片會比較清楚:smile:

先來看 models/models.py

class DemoExpenseTutorial(models.Model):
    _name = 'demo.expense.tutorial'
    _description = 'Demo Expense Tutorial'
    _order = "sequence, id desc"

    ......
    active = fields.Boolean(default=True, help="Set active.")

通常如果有定義 active fields,

預設的情況下, 你的 view 就是會顯示只有 active=True 的 records,

如果你要顯示 active=False 的 records,

則必須另外去 filter 出來, 如下圖

alt tag

這樣你可能會問我, 為什麼會這樣呢:question:

原因是 odoo 原始碼內的 odoo/models.py 這段

@api.model
def _where_calc(self, domain, active_test=True):
  ......
  if 'active' in self._fields and active_test and self._context.get('active_test', True):
      # the item[0] trick below works for domain items and '&'/'|'/'!'
      # operators too
      if not any(item[0] == 'active' for item in domain):
          domain = [('active', '=', 1)] + domain

預設如果沒有特別指定, 邏輯就是會跑 active = 1 也就是 True.

那如果我今天希望預設顯示 active 為 True 和 False 同時都顯示, 這樣要如何實作:question:

搭配 <field name="context">{'active_test':False}</field> 這段程式碼,

可參考 views/menu.xml

<!-- Action to open the demo_expense_tutorial_test_active -->
  <record id="action_expense_tutorial_test_active" model="ir.actions.act_window">
      <field name="name">Demo Expense Tutorial Action Test Active</field>
      <field name="res_model">demo.expense.tutorial</field>
      <field name="view_type">form</field>
      <field name="view_mode">tree,form</field>
      <field name="domain">[]</field>

      <!-- init show all (active True False) record -->
      <field name="context">{'active_test':False}</field>

      <!-- init show only (active True) record -->
      <!-- <field name="context">{}</field> -->
  </record>

這樣子預設就會把全部的 records (不管 active 狀態) 都顯示出來.

context 同樣也可以在 developer mode 中的 Edit Action 看到,

alt tag

odoo 手把手教學 - view 搭配 domain - part14

這部份將介紹 view 搭配 domain 的使用,

使用方法和 context 差不多:smile:

可參考 views/menu.xml

  <!-- Action to open the demo_expense_tutorial_domain -->
  <record id="action_expense_tutorial_domain" model="ir.actions.act_window">
      <field name="name">Demo Expense Tutorial Action Domain</field>
      <field name="res_model">demo.expense.tutorial</field>
      <field name="view_type">form</field>
      <field name="view_mode">tree,form</field>
      <field name="domain">[('name', 'like', 'test')]</field>
      <field name="context">{}</field>
  </record>

說明 <field name="domain">[('name', 'like', 'test')]</field> 這段程式碼,

只會顯示 name fields like test 的內容,

注意:exclamation: 和 search_default_name 不一樣的地方是, 他不會顯示 search 的東西,

使用者也不能自行修改

alt tag

domain 同樣也可以在 developer mode 中的 Edit Action 看到,

alt tag

odoo 手把手教學 - 如何看到當下 view 繼承頁面 - part15

有時候當我們寫了很多的繼承 ( tree 或 form), 在當下的頁面, 會不知道是否有被繼承過,

這時候推薦大家一個小技巧:smile:

使用 demo_class_inheritance 這個 addons 當作範例.

首先, 先進去你想要查看的頁面, 這邊進入 hr_expnese

alt tag

debug developer mode 請打開, 可參考 odoo12 如何開啟 odoo developer mode

點選 Edit View: List

alt tag

點選 Inherited Views 這個 tab

alt tag

你就可以很清楚的看到這個頁面被 demo_class_inheritance 繼承:smile:

像是 form 或其他的 view_type 也都是同樣的方法哦:smirk:

odoo 手把手教學 - odoo rainbow - part16

在 odoo 中也有特效這個東西

@api.multi
def button_rainbow_man(self):
    return {
        'effect': {
            'fadeout': 'slow',
            'message': 'hello',
            'type': 'rainbow_man',
        }
    }

alt tag

odoo 手把手教學 - tree decoration - part17

在 odoo 中有很多的 decoration 可以使用, 通常是搭配 tree 顯示特殊的資料.

使用方法非常的簡單, 直接加上需要顯示的邏輯即可,

可參考 views/view.xml

......
  <record id="view_tree_demo_expense_tutorial" model="ir.ui.view">
    <field name="name">Demo Expense Tutorial List</field>
    <field name="model">demo.expense.tutorial</field>
    <field name="priority" eval="1"/>
    <field name="arch" type="xml">
      <tree decoration-info="'info' in name" decoration-muted="'muted' in name" decoration-danger="'danger' in name" decoration-bf="'bf' in name" decoration-warning="'warning' in name" decoration-success="'success' in name">
        ......
      </tree>
    </field>
  </record>
......

alt tag

關於 decoration-{$name} 的詳細說明, 可參考官方文件 Advanced Views

odoo 手把手教學 - model _rec_name 說明 - part18

今天要和大家介紹在 model 中有時會看到的 _rec_name,

models/models.py

......
class DemoTag(models.Model):
    _name = 'demo.tag'
    _description = 'Demo Tags'
    _rec_name = 'complete_name'

    name = fields.Char(string='Tag Name', index=True, required=True)
    complete_name = fields.Char('Complete Name', compute='_compute_complete_name')
    active = fields.Boolean(default=True, help="Set active.")

    @api.depends('name')
    def _compute_complete_name(self):
        for record in self:
            record.complete_name = 'hello world - {}'.format(record.name)
......

首先, 你需要知道一件事, 如果你建立一個 model, 該 model 沒有特別定義 name, 且沒另外指定 _rec_name

(如果沒特別指定 _rec_name, default 就是使用 name)

通常這時候 odoo 的訊息會提醒你建議你設定 name field 或是指定 _rec_name.

alt tag

但這只是 WARNING, 你也可以不要理他.

但我的建議是, 如果你不指定 name, 就請特別去指定 _rec_name.

當然, 如果你有設定了 name, 你也可以特別去指定 _rec_name 為其他的 field.

這個 name_rec_name 只是指定顯示的名稱而已.

詳細的 demo 差異可以看影片的說明.

odoo 手把手教學 - copy override 說明 - part19

在 odoo 中有幾個比較特殊的 function, 分別是 create write copy unlink,

create 建立一比 record 時.

write 更新一比 record 時.

copy 複製一比 record 時.

unlink 刪除一比 record 時.

今天來介紹 copy 當作範例, 其他的大家可以以此類推:smile:

class DemoExpenseTutorial(models.Model):
    _name = 'demo.expense.tutorial'
    _description = 'Demo Expense Tutorial'
    _order = "sequence, id desc"

    name = fields.Char('Description', required=True)
    ......

    tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id', string='Tges', copy=False)

......

    @api.multi
    def copy(self, default=None):
        default = dict(default or {})
        if not default.get('name'):
            default['name'] = '{} copy'.format(self.name)
        return super(DemoExpenseTutorial, self).copy(default)

當點選 Duplicate 時, 會觸發這個 copy

(在這邊做的事情是 override, 和之前介紹的繼承觀念其實是差不多的)

alt tag

你會發現 name 被加上 copy 了.

alt tag

然後預設的 field 是 copy=True, 如果不想要他被 copy, 可以直接在 fields

上設定 copy=False.

最後要提醒大家, 在 odoo 中少用/小心使用 duplicate, 不然就是你要非常清楚裡面寫了甚麼,

因為我很常用到複製出來的 record 有問題, 可能是翻譯, 又可能是複製出來的這比很奇怪,

唯一的可能就是他的 copy 沒有寫好, 特殊的邏輯沒有補上去, 導致你複製出來的 record 行為很怪.

基本上在要修改 create write copy unlink 時, 可以先想想有沒有比較簡單的方式能

改動你的需求, 如果真的沒有, 才選擇改他:smirk:

odoo 手把手教學 - move position 說明 - part20

在 odoo 中常常容易使用到繼承的方式改寫 view, 最常見的 position 就是 after, before, replace,

但有時候會有一種狀況, 就是單純想要交換兩個既有的 fields 位置 (不使用直接在 odoo 上面改, 不推薦),

例如, 下面這個已經存在的 view, 希望交換 Employee 和 Description 的位置,

alt tag

假設這個 view 只能使用繼承的方式修改, 這時候通常很麻煩, 因為有可能你的作法是把整個 tree 去 replace 掉,

再自己去排版, 但為了幾個 fields 就去 replace 掉整個 view, 真得有點麻煩:expressionless:

所以, 今天來認識 move position 這個東西, 寫法可參考 view.xml

<!-- change name, employee_id fields-->
<record id="view_tree_demo_expense_tutorial_move" model="ir.ui.view">
  <field name="name">view_tree_demo_expense_tutorial_move</field>
  <field name="model">demo.expense.tutorial</field>
  <field name="inherit_id" ref="demo_expense_tutorial_v1.view_tree_demo_expense_tutorial"/>
  <field name="arch" type="xml">
      <xpath expr="//field[@name='name']" position="before">
        <field name="employee_id" position="move"/>
      </xpath>
  </field>
</record>

基本上就是在要移動的 fields 上的後面加上 position="move" 即可, 效果如下

alt tag

有了這個東西, 以後單純想要交換兩個 fields, 只需要使用 move 即可, 不需要再整個 replace 了:smile:

odoo 手把手教學 - ir.actions.act_url 說明 - part21

這部份很簡單, 只是要和大家說可以透過 ir.actions.act_url 來開始 url

可參考 models/models.py

@api.multi
def button_act_url(self):
    self.ensure_one()
    return {
        'type': 'ir.actions.act_url',
        'target': 'new',
        # 'target': 'self',
        'url': 'https://github.com/twtrubiks/odoo-demo-addons-tutorial',
    }

odoo 手把手教學 - Smart Button 說明 - part22

甚麼是 Smart Button ❓ 如果你常用 odoo, 你一定常看到這個東西,

如下, 這就是所謂的 Smart Button

alt tag

其實之前就有介紹過 Smart Button 了, 今天就再順便說明要如何設計底下的那個數字

首先, 你需要透過 compute 這個參數建立一個 fields (用來計算出這個數字)

models/models.py

......
demo_expenses_count = fields.Integer(
      compute='_compute_demo_expenses_count',
      string='Demo Expenses Count')

......

def _compute_demo_expenses_count(self):
    # usually used read_group
    for record in self:
        record.demo_expenses_count = len(self.expense_line_ids)
......

在這邊只是簡單的算出數量而已(為了demo), 註解在這邊的意思是說, 通常都會使用 read_group

來做計算 (可自行參考 odoo source code)

最後, 自行將這個 fields 放到 view 中即可, 可參考 views/view.xml

......
<div class="oe_button_box" name="button_box">
  <button class="oe_stat_button"
          name="button_line_ids"
          type="object"
          attrs="{'invisible':[('expense_line_ids','=', False)]}"
          icon="fa-bars">
          <field name="demo_expenses_count" widget="statinfo" string="Counts"/>
  </button>
</div>
......

效果如下

alt tag

odoo 手把手教學 - options create_edit 說明 - part23

今天要來介紹 options 這個參數,

請參考 view.xml

  <record id="view_form_demo_expense_tutorial" model="ir.ui.view">
    <field name="name">Demo Expense Tutorial Form</field>
    <field name="model">demo.expense.tutorial</field>
    <field name="arch" type="xml">
      <form string="Demo Expense Tutorial">
        ......
            <field name="name"/>
            <field name="employee_id"/>
            <!-- <field name="employee_id" options="{'no_quick_create': True}"/> -->
            <!-- <field name="employee_id" options="{'no_create_edit': True}"/> -->
            <!-- <field name="employee_id" options="{'no_create': True}"/> -->
            <!-- <field name="employee_id" options="{'no_open': True}"/> -->
        ......
      </form>
    </field>
  </record>

在一般的情況下 (不設定任何的 option), 顯示如下,

alt tag

alt tag

以下是每一種 option 呈現的效果, 大家可以自行玩玩看:smile:

no_quick_create

alt tag

no_create_edit

alt tag

no_create

alt tag

no_open

alt tag

當然, 如果你的需求是多個組合, 也可以多個一起使用.

odoo 手把手教學 - PostgreSQL ondelete cascade 說明 - part24

今天要介紹 Many2one fields 中的 ondelete="cascade" 參數代表的意思,

這邊要先說明一下, ondelete='cascade' 這個東西並不是 odoo 的, 它是 PostgreSQL 的特性:exclamation:

使用方法很簡單, 如下, 可參考 models/models.py

......
class DemoExpenseTutorial(models.Model):
    _name = 'demo.expense.tutorial'
    ......
    sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report", ondelete='cascade')
    ......

class DemoExpenseSheetTutorial(models.Model):
    _name = 'demo.expense.sheet.tutorial'

    ......
    expense_line_ids = fields.One2many(
        'demo.expense.tutorial', # related model
        'sheet_id', # field for "this" on related model
        string='Expense Lines')
    ......

在開始介紹之前, 大致上除了 'cascade' 之外, 還有其他幾個選項, 如下分別是

說明 ondelete='set null'

這個是 default, 可參考 odoo fields.py 中的 Many2one 說明(如下).

......
class Many2one(_Relational):
    """ The value of such a field is a recordset of size 0 (no
    ......

    :param ondelete: what to do when the referred record is deleted;
        possible values are: ``'set null'``, ``'restrict'``, ``'cascade'``

    ......
    """
    ......
    _slots = {
        'ondelete': 'set null',         # what to do when value is deleted
        'auto_join': False,             # whether joins are generated upon search
        'delegate': False,              # whether self implements delegation
    }
......

( 如果文章看的不是很清楚, 請參考影片中的說明, 我會一個一個說明 😎 )

說明 ondelete='set null' (預設行為)

如果直接刪除 sheet_id ( 底下有很多expense_line_ids), 可以成功刪除 sheet_id, 但你會發現

expense_line_ids 並沒有被刪除 ( sheet_id 變為 null).

說明 ondelete='cascade'

如果直接刪除 sheet_id ( 底下有很多expense_line_ids), 可以成功刪除 sheet_id, 且你會發現

expense_line_ids 也自動都被移除了.

說明 ondelete='restrict'

如果直接刪除 sheet_id ( 底下有很多expense_line_ids), 無法刪除 sheet_id,

alt tag

你必需先移除 sheet_id 底下的 expense_line_ids, 才可以刪除 sheet_id.

其實, 你可以把他們想成是 child 和 parent 的關係即可:smile:

要如何知道 fields 有 ondelete='....' 之類的特性呢:question:

除了可以透過 code 或 odoo 的 model fields 中查看之外,

alt tag

也可以利用查看 db table 的工具 (pgadmin4)

alt tag

odoo 手把手教學 - view parent 說明 - part26

在 view 中可以透過 parent 這個值, 拿到 parentfields 內容 (可能有點繞口:smile:)

不懂沒關係, 請看以下的說明:smile:

通常會使用在 view 中的 domain 或是 attrs,

首先, 先看一下 models/models.py

......
class DemoExpenseTutorial(models.Model):
    _name = 'demo.expense.tutorial'
    _description = 'Demo Expense Tutorial'
    _order = "sequence, id desc"

    name = fields.Char('Description', required=True)
    ......
    sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report", ondelete='restrict')

......

class DemoExpenseSheetTutorial(models.Model):
    _name = 'demo.expense.sheet.tutorial'
    _description = 'Demo Expense Sheet Tutorial'

    name = fields.Char('Expense Demo Report Summary', required=True)

    # One2many is a virtual relationship, there must be a Many2one field in the other_model,
    # and its name must be related_field
    expense_line_ids = fields.One2many(
        'demo.expense.tutorial', # related model
        'sheet_id', # field for "this" on related model
        string='Expense Lines')
......

接著看 views/view.xml

......
  <record id="view_form_demo_expense_sheet_tutorial" model="ir.ui.view">
    <field name="name">Demo Expense Sheet Tutorial Form</field>
    <field name="model">demo.expense.sheet.tutorial</field>
    <field name="arch" type="xml">
      <form string="Demo Expense Sheet Tutorial">
      ......
          <group>
            <field name="name"/>
          </group>
          <notebook>
              <page string="Expense">
                <field name="expense_line_ids" >
                  <!-- <tree> -->
                  <tree editable="top">   <!-- <<<<<<<<<<<< -->
                  <!-- <tree editable="bottom"> --> <!-- <<<<<<<<<<<< -->
                    <field name="name"/>
                    <field name="employee_id"/>
                    <field name="tag_ids" widget="many2many_tags" attrs="{'readonly': [('parent.name', '=', 'test-readonly')]}"/>
                  </tree>
                </field>
              </page>
          </notebook>
        </sheet>
      </form>
    </field>
  </record>
......

主要請看 <field name="tag_ids" widget="many2many_tags" attrs="{'readonly': [('parent.name', '=', 'test-readonly')]}"/>

當 sheet 的 nametest-readonly 的時候, tag_ids 這個 fields 會變成 readonly.

alt tag

請注意:exclamation: 我們並沒有 parent 這個欄位, 但是在 view 中可以透過這種方式使用 parent (也就是 sheet ) 的東西.

當 sheet 的 name 不是 test-readonly 時, tag_ids 這個 fields 會變成可以 edit ( 不是readonly ).

alt tag

另外一點要注意的是, 請搭配 <tree editable="top"><tree editable="bottom">, 單純使用 <tree> 不會生效:exclamation:

editable 的效果可參考之前的介紹 odoo 手把手教學 - One2many Editable Bottom and Top

odoo 手把手教學 - domain 搭配 fields 的三種用法 - part27

在 odoo 中 domain 幾乎無所不在:smile: 今天和大家介紹三種 domain 搭配 fields 的用法,

第一種 - 直接在 model 中的 fileds 定義

model/models.py

class DemoExpenseTutorial(models.Model):
    _name = 'demo.expense.tutorial'
    ......
    employee_id = fields.Many2one('hr.employee', string="Employee", required=True,
                domain=[('active', '=', True)] )
    ......

開啟 odoo12 如何開啟 odoo developer mode, 並且到 fields 上觀看, 會看到我們定義的 domain

alt tag

第二種 - 直接在 view 中的 fileds 定義

views/view.xml

  <record id="view_form_demo_expense_tutorial" model="ir.ui.view">
    <field name="name">Demo Expense Tutorial Form</field>
    <field name="model">demo.expense.tutorial</field>
    <field name="arch" type="xml">
      <form string="Demo Expense Tutorial">
        ......
            <field name="employee_id" domain="[('user_id', '=', user_id)]"/>
        ......
          </group>
        </sheet>
      </form>
    </field>
  </record>

開啟 odoo12 如何開啟 odoo developer mode, 並且到 fields 上觀看, 會看到我們定義的 domain

alt tag

❗這邊要注意的是, 如果第一種和第二種同時寫, 以第二種在 view 上定義的為主❗

第三種 - 透過 onchange 的方法增加 domain

這種方法蠻酷的, 所以我留到最後來講:smile:

首先, 如果不了解 onchange 可參考 介紹 model.

請看下面的範例 model/models.py

......
@api.onchange('user_id')
def onchange_user_id(self):
    # domain
    result = dict()
    result['domain'] = {
        'employee_id': [('user_id', '=', self.user_id.id)]
    }
    # equal
    # self.env['hr.employee'].search([('user_id', '=', self.user_id.id)])
    return result
......

當改變 user_id 時, 會增加對應的 domain, 需要回傳一個 dict,

這個 dict 包含 fields, 也就是 employee_id, 後面則是我們所需要的 domain.

odoo 手把手教學 - form_view_ref 以及 tree_view_ref 說明 - part28

還記得 odoo 手把手教學 - 同一個 model 使用不同的 view_ids - part10 這篇教學嗎:question:

那時候是使用 ir.actions.act_window 也就是 action 的方式定義不同的 view_ids,

今天如果想單獨針對 fields 定義 view 時, 就需要使用 form_view_ref tree_view_ref

使用方法也很簡單, 請參考 views/view.xml

  <record id="view_form_demo_expense_tutorial" model="ir.ui.view">
    <field name="name">Demo Expense Tutorial Form</field>
    <field name="model">demo.expense.tutorial</field>
    <field name="arch" type="xml">
      <form string="Demo Expense Tutorial">
        ......
            <field name="sheet_id" context="{'form_view_ref':'demo_expense_tutorial_v1.view_form_demo_expense_sheet_tutorial'}"/>
            <!-- <field name="sheet_id" context="{'form_view_ref':'demo_expense_tutorial_v1.custom_view_form_demo_sheet'}"/> -->

        ......
          </group>
        </sheet>
      </form>
    </field>
  </record>
  ......
   <record id="custom_view_form_demo_sheet" model="ir.ui.view">
    <field name="name">Custim Demo Sheet Form</field>
    <field name="model">demo.expense.sheet.tutorial</field>
    <field name="arch" type="xml">
      <form string="custom_view_form_demo_sheet">
        <sheet>
          ......
        </sheet>
      </form>
    </field>
  </record>

在需要的 fields 上, 加上 context="{'form_view_ref':......}", 然後再定義你的 view 即可,

tree_view_ref 也是一樣的概念:smile:

注意:exclamation:, 在這裡只要你有定義一個以上的 demo.expense.sheet.tutorial form view 時,

記得一定要使用 form_view_ref ( 否則它會自動選最後一個 ).

odoo 手把手教學 - Message Post 教學 - part29

可參考 models/models.py

......
@api.multi
def btn_message_post(self):
    for rec in self:
        if rec.user_id:
            rec.user_id.partner_id.message_post(body="test body", subject="test subject")
        else:
            raise UserError('請選擇使用者(user_id)')
......

透過 partner_id.message_post(....")

可以完成 Message Post, 資訊要到 Contacts (res.partner) 底下看,

alt tag

odoo 手把手教學 - groups 搭配 fields 用法 - part30

這邊的用法和 odoo 手把手教學 - domain 搭配 fields 的三種用法 - part27 是類似的,

只不過對象換成了 groups,

寫法如下, 請參考 models/models.py 中,

class DemoExpenseTutorial(models.Model):
    _name = 'demo.expense.tutorial'
    ......
    tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id',
        string='Tges', copy=False,
        groups='demo_expense_tutorial_v1.demo_expense_tutorial_group_manager'
    )
......

tag_ids field 增加了 groups='demo_expense_tutorial_v1.demo_expense_tutorial_group_manager',

代表的意思是只有 Manager 可以看到這個 field(擁有權限),

假如今天一個 User 權限的人, 不管在 tree 或是 form 都看不到 tag_ids.

但這不只是隱藏起來, 也就是說如果你強制去取值, 還是無法拿到資料的(因為權限不夠),

建議可以用 shell 模式下去嘗試取值.

然後另一種寫法如下,

請參考 views/view.xml,

  <record id="view_form_demo_expense_tutorial" model="ir.ui.view">
......
    <field name="tag_ids" widget="many2many_tags" groups="demo_expense_tutorial_v1.demo_expense_tutorial_group_manager"/>
......
  </record>

但這只是隱藏起來, 也就是說如果你強制去取值, 還是可以拿到資料的(沒有權限限制),

差別在於,

如果你是寫在 model, 會自動幫你產生到全部的 view 上(並且需要對應的權限).

如果你單獨寫在 view 上, 是針對個別的 view (像這邊就是只在 form 上) 生效,

(但不需要對應的權限).

odoo 手把手教學 - ACID transactions 說明 - part31

這邊先說結論, 下面會再說明, 如果你遵守 odoo 的 ORM,

你是不需要另外去處理 ACID transactions 的問題.

相關的 odoo source code 可參考 odoo/odoo/sql_db.py

class Cursor(object):
    """Represents an open transaction to the PostgreSQL DB backend,
       acting as a lightweight wrapper around psycopg2's
       ``cursor`` objects.

        ``Cursor`` is the object behind the ``cr`` variable used all
        over the OpenERP code.

        .. rubric:: Transaction Isolation
      ......

如果你不知道甚麼是 ACID, 可參考 Transaction 概念簡介.

這邊就用一個例子來說明 Atomicity (原子性),

可參考 models/models.py

......
@api.multi
def btn_test_acid_atomicity(self):
    for index in range(3):
        self.create({
            'name': index,
            'employee_id': 1
        })
        if index == 1:
            raise UserError('error - auto rollback')
......

嘗試上面的 code.

假設今天有3筆資料,

只有兩種結果, 3筆全都寫入成功, 或是3筆全都失敗未寫入,

不會有1筆成功寫入, 2筆失敗的狀況.

odoo 手把手教學 - 特殊 groups 應用說明 - part32

這邊介紹幾個比較特殊的 groups 給大家,

首先是 base.group_no_one,

它的 groups 定義在原始碼中的 /odoo/addons/base/security/base_groups.xml

......
<record model="res.groups" id="group_no_one">
    <field name="name">Technical Features</field>
</record>
......

這個 groups 只有在你打開 odoo developer mode 的時候才看的到.

接著是 base.group_erp_managerbase.group_system,

這些 groups 則是當你擁有 Access RightsSettings 權限的時候你才看的到.

它的 groups 定義在原始碼中的 /odoo/addons/base/security/base_groups.xml

......
<record model="res.groups" id="group_erp_manager">
    <field name="name">Access Rights</field>
</record>

<record model="res.groups" id="group_system">
    <field name="name">Settings</field>
    <field name="implied_ids" eval="[(4, ref('group_erp_manager'))]"/>
    <field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
......

當然除了使用 groups 定義之外, 也可以直接指定 user,

像是 base.user_admin 就是只有 admin user 才看的到.

它的定義在原始碼中的 /odoo/addons/base/data/res_users_data.xml

......

<!-- user 2 is the human admin user -->
<record id="user_admin" model="res.users">
    <field name="login">admin</field>
    <field name="password">admin</field>
    <field name="partner_id" ref="base.partner_admin"/>
    <field name="company_id" ref="main_company"/>
    <field name="company_ids" eval="[(4, ref('main_company'))]"/>
    <field name="groups_id" eval="[(6,0,[])]"/>
    <field name="signature"><![CDATA[<span>-- <br/>
Administrator</span>]]></field>
</record>
......

使用方法其實之前都說明過了, 可參考 views/view.xml

  <record id="view_form_demo_expense_tutorial" model="ir.ui.view">
    <field name="name">Demo Expense Tutorial Form</field>
    <field name="model">demo.expense.tutorial</field>
    ......
            <field name="debug_field" groups="base.group_no_one"/>
            <field name="admin_field" groups="base.user_admin"/>
    ......

基本上原生的都是定義在原始碼中的 odoo/addons/base/security/base_groups.xml

像是基本的 user groups 種類,

......
<record model="res.groups" id="group_user">
    <field name="name">Internal User</field>
</record>

......

<record id="group_portal" model="res.groups">
    <field name="name">Portal</field>
    ......
</record>

......

<record id="group_public" model="res.groups">
    <field name="name">Public</field>
    ......
</record>

......

透過定義 user 以及 groups, 可以組合出更靈活的架構.