Author: |
My Company |
License: |
no license |
Branch: |
14.0 |
Repository: |
twtrubiks/odoo-demo-addons-tutorial |
Dependencies: |
hr_contract |
Languages: |
Markdown (724, 70.4%),
Python (95, 9.2%),
and
XML (209, 20.3%) |
Other branches: |
master |
<h1>odoo 入門篇</h1>
<p>建議觀看影片, 會更清楚:smile:</p>
<ul>
<li><p><a href="https://youtu.be/vb_Z8KCI-wk">Youtube Tutorial - odoo 手把手教學 - Many2one - part1</a> - <a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_expense_tutorial_v1#odoo-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E5%AD%B8---many2one---part1">文章快速連結</a></p></li>
<li><p><a href="https://youtu.be/QeZfJqTGP-w">Youtube Tutorial - odoo 手把手教學 - Many2many - part2</a> - <a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_expense_tutorial_v1#odoo-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E5%AD%B8---many2many---part2">文章快速連結</a></p></li>
<li><p><a href="https://youtu.be/WiLdXP781N0">Youtube Tutorial - odoo 手把手教學 - One2many - part3</a> - <a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_expense_tutorial_v1#odoo-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E5%AD%B8---one2many---part3">文章快速連結</a></p></li>
<li><p><a href="https://youtu.be/HJcBAFXQYVc">Youtube Tutorial - odoo 手把手教學 - One2many Editable Bottom and Top - part3-1</a> - <a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_expense_tutorial_v1#odoo-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E5%AD%B8---one2many-editable-bottom-and-top---part3-1">文章快速連結</a></p></li>
<li><p><a href="https://youtu.be/zcWMs16p9Xw">Youtube Tutorial - odoo 手把手教學 - Search Filters - part4</a> - <a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_expense_tutorial_v1#odoo-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E5%AD%B8---search-filters---part4">文章快速連結</a></p></li>
<li><p><a href="https://youtu.be/twn6zz3OeRs">Youtube Tutorial - odoo 手把手教學 - 說明 noupdate 以及 domain_force - part5</a> - <a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_expense_tutorial_v1#odoo-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E5%AD%B8---%E8%AA%AA%E6%98%8E-noupdate-%E4%BB%A5%E5%8F%8A-domain_force---part5">文章快速連結</a></p></li>
<li><p><a href="https://youtu.be/URxuH2HG44Q">Youtube Tutorial - odoo 手把手教學 - 如何透過 button 呼叫 view, form - part6</a> - <a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_expense_tutorial_v1#odoo-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E5%AD%B8---%E5%A6%82%E4%BD%95%E9%80%8F%E9%81%8E-button-%E5%91%BC%E5%8F%AB-view-form---part6">文章快速連結</a></p></li>
<li><p><a href="https://youtu.be/g-dclCkwY5c">Youtube Tutorial - odoo 手把手教學 - 說明 name<em>get 和 _name</em>search - part7</a> - <a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_expense_tutorial_v1#odoo-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E5%AD%B8---%E8%AA%AA%E6%98%8E-name_get-%E5%92%8C-_name_search---part7">文章快速連結</a></p></li>
<li><p><a href="https://youtu.be/GBCGS2znnT8">Youtube Tutorial - odoo 手把手教學 - 使用 python 增加取代 One2many M2X record - part8</a> - <a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_expense_tutorial_v1#odoo-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E5%AD%B8---%E4%BD%BF%E7%94%A8-python-%E5%A2%9E%E5%8A%A0%E5%8F%96%E4%BB%A3-one2many-m2x-record---part8">文章快速連結</a></p></li>
<li><p><a href="https://youtu.be/0fpA89QcYZM">Youtube Tutorial - odoo 手把手教學 - tree create delete edit False - part9</a> - <a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_expense_tutorial_v1#odoo-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E5%AD%B8---tree-create-delete-edit-false---part9">文章快速連結</a></p></li>
<li><p><a href="https://youtu.be/thhdGK9oebg">Youtube Tutorial - odoo14 手把手教學 - Active Archive Ribbon 教學 - part10</a> - <a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/14.0/demo_expense_tutorial_v1#odoo14-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E5%AD%B8---active-archive-ribbon-%E6%95%99%E5%AD%B8---part10">文章快速連結</a></p></li>
<li><p><a href="https://youtu.be/tZ6_2Q3r3Ok">Youtube Tutorial - odoo14 手把手教學 - Search Panel 教學 - part11</a> - <a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/14.0/demo_expense_tutorial_v1#odoo14-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E5%AD%B8---search-panel-%E6%95%99%E5%AD%B8---part11">文章快速連結</a></p></li>
<li><p><a href="https://youtu.be/OOlPZETkYKw">Youtube Tutorial - odoo14 手把手教學 - auto_join 說明 - part12</a> - <a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/14.0/demo_expense_tutorial_v1#odoo14-%E6%89%8B%E6%8A%8A%E6%89%8B%E6%95%99%E5%AD%B8---auto_join-%E8%AA%AA%E6%98%8E">文章快速連結</a></p></li>
</ul>
<p>建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)</p>
<p><a href="https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial">odoo 手把手建立第一個 addons</a></p>
<p>這篇主要介紹 Many2one, Many2many, One2many 這三個東西,</p>
<p>以下將介紹這個 addons 的結構</p>
<h2>說明</h2>
<h3>odoo 手把手教學 - Many2one - part1</h3>
<ul>
<li><a href="https://youtu.be/vb_Z8KCI-wk">Youtube Tutorial - odoo 手把手教學 - Many2one - part1</a></li>
</ul>
<p>先來看 <a href="models/models.py">models/models.py</a></p>
<p><code>Many2one</code></p>
<p>```python
......
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'</p>
<pre><code>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)
</code></pre>
<p>......
```</p>
<p><img src="https://i.imgur.com/lBj9pgz.png" alt="alt tag"></p>
<p>一個 <code>hr.employee</code> 可以對到很多個 <code>demo.expense.tutorial</code>,</p>
<p>所以是 多(<code>demo.expense.tutorial</code>) 對 一(<code>hr.employee</code>) 的關係,</p>
<p>來看 db 中的狀況</p>
<p><code>demo_expense_tutorial</code> 會多出一個欄位 ( 對應 <code>hr_employee</code> 的 id )</p>
<p><img src="https://i.imgur.com/8BVrrJa.png" alt="alt tag"></p>
<p><code>user_id</code> field 中的 <code>default=lambda self: self.env.user</code> 代表預設的值會設定當前登入的 user</p>
<p><img src="https://i.imgur.com/QiDj6iM.png" alt="alt tag"></p>
<p>因為 One2many 比較特別, 所以我們先介紹 Many2many:laughing:</p>
<h3>odoo 手把手教學 - Many2many - part2</h3>
<p><code>Many2many</code></p>
<ul>
<li><a href="https://youtu.be/QeZfJqTGP-w">Youtube Tutorial - odoo 手把手教學 - Many2many - part2</a></li>
</ul>
<p>要建立 Many2many 之前, 一定要先定義一個 model,</p>
<p>先定義 DemoTag (也請記得設定 <a href="security/ir.model.access.csv">security/ir.model.access.csv</a> )</p>
<p><a href="models/models.py">models/models.py</a></p>
<p>```python
......
class DemoTag(models.Model):
_name = 'demo.tag'
_description = 'Demo Tags'</p>
<pre><code>name = fields.Char(string='Tag Name', index=True, required=True)
active = fields.Boolean(default=True, help="Set active.")
</code></pre>
<p>......
```</p>
<p>然後接著到底下 <a href="models/models.py">models/models.py</a></p>
<p>```python
......
class DemoExpenseTutorial(models.Model):
<em>name = 'demo.expense.tutorial'
......
# https://www.odoo.com/documentation/12.0/reference/orm.html#odoo.fields.Many2many
# Many2many(comodel</em>name=<object object>, relation=<object object>, column1=<object object>, column2=<object object>, string=<object object>, **kwargs)
#
# relation: database table name
#</p>
<pre><code># 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.
</code></pre>
<h3>odoo 手把手教學 - Many2many - part2</h3>
<pre><code>tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id', string='Tges')
</code></pre>
<p>......
```</p>
<p>Many2many 比較多欄位, 我來說明一下,</p>
<p><code>comodel_name</code> 為 <code>demo.tag</code> (需要對應的 model)</p>
<p><code>relation</code> 為 <code>demo_expense_tag</code> (table 名稱),</p>
<p>Many2many 會多出一個 table, 這邊是針對 table 命名,</p>
<p>也就是 db 中的 table 名稱,</p>
<p><img src="https://i.imgur.com/B7ren2r.png" alt="alt tag"></p>
<p>如果你沒填 <code>relation</code> 這個值, 預設的 table 名稱會是 model名稱 + comodel<em>name + `</em>rel`,</p>
<p>所以也就會是 <code>demo_expense_tutorial_demo_tag_rel</code>.</p>
<p><code>column1</code> 為 <code>demo_expense_id</code>, <code>demo.expense.tutorial</code> table 中對應的 id.</p>
<p><code>column2</code> 為 <code>tag_id</code>, <code>demo.tag</code> table 中對應的 id.</p>
<p>繼續看 <a href="models/models.py">models/models.py</a></p>
<p><code>python
......
# Related (Reference) fields (不會存在 db)
# readonly default 為 True
# store default 為 False
gender = fields.Selection('Gender', related='employee_id.gender')
......
</code></p>
<p><code>fields.Selection</code> 就只是下拉選單而已, 比較特別的是 <code>related</code> 這個,</p>
<p><code>related='employee_id.gender'</code> 這邊的意思是, 會自己去找 employee_id 中的 gender,</p>
<p>到 employee 中找到 gender 為 Male</p>
<p><img src="https://i.imgur.com/GDOC0hS.png" alt="alt tag"></p>
<p>DemoExpenseTutorial 中的 <code>gender</code> 自然會是 Male,</p>
<p><img src="https://i.imgur.com/mJrzLjk.png" alt="alt tag"></p>
<p>但要注意幾件事情,</p>
<p><code>related</code> 預設的 field 是不會儲存在 db 中的, store default 為 False,</p>
<p>你在 table 中是找不到 <code>gender</code> field 的 (如下圖),</p>
<p>如果你想要儲存在 db 中的, 請另外設定 <code>store=Ture</code>,</p>
<p><img src="https://i.imgur.com/0Xcvzas.png" alt="alt tag"></p>
<p>然後 readonly default 為 True, 也就是說你是不可以去修改的,</p>
<p>( 如果要修改請去 employee 中找到 gender 修改 )</p>
<p><img src="https://i.imgur.com/mJrzLjk.png" alt="alt tag"></p>
<p>接著來看最後一個</p>
<h3>odoo 手把手教學 - One2many - part3</h3>
<p><code>One2many</code></p>
<ul>
<li><a href="https://youtu.be/WiLdXP781N0">Youtube Tutorial - odoo 手把手教學 - One2many - part3</a></li>
</ul>
<p><img src="https://i.imgur.com/lV2J3Tu.png" alt="alt tag"></p>
<p><a href="models/models.py">models/models.py</a></p>
<p>一個 <code>demo.expense.sheet.tutorial</code> 可以對應很多個 <code>demo.expense.tutorial</code></p>
<p>所以是 一(<code>demo.expense.sheet.tutorial</code>) 對 多(<code>demo.expense.tutorial</code>) 的關係,</p>
<p>```python
......
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'</p>
<pre><code>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')
</code></pre>
<p>......
```</p>
<p>說明 expense<em>line</em>ids 裡面的參數意義,</p>
<p><code>demo.expense.tutorial</code> 代表關連的 model (必填)</p>
<p><code>sheet_id</code> 代表所關連 model 的 field (必填)</p>
<p>也就是說如果你要建立 One2many, 一定也要有一個 Many2one,</p>
<p>但如果建立 Many2one 則不一定要建立 One2many.</p>
<p>One2many 是一個虛擬的欄位, 你在資料庫中是看不到它的存在(如下圖)</p>
<p><img src="https://i.imgur.com/F6YTOdq.png" alt="alt tag"></p>
<p>你只會看到 Many2one 中的 sheet_id</p>
<p><img src="https://i.imgur.com/lsiLpZK.png" alt="alt tag"></p>
<p><a href="models/models.py">models/models.py</a>, <code>demo.expense.tutorial</code> 中的 sheet_id</p>
<p><code>python
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
......
sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report")
......
</code></p>
<p>記得也要設定對應的 <a href="security/ir.model.access.csv">security/ir.model.access.csv</a> 和 <a href="security/security.xml">security/security.xml</a>.</p>
<p><a href="views/view.xml">views/view.xml</a></p>
<p>```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"/>
......</p>
<p>```</p>
<p>在 odoo 中很有多 widget, 大家可以改成其他的 widget 試試看, 像是 many2many_tags 的 widget</p>
<p><img src="https://i.imgur.com/UBYyUcf.png" alt="alt tag"></p>
<p><a href="views/view.xml">views/view.xml</a></p>
<p><code>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>
......
</code></p>
<p>在 <code>view_form_demo_expense_sheet_tutorial</code> 裡的 One2many 中的 expense<em>line</em>ids fields,</p>
<p>就把需要的欄位填進去即可,</p>
<p><img src="https://i.imgur.com/jiFHHST.png" alt="alt tag"></p>
<h3>odoo 手把手教學 - One2many Editable Bottom and Top - part3-1</h3>
<p>這邊補充一下 One2many 中的 Editable Bottom 和 Top</p>
<ul>
<li><a href="https://youtu.be/HJcBAFXQYVc">Youtube Tutorial - odoo 手把手教學 - One2many Editable Bottom and Top - part3-1</a></li>
</ul>
<p><a href="views/view.xml">views/view.xml</a></p>
<p><code>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>
</code></p>
<p>如果你加上 <code>editable</code> 這個參數, 當你新增 record 的時候, 就不會整個跳出視窗, 可以直接在裡面輸入</p>
<p>(或許比較好看:smile:)</p>
<p><img src="https://i.imgur.com/tdues3g.png" alt="alt tag"></p>
<p>至於 <code>editable="bottom"</code> 和 <code>editable="top"</code> 的差別如下</p>
<p><code>editable="top"</code> 一個新增的 record 會顯示在最上面</p>
<p><img src="https://i.imgur.com/qWaIH59.png" alt="alt tag"></p>
<p><code>editable="bottom"</code>一個新增的 record 會顯示在最下面</p>
<p><img src="https://i.imgur.com/d3pfgRX.png" alt="alt tag"></p>
<h3>odoo 手把手教學 - Search Filters - part4</h3>
<p>接著來看 filter 的功能</p>
<ul>
<li><a href="https://youtu.be/zcWMs16p9Xw">Youtube Tutorial - odoo 手把手教學 - Search Filters - part4</a></li>
</ul>
<p><code>xml
......
<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"/>
<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>
......
</code></p>
<p><code><field name="name" string="Name"/></code> 主要是在 tree 中搜尋</p>
<p><img src="https://i.imgur.com/eBmc2Je.png" alt="alt tag"></p>
<p><code><filter string="Sheet" name="sheet" domain="[]" context="{'group_by': 'sheet_id'}"/></code></p>
<p><code><filter string="Employee" name="employee" domain="[]" context="{'group_by': 'employee_id'}"/></code></p>
<p>依照特定的 fields 分組</p>
<p><img src="https://i.imgur.com/jojaYtz.png" alt="alt tag"></p>
<p>點選後的狀態</p>
<p><img src="https://i.imgur.com/acyqVIA.png" alt="alt tag"></p>
<h3>odoo 手把手教學 - 說明 noupdate 以及 domain_force - part5</h3>
<p>再來看看</p>
<p><a href="security/ir_rule.xml">security/ir_rule.xml</a></p>
<ul>
<li><a href="https://youtu.be/twn6zz3OeRs">Youtube Tutorial - odoo 手把手教學 - 說明 noupdate 以及 domain_force - part5</a></li>
</ul>
<p>```xml
......
<data noupdate="1"></p>
<pre><code> <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>
</code></pre>
<p>......
```</p>
<p><code>noupdate="1"</code>的意思為當更新 addons 時, 是不是允許重新 import data,</p>
<p><code>noupdate="1"</code></p>
<p>假如我們在安裝完 addons 之後, 去刪除 record data, 然後再重新去更新 addons,</p>
<p>你會發現你刪除的 data 並沒有被安裝回來 (只能先移除 addons 再重新安裝).</p>
<p><code>noupdate="0"</code></p>
<p>假如我們在安裝完 addons 之後, 去刪除 record data, 然後再重新去更新 addons,</p>
<p>你會發現你刪除的 data 會被安裝回來.</p>
<p><code>id="ir_rule_demo_expense_user"</code> 第一段為針對 <code>demo_expense_tutorial_group_user</code></p>
<p>限制 <code>domain_force</code>, 規則很簡單, 這類的 user 只能看到自己的單子, 也就是</p>
<p><code>[('employee_id.user_id.id', '=', user.id)]</code>.</p>
<p><code>id="ir_rule_demo_expense_manager"</code> 針對 <code>demo_expense_tutorial_group_manager</code></p>
<p>限制 <code>domain_force</code>, 這邊比較特別 <code>[(1, '=', 1)]</code>, 代表沒有限制, 也就是全部的單子都</p>
<p>可以看到.</p>
<p><code>demo</code> 用戶為 User, 所以只能看到自己的單子</p>
<p><img src="https://i.imgur.com/dX9QuMj.png" alt="alt tag"></p>
<p><code>Admin</code> 用戶為 Manager, 所以能看到全部的單子</p>
<p><img src="https://i.imgur.com/CFMsgie.png" alt="alt tag"></p>
<h3>odoo 手把手教學 - 如何透過 button 呼叫 view, form - part6</h3>
<p>接下來介紹前面跳過的部份, 也就是透過 button 的方式呼叫 view, form,</p>
<ul>
<li><a href="https://youtu.be/URxuH2HG44Q">Youtube Tutorial - odoo 手把手教學 - 如何透過 button 呼叫 view, form - part6</a></li>
</ul>
<p><a href="models/models.py">models/models.py</a></p>
<p>```python
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
......</p>
<pre><code>@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'
}
</code></pre>
<p>```</p>
<p>透過前端呼叫 <code>button_sheet_id</code>, 會回傳屬於它的 sheet_id</p>
<p><img src="https://i.imgur.com/gUUgk9g.png" alt="alt tag"></p>
<p>點進去會直接進入 sheet 中的 form</p>
<p><img src="https://i.imgur.com/lU6P9Oj.png" alt="alt tag"></p>
<p><a href="views/view.xml">views/view.xml</a></p>
<p>前端的部份就是呼叫 <code>button_sheet_id</code></p>
<p><code>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">
<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>
</code></p>
<p>既然找了 sheet<em>id, 也來做一個反查回來的, 也就是透過 sheet</em>id 找到 <code>demo.expense.tutorial</code>,</p>
<p><a href="models/models.py">models/models.py</a></p>
<p>```python
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'</p>
<pre><code>......
@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)],
}
......
</code></pre>
<p>```</p>
<p><code>res_model</code> 為目標的 model <code>demo.expense.tutorial</code>.</p>
<p><code>domain</code> 稍微說明一下 <code>[('sheet_id', '=', self.id)],</code>,</p>
<p><code>sheet_id</code> 是指目標 model <code>demo.expense.tutorial</code> 的 sheet_id,</p>
<p><code>self.id</code> 是指當下 model <code>demo.expense.sheet.tutorial</code> 的 id.</p>
<p><img src="https://i.imgur.com/oqDOi3p.png" alt="alt tag"></p>
<p>點下去會帶出它的 <code>demo.expense.tutorial</code></p>
<p><img src="https://i.imgur.com/2ZSl9Qj.png" alt="alt tag"></p>
<p><a href="views/view.xml">views/view.xml</a> 的部份</p>
<p><code>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>
</code></p>
<h3>odoo 手把手教學 - 說明 name<em>get 和 _name</em>search - part7</h3>
<p>最後來看 <a href="models/models.py">models/models.py</a> 中比較特殊的部份,</p>
<ul>
<li><a href="https://youtu.be/g-dclCkwY5c">Youtube Tutorial - odoo 手把手教學 - 說明 name<em>get 和 _name</em>search - part7</a></li>
</ul>
<p>分別是 <code>name_get</code> 和 <code>_name_search</code>,</p>
<p>```python
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'</p>
<pre><code>......
@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()
</code></pre>
<p>```</p>
<p>首先是 <code>name_get</code></p>
<p>這個的功能主要是去修改 name 的名稱, 在這邊我們加上當下的時間</p>
<p>(可以依照自己的需求下去修改)</p>
<p><img src="https://i.imgur.com/JudV8pW.png" alt="alt tag"></p>
<p>Many2one 時也會看到自己定義的 <code>name_get</code></p>
<p>注意:exclamation: 這些增加的值是不會儲存進 db 中的, db 中還是儲存的是 name 的內容而已
(概念和 compute field 一樣:smile:)</p>
<p><img src="https://i.imgur.com/sC9hNA8.png" alt="alt tag"></p>
<p>再來要來說明 <code>_name_search</code>,</p>
<p>如果沒有它, 假設我知道某個資料的 id 是 4, 在搜尋的地方打上 id,</p>
<p>你會發現找不到資料:joy:</p>
<p><img src="https://i.imgur.com/YokDfBf.png" alt="alt tag"></p>
<p>但今天如果有了 <code>_name_search</code> 並實作它,</p>
<p>你會發現這次你打 id 會才成功找到需要的資料:satisfied:</p>
<p>我在 code 中有放幾個範例註解, 大家可以自行玩玩看:smile:</p>
<p><img src="https://i.imgur.com/ztUL9Xd.png" alt="alt tag"></p>
<h3>odoo 手把手教學 - 使用 python 增加取代 One2many M2X record - part8</h3>
<ul>
<li><a href="https://youtu.be/GBCGS2znnT8">Youtube Tutorial - odoo 手把手教學 - 使用 python 增加取代 One2many M2X record - part8</a></li>
</ul>
<p>參考 <a href="models/models.py">models/models.py</a></p>
<p>這邊只需要注意3個 function,</p>
<p><code>add_demo_expense_record</code> <code>link_demo_expense_record</code> <code>replace_demo_expense_record</code></p>
<p>分別對應的 button 為下圖</p>
<p>參考 <a href="views/view.xml">views/view.xml</a></p>
<p><img src="https://i.imgur.com/8gmMe3j.png" alt="alt tag"></p>
<p>```python
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'</p>
<pre><code>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])]
</code></pre>
<p>```</p>
<p>說明 <code>add_demo_expense_record</code></p>
<p>```python
......
@api.multi
def add<em>demo</em>expense_record(self):
# (0, _ , {'field': value}) creates a new record and links it to this one.</p>
<pre><code>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)]
</code></pre>
<p>......
```</p>
<p><code>(0, _ , {'field': value})</code> 新建一筆 record 並且連接它.</p>
<p><code>self.env.ref(......)</code> 這個的用法是去取得既有的資料, 路徑在 <a href="data/demo_expense_tutorial_data.xml">data/demo<em>expense</em>tutorial_data.xml</a>.</p>
<p>當你點選按鈕, 下面就會一直新增資料</p>
<p><img src="https://i.imgur.com/bUI3vZE.png" alt="alt tag"></p>
<p>說明 <code>link_demo_expense_record</code></p>
<p>```python
......
@api.multi
def link<em>demo</em>expense_record(self):
# (4, id, _) links an already existing record.</p>
<pre><code>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)]
</code></pre>
<p>......</p>
<p>```</p>
<p><code>(4, id, _)</code> 連接已經存在的 record.</p>
<p>當你點選按鈕, 下面會直接連接一比資料, 如果已經連接就不會有動作,</p>
<p><img src="https://i.imgur.com/Qw1VDyU.png" alt="alt tag"></p>
<p>說明 <code>replace_demo_expense_record</code></p>
<p>```python
......
@api.multi
def replace<em>demo</em>expense_record(self):
# (6, _, [ids]) replaces the list of linked records with the provided list.</p>
<pre><code>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])]
</code></pre>
<p>......
```</p>
<p><code>(6, _, [ids])</code> 使用 list 取代既有的 records.</p>
<p>當你點選按鈕, 會使用你定義的 list 取代全部的 records.</p>
<p><img src="https://i.imgur.com/b30QdSQ.png" alt="alt tag"></p>
<h3>odoo 手把手教學 - tree create delete edit False - part9</h3>
<ul>
<li><a href="https://youtu.be/0fpA89QcYZM">Youtube Tutorial - odoo 手把手教學 - tree create delete edit False - part9</a></li>
</ul>
<p>通常管理一個使用者可不可以建立 records, 是根據 security 資料夾裡面的檔案,</p>
<p>也就是 <code>security.xml</code> <code>ir_rule.xml</code> <code>ir.model.access.csv</code> 主要是這個.</p>
<p>記住:exclamation: odoo 可以從 model 層(db層) 或權限下手, 也可以從 view 那層下手,</p>
<p>當然, 如果是從安全性的角度來看 從 model 層(db層) 或權限下手 是比較高全的:smile:</p>
<p>今天就是要來介紹 從 view 那層下手,</p>
<p>增加一個 tree <a href="views/view.xml">views/view.xml</a></p>
<p><code>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>
......
</code></p>
<p>重點在 <code><tree string="no_create_tree" create="0" delete="false" edit="1" editable="top"></code></p>
<p>這段, 裡面增加了一下 tag, 允許就是 <code>1</code> 或 <code>True</code>, 不允許就是 <code>0</code> 或 <code>False</code>.</p>
<p>儘管你有權限建立 records, 如果你設定了 <code>create="0"</code>, 你還是沒辦法建立 records.</p>
<p>也記得在 <a href="views/menu.xml">views/menu.xml</a> 增加 action,</p>
<p>並且要指定 <code>view_id</code> (也就是剛剛建立出來的那個)</p>
<p><code>xml
......
<!-- 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>
......
</code></p>
<p>你會發現 create delete 的按鈕都消失了</p>
<p><img src="https://i.imgur.com/siLhdQ4.png" alt="alt tag"></p>
<h3>odoo14 手把手教學 - Active Archive Ribbon 教學 - part10</h3>
<ul>
<li><a href="https://youtu.be/thhdGK9oebg">Youtube Tutorial - odoo14 手把手教學 - Active Archive Ribbon 教學 - part10</a></li>
</ul>
<p>在 odoo 中, <code>active</code> 這個字段是有特殊意義的,</p>
<p>一般來說, 當你進入 form 的 view 底下, 你會看到這個 action</p>
<p><img src="https://i.imgur.com/bjFJDPJ.png" alt="alt tag"></p>
<p>如果你在 model 中有了 <code>active</code> fields, 而且有將他放在該 form 底下(儘管你是隱藏)</p>
<p>在 odoo14 中, 會自動出現 Archive 的選項</p>
<p><img src="https://i.imgur.com/GIsrvd2.png" alt="alt tag"></p>
<p>然後, 你也可以再加一個 odoo14 中的 Ribbon</p>
<p><img src="https://i.imgur.com/ovupHAk.png" alt="alt tag"></p>
<p>寫法如下, 可參考 <a href="views/view.xml">views/view.xml</a></p>
<p><code>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">
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
......
<group>
......
<field name="active" invisible="1"/>
</group>
</sheet>
</form>
</field>
</record>
</code></p>
<h3>odoo14 手把手教學 - Search Panel 教學 - part11</h3>
<ul>
<li><a href="https://youtu.be/tZ6_2Q3r3Ok">Youtube Tutorial - odoo14 手把手教學 - Search Panel 教學 - part11</a></li>
</ul>
<p>在 odoo14 中, 原生就多了 Search Panel 可以使用, 今天就來看看這個東西:smile:</p>
<p>下圖就是所謂的 Search Panel</p>
<p><img src="https://i.imgur.com/V5YA2wJ.png" alt="alt tag"></p>
<p>寫法也不難, 就是在 search 底下再加上 searchpanel, 可參考 <a href="views/view.xml">views/view.xml</a></p>
<p><code>xml
......
<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">
......
<searchpanel>
<field name="employee_id" select="multi" icon="fa-building" enable_counters="1"/>
<field name="sheet_id" icon="fa-users" enable_counters="1"/>
<field name="gender" icon="fa-cutlery" enable_counters="1" color="#d10202"/>
<!-- error example -->
<!-- <field name="tag_ids" icon="fa-users"/> -->
</searchpanel>
</search>
</field>
</record>
......
</code></p>
<p><code>select="multi"</code> 是否可以多選.</p>
<p><code>icon="fa-building"</code> icon 圖示顯示.</p>
<p><code>enable_counters="1"</code> 是否顯示數量.</p>
<p><code>color="#d10202"</code> color 顯示.</p>
<p>另外, 註解的部份只是和大家說明, many2many 是不支援的, 在 odoo14 中, 錯誤訊息愈來愈清楚了,</p>
<p>如果你把註解打開, 更新後你會看到這個錯誤訊息</p>
<p><img src="https://i.imgur.com/QWQ8Z76.png" alt="alt tag"></p>
<p>另外, 放在 searchpanel 裡的東西必須是 <code>store=True</code>, 否則也會錯誤.(但 odoo 也會提醒你)</p>
<p>可參考 <a href="models/models.py">models/models.py</a></p>
<p><code>python
gender = fields.Selection(string='Gender', related='employee_id.gender', store=True)
</code></p>
<h3>odoo14 手把手教學 - auto_join 說明</h3>
<ul>
<li><a href="https://youtu.be/OOlPZETkYKw">Youtube Tutorial - odoo14 手把手教學 - auto_join 說明</a></li>
</ul>
<p>今天要來介紹在 <code>Many2one</code> <code>One2many</code> <code>Many2many</code> 中有個參數是 <code>auto_join</code>,</p>
<p>預設都是 False, 可參考 soucecode 中的 <code>odoo/fields.py</code></p>
<p>```python</p>
<p>class Many2one(<em>Relational):
......
type = 'many2one'
column</em>type = ('int4', 'int4')</p>
<pre><code>ondelete = None # what to do when value is deleted
auto_join = False # whether joins are generated upon search
delegate = False # whether self implements delegation
</code></pre>
<p>......
```</p>
<p><code>auto_join</code> 的主要功能就是允許 ORM 使用 join 的方式撈資料(用的好的話效能會更好:smile:)</p>
<p>使用方法也很簡單, 直接加上 <code>auto_join=True</code> 即可</p>
<p>```python
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
_order = "sequence, id desc"</p>
<pre><code>name = fields.Char('Description', required=True)
sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report", ondelete="cascade", auto_join=True)
</code></pre>
<p>......</p>
<p>class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'</p>
<pre><code>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')
</code></pre>
<p>.....
```</p>
<p>至於要怎麼樣看到 <code>auto_join</code> 的變化, 就需要從 RAW SQL下去觀察:exclamation:</p>
<p>可參考之前的教學 <a href="https://github.com/twtrubiks/odoo-docker-tutorial#odoo---%E5%A6%82%E4%BD%95%E9%80%8F%E9%81%8E-log_level-%E4%BA%86%E8%A7%A3-orm-raw-sql">odoo 手把手教學 - 如何透過 log_level 了解 ORM RAW SQL</a></p>
<p>現在我們先來看 <code>auto_join=False</code> 的情況</p>
<p><code>python
sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report", ondelete="cascade", auto_join=False)
</code></p>
<p>使用 shell 執行</p>
<p><code>pythom
self.env['demo.expense.tutorial'].search([('sheet_id.name', '=', '111')])
</code></p>
<p>你可以發現他是使用 Subquery 的方式下去撈資料</p>
<p><code>sql
SELECT "demo_expense_tutorial".id
FROM "demo_expense_tutorial"
WHERE (("demo_expense_tutorial"."active" = TRUE)
AND ("demo_expense_tutorial"."sheet_id" in
(SELECT "demo_expense_sheet_tutorial".id
FROM "demo_expense_sheet_tutorial"
WHERE ("demo_expense_sheet_tutorial"."name" = '111')
ORDER BY "demo_expense_sheet_tutorial"."id")))
ORDER BY "demo_expense_tutorial"."id"
</code></p>
<p>接著來看 <code>auto_join=True</code> 的情況</p>
<p><code>python
sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report", ondelete="cascade", auto_join=True)
</code></p>
<p>使用 shell 執行</p>
<p><code>pythom
self.env['demo.expense.tutorial'].search([('sheet_id.name', '=', '111')])
</code></p>
<p>你可以發現他是使用 Left join 的方式下去撈資料</p>
<p><code>sql
SELECT "demo_expense_tutorial".id
FROM "demo_expense_tutorial"
LEFT JOIN "demo_expense_sheet_tutorial" AS "sheet" ON ("demo_expense_tutorial"."sheet_id" = "sheet"."id")
WHERE (("demo_expense_tutorial"."active" = TRUE)
AND ("sheet"."name" = '111'))
ORDER BY "demo_expense_tutorial"."id"
</code></p>
<p>另外, 在 odoo12 的時候有相關的 issues 可參考 <a href="https://github.com/odoo/odoo/issues/25175">auto_join incorrect results...</a></p>
<p>簡單說就是在 odoo12 中的 <code>auto_join</code> 是使用 inner join 的方法, 而現在已經改成了 left join:smile:</p>
<p>在 odoo12 中如果你是用 <code>auto_join=True</code> 並且執行以下的 ORM 會有 bug</p>
<p><code>python
self.env['demo.expense.tutorial'].search(['|', ('sheet_id.name', '=', '111'), ('sheet_id', '=', False)])
</code></p>
<p>在 odoo12 中你會撈不出 <code>('sheet_id', '=', False)</code>, 原因是因為他是使用 inner join 的概念.</p>