Repository: twtrubiks/odoo-demo-addons-tutorial
Branch: master
Commit: 4f8cce99a613
Files: 197
Total size: 357.9 KB
Directory structure:
gitextract_x6lf3xkp/
├── .gitignore
├── README.md
├── demo_abstractmodel_tutorial/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── reports/
│ │ └── report.xml
│ └── wizard/
│ ├── __init__.py
│ ├── model_wizard.py
│ └── model_wizard.xml
├── demo_abstractmodel_v2_tutorial/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── models/
│ │ ├── __init__.py
│ │ ├── models.py
│ │ └── models_v2.py
│ ├── security/
│ │ └── ir.model.access.csv
│ └── views/
│ ├── menu.xml
│ └── view.xml
├── demo_actions_singleton/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── data/
│ │ └── action_data.xml
│ ├── models/
│ │ ├── __init__.py
│ │ └── models.py
│ ├── security/
│ │ ├── ir.model.access.csv
│ │ └── security.xml
│ └── views/
│ ├── menu.xml
│ └── view.xml
├── demo_activity/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── data/
│ │ └── mail_data.xml
│ ├── models/
│ │ ├── __init__.py
│ │ └── models.py
│ ├── security/
│ │ ├── ir.model.access.csv
│ │ └── security.xml
│ └── views/
│ ├── menu.xml
│ └── view.xml
├── demo_class_inheritance/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── model.py
│ └── views/
│ └── views.xml
├── demo_config_settings/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── models.py
│ ├── security/
│ │ └── security.xml
│ └── views/
│ └── view.xml
├── demo_datetime_tutorial/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── models.py
│ ├── security/
│ │ └── ir.model.access.csv
│ └── views/
│ ├── menu.xml
│ └── view.xml
├── demo_delegation_inheritance/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── model.py
│ ├── security/
│ │ ├── ir.model.access.csv
│ │ └── security.xml
│ └── views/
│ ├── menu.xml
│ └── view.xml
├── demo_expense_tutorial_v1/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── controllers/
│ │ ├── __init__.py
│ │ └── controllers.py
│ ├── data/
│ │ └── demo_expense_tutorial_data.xml
│ ├── demo/
│ │ └── demo.xml
│ ├── models/
│ │ ├── __init__.py
│ │ └── models.py
│ ├── security/
│ │ ├── ir.model.access.csv
│ │ ├── ir_rule.xml
│ │ └── security.xml
│ └── views/
│ ├── menu.xml
│ └── view.xml
├── demo_fields_view_get_tutorial/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ └── models/
│ ├── __init__.py
│ └── account_invoice.py
├── demo_hierarchy_tutorial/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── models.py
│ ├── security/
│ │ ├── ir.model.access.csv
│ │ └── security.xml
│ └── views/
│ ├── menu.xml
│ └── view.xml
├── demo_hook_tutorial/
│ ├── README.md
│ ├── __init__.py
│ └── __manifest__.py
├── demo_i18n_expense_tutorial/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ └── i18n/
│ └── zh_TW.po
├── demo_inherit_controller/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ └── controllers/
│ ├── __init__.py
│ └── portal.py
├── demo_multi_company/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── model.py
│ ├── security/
│ │ ├── ir.model.access.csv
│ │ └── security.xml
│ └── views/
│ ├── menu.xml
│ └── view.xml
├── demo_odoo_tutorial/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── controllers/
│ │ ├── __init__.py
│ │ └── controllers.py
│ ├── data/
│ │ └── data_demo_odoo.xml
│ ├── demo/
│ │ └── demo.xml
│ ├── models/
│ │ ├── __init__.py
│ │ └── models.py
│ ├── reports/
│ │ └── report.xml
│ ├── security/
│ │ ├── ir.model.access.csv
│ │ └── security.xml
│ ├── tests/
│ │ ├── __init__.py
│ │ ├── test_demo_odoo_singletransactioncase.py
│ │ └── test_demo_odoo_transactioncase.py
│ └── views/
│ ├── demo_odoo_template.xml
│ ├── menu.xml
│ └── view.xml
├── demo_odoo_tutorial_wizard/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── models.py
│ ├── security/
│ │ ├── ir.model.access.csv
│ │ └── security.xml
│ ├── views/
│ │ ├── menu.xml
│ │ └── view.xml
│ └── wizard/
│ ├── __init__.py
│ ├── model_wizard.py
│ └── model_wizard.xml
├── demo_orm_cache/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── model.py
│ ├── security/
│ │ ├── ir.model.access.csv
│ │ └── security.xml
│ └── views/
│ ├── menu.xml
│ └── view.xml
├── demo_prototype_inheritance/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── model.py
│ ├── security/
│ │ ├── ir.model.access.csv
│ │ └── security.xml
│ └── views/
│ ├── menu.xml
│ └── view.xml
├── demo_recruitment_website_form/
│ └── README.md
├── demo_sale_scan_barcode/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── models.py
│ └── views/
│ └── view.xml
├── demo_scheduler/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── models/
│ │ ├── __init__.py
│ │ └── scheduler.py
│ ├── security/
│ │ ├── ir.model.access.csv
│ │ └── security.xml
│ └── views/
│ └── scheduler.xml
├── demo_sequence/
│ ├── README.md
│ ├── __init__.py
│ ├── __manifest__.py
│ ├── data/
│ │ └── sequence_data.xml
│ ├── models/
│ │ ├── __init__.py
│ │ └── model.py
│ ├── security/
│ │ ├── ir.model.access.csv
│ │ └── security.xml
│ └── views/
│ ├── menu.xml
│ └── view.xml
├── domain_operator_tutorial/
│ └── README.md
├── odoo_domain_tutorial/
│ └── README.md
├── odoo_index_tutorial/
│ └── README.md
├── session_redis_tutorial/
│ ├── README.md
│ └── docker-compose.yml
└── xml-rpc-odoo/
├── README.md
└── demo.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
================================================
FILE: README.md
================================================
# odoo-demo-addons-tutorial-odoo-12
此版本為 odoo12,
odoo14 版本請參考 [odoo14](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/14.0) 分支.
odoo15 版本請參考 [odoo15](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/15.0) 分支.
odoo16 版本請參考 [odoo16](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/16.0) 分支.
odoo17 版本請參考 [odoo17](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/17.0) 分支.
odoo18 版本請參考 [odoo18](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/18.0) 分支.
odoo19 版本請參考 [odoo19](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/19.0) 分支.
本文章會持續更新 :smile:
這邊文章主要是會手把手教大家撰寫 odoo 的 addons, 建議再閱讀這篇文章之前, 你已經看過以下的文章
[odoo-development-environment-tutorial](https://github.com/twtrubiks/odoo-development-environment-tutorial) - 建立 odoo 開發環境 ( source code )
[odoo-docker-tutorial](https://github.com/twtrubiks/odoo-docker-tutorial) - 利用 docker 快速建立 odoo 環境
## 前言
為甚麼我要寫一堆 addons, 因為其實 odoo 和 django 一樣的點是都很麻煩, 要寫個範例超級麻煩的,
因為一個小地方錯可能就會造成錯誤之類的 :sweat:
## addons 目錄
非常建議按照順序看, 因為會一步一步帶大家 :smile:
1. [odoo 手把手建立第一個 addons](demo_odoo_tutorial)
2. [odoo 入門篇](demo_expense_tutorial_v1)
3. [odoo 繼承 - class inheritance](demo_class_inheritance)
4. [odoo 繼承 - prototype inheritance](demo_prototype_inheritance)
5. [odoo 繼承 - delegation inheritance](demo_delegation_inheritance)
6. [odoo 觀念 - actions 和 singleton](demo_actions_singleton)
7. [odoo 觀念 - scheduler](demo_scheduler)
8. [odoo 觀念 - sequence](demo_sequence)
9. [odoo 觀念 - activity](demo_activity)
10. [odoo 觀念 - TransientModel-Wizard](demo_odoo_tutorial_wizard)
11. [odoo 觀念 - AbstractModel](demo_abstractmodel_tutorial)
12. [odoo 觀念 - 實作 config settings](demo_config_settings)
13. [odoo 觀念 - datetime 教學](demo_datetime_tutorial)
14. [odoo 觀念 - 實作 scan barcode](demo_sale_scan_barcode)
15. [odoo 觀念 - 實作 hierarchy](demo_hierarchy_tutorial)
16. [odoo 觀念 - 如何使用 python xmlrpc 連接 odoo](xml-rpc-odoo)
17. [odoo 觀念 - Translating 翻譯教學 i18n](demo_i18n_expense_tutorial)
18. [odoo 觀念 - recruitment_website_form 介紹](demo_recruitment_website_form)
19. [odoo 觀念 - 實作 init hook](demo_hook_tutorial)
20. [odoo 教學 - 如何繼承 inherit controller](demo_inherit_controller)
21. [odoo 教學 - fields_view_get 介紹教學](demo_fields_view_get_tutorial)
22. [odoo 教學 - multi company](demo_multi_company)
23. [odoo 教學 - testing 教學](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#odoo-testing-%E6%95%99%E5%AD%B8)
24. [odoo 觀念 - orm cache 說明](demo_orm_cache)
25. [odoo 觀念 - 使用 RAW SQL 說明](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#%E4%BD%BF%E7%94%A8-raw-sql-%E8%AA%AA%E6%98%8E)
26. [odoo 14 觀念 - image mixin 教學](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/14.0/demo_image_mixin)
27. [odoo 14 觀念 - Active Archive Ribbon 教學](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)
28. [odoo 14 觀念 - Search Panel 教學](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)
29. [odoo domain 教學](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/odoo_domain_tutorial)
30. [odoo domain operator 教學](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/domain_operator_tutorial)
31. [odoo index 教學](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/odoo_index_tutorial)
32. [odoo 觀念 - odoo12 和 odoo14 的 ORM Write 差異](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/14.0/odoo_write_tutorial)
33. [odoo 14 教學 - 透過 controller 建立簡單 api](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/14.0/demo_controller_json)
34. [odoo 教學 - 透過 AbstractModel 擴充 Model](demo_abstractmodel_v2_tutorial)
35. [odoo 教學 - odoo session_redis 教學](session_redis_tutorial)
36. [Odoo 15 中的 LISTEN/NOTIFY 運作原理](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/15.0/odoo-pg-listen-notify)
37. [Odoo 15 建立簡易 REST API](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/15.0/demo_controller_api)
38. [odoo 18 OWL 範例 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/18.0/demo_owl_tutorial)
## 其他
* [Youtube Tutorial - 使用 CLI 安裝,更新 addons](https://youtu.be/k19N2x8f4gw)
建立 addons 模組
```cmd
./odoo-bin scaffold your_addons_name my-addons/
```
在介紹如何透過 cli 安裝 addons 之前, 請先知道一件事情,
就是你可以選擇將指令全部放到 cli 中, 或是在 `odoo.conf` 設定,
像是如果有設定 `odoo.conf`
```cmd
[options]
......
db_user = odoo
db_password = odoo
db_port = 5432
```
這樣我們直接執行以下指令即可
```cmd
python3 odoo-bin -d odoo -c /home/twtrubiks/work/odoo12/odoo/config/odoo.conf
```
如果你沒有設定 `odoo.conf` , 也可以在 cli 中設定
```cmd
python3 odoo-bin -r odoo -w odoo -d odoo -c /home/twtrubiks/work/odoo12/odoo/config/odoo.conf
```
`-r` 代表 db_user. `-w` 代表 db_password. `-d` 代表指定 database.
安裝 addons
```cmd
python3 odoo-bin -i addons_1 -d odoo
```
更新 addons
```cmd
python3 odoo-bin -u addons_1 -d odoo
```
也可以一次更新或安裝多個 addons
```cmd
python3 odoo-bin -u addons_1,addons_2 -d odoo
```
例外還有比較進階的用法 `--dev`
```cmd
python3 odoo-bin -u addons_1 -d odoo --dev=all
```
`--dev=all` 代表全部都啟用.
`--dev=xml` 代表當 xml 改變的時候, 會自動幫你更新(不用手動更新).
`--dev=reload` 代表當 python code 改變時, 自動更新(不用手動更新).
但有時候如果你覺得怪怪的, 我還是建議手動重新直接更新 addons 的指令比較好 :smile:
注意 :exclamation: 沒有刪除 addons 的指令, 只能從 web 上移除.
### shell
* [Youtube Tutorial - odoo shell 基本教學 - CRUD](https://youtu.be/kmbiT54hUkw)
```cmd
python odoo-bin shell -w odoo -r odoo -d odoo --db_port=5432 --db_host=localhost --addons-path='/home/twtrubiks/odoo/addons'
```
如果有很多路徑請使用 `,` 隔開
```cmd
--addons-path='/home/twtrubiks/odoo/addons,/home/twtrubiks/odoo/addons2'
```
`search`
```python
>>> self.env['res.partner'].search([])
res.partner(14, 26, 33, 27, 10, 35, 18, 19, 11, 20, 22, 31, 23, 15, 34, 12, 21, 25, 37, 24, 36, 30, 38, 13, 29, 28, 9, 17, 32, 16, 1, 39, 40, 8, 7, 3)
>>> self.env['res.partner'].search([('name', 'like', 'kim')])
res.partner(24,)
>>> self.env['res.partner'].browse([11, 20])
res.partner(11, 20)
```
* [Youtube Tutorial - odoo shell orm 基本教學 - search_read](https://youtu.be/AzGnFX4pHWI)
`search_read`
通常比較常使用在 js 呼叫 odoo 或是第三方呼叫 odoo api,
```python
>>> self.env['hr.expense'].search_read([], ['id', 'employee_id'])
[{'id': 4, 'employee_id': (7, 'Marc Demo')}, {'id': 3, 'employee_id': (7, 'Marc Demo')}, {'id': 2, 'employee_id': (1, 'Mitchell Admin')}, {'id': 1, 'employee_id': (1, 'Mitchell Admin')}]
>>> self.env['hr.expense'].search_read([('employee_id', '=', 1)], ['id', 'name', 'employee_id'])
[{'id': 2, 'name': 'Hotel Expenses', 'employee_id': (1, 'Mitchell Admin')}, {'id': 1, 'name': 'Travel by Air', 'employee_id': (1, 'Mitchell Admin')}]
```
* [Youtube Tutorial - odoo orm group 基本教學 - read_group](https://youtu.be/ALq6CcADygs)
`read_group`
通常使用在 SQL 中的 GROUP BY (很適合拿來處理比較大的資料, 效能應該也會比較好 :smile: ).
read_group 的定義可參考原始碼中的 `odoo/models.py`
```python
......
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
"""
Get the list of records in list view grouped by the given ``groupby`` fields
:param domain: list specifying search criteria [['field_name', 'operator', 'value'], ...]
:param list fields: list of fields present in the list view specified on the object.
Each element is either 'field' (field name, using the default aggregation),
or 'field:agg' (aggregate field with aggregation function 'agg'),
or 'name:agg(field)' (aggregate field with 'agg' and return it as 'name').
The possible aggregation functions are the ones provided by PostgreSQL
(https://www.postgresql.org/docs/current/static/functions-aggregate.html)
and 'count_distinct', with the expected meaning.
:param list groupby: list of groupby descriptions by which the records will be grouped.
A groupby description is either a field (then it will be grouped by that field)
or a string 'field:groupby_function'. Right now, the only functions supported
are 'day', 'week', 'month', 'quarter' or 'year', and they only make sense for
date/datetime fields.
......
```
比較特別要注意的地方是 fields, groupby, lazy 這幾個欄位 (請參考註解說明 :smile: ).
如果你想參考寫法, 建議參考 odoo14 的, odoo12 也可以使用, 但是有些寫法比較舊了.
這邊使用 `sale.order` 當作範例,
假設想要得到每個 partner_id 的平均 amount_total,
```python
self.env['sale.order'].read_group([], ['partner_id', 'amount_total:avg'], ['partner_id'])
```

同等如下 SQL
```sql
SELECT partner_id, avg(amount_total)
FROM sale_order
GROUP BY partner_id;
```
注意 :exclamation: :exclamation: 這邊 field 的格式為 `field:agg`.
agg 代表 aggregate, odoo 的 orm 是有支援的, 更多詳細可參考 [postgresql functions-aggregate](https://www.postgresql.org/docs/current/functions-aggregate.html).
假設想要得到每個 partner_id 的平均 amount_total 以及 總和 amount_total,
```python
self.env['sale.order'].read_group([], ['partner_id', 'total:sum(amount_total)', 'avg_total:avg(amount_total)'], ['partner_id'])
```

同等如下 SQL
```sql
SELECT partner_id, avg(amount_total), sum(amount_total)
FROM sale_order
GROUP BY partner_id;
```
注意 :exclamation: :exclamation:這邊的 fields 的格式為 `name:agg(field)`
(因為是相同的 fields 名稱, 如果使用前一種寫法會錯誤)
如果想要分的更細, 甚至可以再加上 fields, 這邊增加一個狀態
```python
self.env['sale.order'].read_group([], ['partner_id', 'total:sum(amount_total)', 'avg_total:avg(amount_total)'], ['partner_id', 'state'], lazy=False)
```

同等如下 SQL
```sql
SELECT partner_id, state, avg(amount_total), sum(amount_total)
FROM sale_order
GROUP BY partner_id, state;
```
`lazy` 這個參數預設為 True, 也就代表只會拿第一個 field 下去分組,
如果設定為 False, 就會把全部你所指定的 fields 都拿進去分組.
根據 date_order 下去分組
```python
self.env['sale.order'].read_group([], ['total:sum(amount_total)'], ['date_order:month'])
```
同等如下 SQL
```sql
SELECT DATE_TRUNC('month', date_order),
sum(amount_total)
FROM sale_order
GROUP BY DATE_TRUNC('month', date_order);
```
`day` `week` `month` `quarter` `year` 這些都是可用的參數.

`search_count`
```python
>>> self.env['res.partner'].search_count([])
73
```
`recordset.ids` 回傳 recordset 全部的 id
```python
>>> recordset = self.env['res.partner'].search([])
>>> recordset.ids
[14, 26, 33, 27, 10, 35, 18, 19, 11, 20, 22, 31, 23, 15, 34, 12, 21, 25, 37, 24, 36, 30, 38, 13, 29, 28, 9, 17, 32, 16, 1, 39, 40, 8, 7, 3]
```
繼續使用上面的範例
`recordset.filtered(func)` 和 python 中的 [filter](https://github.com/twtrubiks/python-notes/blob/master/filter.py) 類似
```python
>>> recordset.filtered(lambda r: r.name.startswith('C'))
res.partner(33, 39)
```
`recordset.mapped(func)` 和 python 中的 [map](https://github.com/twtrubiks/python-notes/blob/master/map_tutorial.py) 類似
```python
>>> recordset.mapped('name')
['Azure Interior', 'Brandon Freeman', 'Colleen Diaz', 'Nicole Ford', 'Deco Addict', 'Addison Olson', 'Douglas Fletcher', 'Floyd Steward',...
```
`recordset.sorted(func)` 和 python 中的 [sorted](https://github.com/twtrubiks/python-notes/blob/master/sorted.py) 類似
```python
>>> recordset.sorted(key=lambda r: r.id, reverse=True)
res.partner(40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, ...
```
`create`
```python
>>> partner = self.env['res.partner']
>>> partner.create({'name': 'twtrubiks', 'is_company': True})
res.partner(66)
>>> self.env.cr.commit() # 需要特別執行這行才會寫進資料庫中
```
`write`
update data
```python
>>> partner = self.env['res.partner'].browse([2])
>>> partner
res.partner(2,)
>>> partner.name
'OdooBot'
>>> partner.write({'name': 'hello'})
True
>>> partner.name
'hello'
>>> self.env.cr.commit() # 需要特別執行這行才會寫進資料庫中
```
當你更新 `One2many` 和 `Many2many` 時, 要使用比較特別的語言,
我之後會補充上來.
`copy`
如果 fields 有定義 `copy=False`, 就沒有辦法複製.
```python
# odoo/addons/base/data/res_users_demo.xml
>>> demo = self.env.ref('base.user_demo')
>>> demo.copy({'name': 'twtrubiks', 'login': 'twtrubiks', 'email':''})
>>> self.env.cr.commit() # 需要特別執行這行才會寫進資料庫中
```
`delete`
```python
>>> user = self.env['res.users'].browse([3])
>>> user.unlink()
2020-06-21 06:45:51,958 19735 INFO odoo odoo.models.unlink: User #1 deleted ir.model.data records with IDs: [1884]
2020-06-21 06:45:51,996 19735 INFO odoo odoo.models.unlink: User #1 deleted res.users records with IDs: [3]
True
>>> self.env.cr.commit() # 需要特別執行這行才會寫進資料庫中
```
`sudo`
* [Youtube Tutorial - odoo 基本教學 - sudo](https://youtu.be/nAmNmPCSbGg)
可參考 odoo 原始碼的 `odoo/models.py`
```python
def sudo(self, user=SUPERUSER_ID):
""" sudo([user=SUPERUSER])
Returns a new version of this recordset attached to the provided
user.
By default this returns a ``SUPERUSER`` recordset, where access
control and record rules are bypassed.
.. note::
Using ``sudo`` could cause data access to cross the
boundaries of record rules, possibly mixing records that
are meant to be isolated (e.g. records from different
companies in multi-company environments).
It may lead to un-intuitive results in methods which select one
record among many - for example getting the default company, or
selecting a Bill of Materials.
.. note::
Because the record rules and access control will have to be
re-evaluated, the new recordset will not benefit from the current
environment's data cache, so later data access may incur extra
delays while re-fetching from the database.
The returned recordset has the same prefetch object as ``self``.
"""
return self.with_env(self.env(user=user))
```
`sudo([user=SUPERUSER])` 如果裡面沒有填入 user id, 預設就是使用 SUPERUSER, 如果
有帶入 user id, 就是使用指定的 user 的權限.
注意 :exclamation: 這是 odoo12 的作法,
從 odoo13 開始切換 user 已經改成 `with_user(user)` :exclamation: 可參考 [odoo13-souece code](https://github.com/odoo/odoo/blob/13.0/odoo/models.py#L5160)
`deprecated use of sudo(user), use with_user(user) instead`
來看下面這個例子,
因為沒有指定 user id, 所以是使用 SUPERUSER, 自然可以看到全部的 records,
```python
>>> self.env['hr.expense'].sudo().search([])
hr.expense(4, 3, 2, 1)
```
再來看這個例子, user_id = 6 只能看到自己的 records, 因為他是一般的 user,
```python
>>> self.env['hr.expense'].sudo(user=6).search([])
hr.expense(4, 3)
```
也就是說, 知道這個特性, 我們甚至可以讓沒有權限的人看到 records (請依照自己的需求去調整) :smile:
另外提醒一下, 這個 sudo 除了在 ORM 底下生效外, 在 QWeb 中也會生效, 如下方這段 code 是可行的,
```xml
......
| Email |
|
......
```
`with_context`
可參考 odoo 原始碼的 `odoo/models.py`
```python
def with_context(self, *args, **kwargs):
""" with_context([context][, **overrides]) -> records
Returns a new version of this recordset attached to an extended
context.
The extended context is either the provided ``context`` in which
``overrides`` are merged or the *current* context in which
``overrides`` are merged e.g.::
# current context is {'key1': True}
r2 = records.with_context({}, key2=True)
# -> r2._context is {'key2': True}
r2 = records.with_context(key2=True)
# -> r2._context is {'key1': True, 'key2': True}
.. note:
The returned recordset has the same prefetch object as ``self``.
"""
context = dict(args[0] if args else self._context, **kwargs)
return self.with_env(self.env(context=context))
```
`with_context` 可以用在很多地方, 這邊用一個翻譯的舉例, 如果我同時有 `en_US` 和 `zh_TW`
這兩個語言, 可以使用 `with_context`帶入不同的語言, 會自動依照語言進行翻譯,
```python
>>> self.env['product.product'].with_context(lang='zh_TW').browse(41).name
'飛機票'
>>> self.env['product.product'].with_context(lang='en_US').browse(41).name
'Air Flight'
```
`with_context` 也常使用在傳值中, 可參考 [odoo 觀念-TransientModel-Wizard](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial_wizard)
### odoo shell 注意事項
* [Youtube Tutorial - odoo shell 教學 - 注意事項](https://youtu.be/YS6mGE3-y1k)
odoo-shell 下 command 無法 save 問題,
當使用 Odoo Shell 測試資料時, 會發現當我們下了指令時, db 裡面的值沒有改變,
這時候必須另外執行
```cmd
self.env.cr.commit()
```
在非 Odoo Shell 中會自動執行, 在 Odoo Shell 中不會自動執行 (需要手動執行).
除非你不想要把修改資料寫進去資料庫.
## odoo log 說明
```log
2019-12-05 03:04:15,734 1 INFO localhost werkzeug: 172.18.0.1 - - [05/Dec/2019 03:04:15] "POST /longpolling/poll HTTP/1.0" 200 - {query_count} {query_time} {remaining_time}
```
query_count = query 次數
query_time = query 時間
remaining_time = 剩餘時間
如何透過 elk 搭配 odoo, 請參考 [docker-elk-tutorial 7.6.0](https://github.com/twtrubiks/docker-elk-tutorial/tree/elk-7.6.0)
## odoo 使用 gmail 發信
* [Youtube Tutorial - odoo 教學 - 使用 gmail 發信](https://youtu.be/CkFHCQuzEoo)
gmail 需要一些前製作業, 建議先閱讀 [使用 Gmail 寄信 - 前置作業](https://github.com/twtrubiks/Flask-Mail-example#%E4%BD%BF%E7%94%A8-gmail-%E5%AF%84%E4%BF%A1---%E5%89%8D%E7%BD%AE%E4%BD%9C%E6%A5%AD) 這篇的 gmail 設定
Technical -> Email -> Outgoing Mail Servers

SMTP Server 填入 `smtp.gmail.com`
SMTP Port 填入 `465`
Connection Security 填入 `SSL/TLS`
填入自己的 Username 和 Password

建議輸入資料後, 可以先點選測試連接 (以下是成功的畫面)

如果出現錯誤, 請確認你的帳密是否有錯誤

接著可以使用 odoo 內的 email 測試看是否可以成功發信

成功發信

## 如何全域修改時間日期格式
路徑為 Translations -> Languages, 點選語言, 就會看到以下的畫面,
圖片下方有一些參數的說明(可自行依照需求調整)

## 其他注意事項
`odoo.conf` 中的 `data_dir` 參數建立好了就不要亂改,
因為亂改動可能會導致你的 odoo 打開時一片空白或是破圖的狀況.
```conf
[options]
......
data_dir = /home/twtrubiks/work/odoo12/odoo-data
```
另外如果你的 odoo 不知道甚麼原因導致破圖(非上述的狀況),
錯誤訊息通常可能是遺失 filestore, 這時候可以嘗試以下的幾個方法,
可以試試看更新 odoo 中的 `base`,
或是從 db 中刪除 `ir_attachment` table,
重新使用 debug mode 中的 Regenerate Assets Bundles.
(assets 這個的功能是刪除舊的 css 和 js, 然後重新產生新的, 有時遇到 assets 快取的問題, 可以選這個選項)

## 建議使用繼承 addons 的方式修改 odoo
[Youtube Tutorial - odoo 教學 - 建議使用繼承 addons 的方式修改 odoo](https://youtu.be/Yncbx95YT1Q)
這邊提醒大家, 建議在修改 odoo 的時候, 儘量使用 addons 繼承的方式去修改 code,
原因是維護性的問題, 原生的 code 保持乾淨,
雖然用 odoo developer mode 可以很快的修改 view,
但是 :exclamation: :exclamation:
只要你一更新你修改的那個 addons, 就會自動還原 :exclamation: :exclamation:
這邊使用 `hr_expense` 舉的例子,
我透過 Edit View 修改了 view,

當你保存是會生效的.
可是當你去更新 `hr_expense` 的時候, 你會發生他被還原了.
所以, 使用 Edit View 選項去修改 view 可以使用在測試時.
正式的修改, 還是推薦使用 addons 繼承的方式 :smile:
## Donation
文章都是我自己研究內化後原創,如果有幫助到您,也想鼓勵我的話,歡迎請我喝一杯咖啡 :laughing:
綠界科技ECPAY ( 不需註冊會員 )

[贊助者付款](http://bit.ly/2F7Jrha)
歐付寶 ( 需註冊會員 )

[贊助者付款](https://payment.opay.tw/Broadcaster/Donate/9E47FDEF85ABE383A0F5FC6A218606F8)
## 贊助名單
[贊助名單](https://github.com/twtrubiks/Thank-you-for-donate)
## License
MIT license
================================================
FILE: demo_abstractmodel_tutorial/README.md
================================================
# 介紹 AbstractModel
建議觀看影片, 會更清楚 :smile:
[Youtube Tutorial - odoo 手把手教學 - AbstractModel](https://youtu.be/jsMTVe12vRY)
建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)
[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)
主要介紹 demo_abstractmodel_tutorial
## 說明
`AbstractModel` AbstractModel = BaseModel,
注意 :exclamation: :exclamation: AbstractModel **不會** 在資料庫中產生對應的 table.
AbstractModel 除了常常使用在之前介紹的 [demo_prototype_inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_prototype_inheritance) 中,
也很常使用在 report 中 (自定義一些額外的邏輯),
可參考 odoo code 中的 `addons/sale/report/sale_report.py`,
```python
......
class SaleOrderReportProforma(models.AbstractModel):
_name = 'report.sale.report_saleproforma'
_description = 'Proforma Report'
def _get_report_values(self, docids, data=None):
docs = self.env['sale.order'].browse(docids)
return {
'doc_ids': docs.ids,
'doc_model': 'sale.order',
'docs': docs,
'proforma': True
}
```
假如你的 report 有額外的邏輯, 可以將邏輯寫在 `_get_report_values` 中.
請一定要先了解 Transient Model, 如果不了解可參考 [demo_odoo_tutorial_wizard](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial_wizard).
開始今天的介紹 :smile:
先來看 [wizard/model_wizard.py](wizard/model_wizard.py)
```python
class ReportWizard(models.TransientModel):
_name = 'report.wizard'
_description = "Report Wizard"
date_start = fields.Date(string="Start Date", required=True, default=fields.Date.today)
date_end = fields.Date(string="End Date", required=True, default=fields.Date.today)
@api.multi
def download_report(self):
_logger.warning('=== CALL get_report ===')
data = {
'ids': self.ids,
'model': self._name,
'form': {
'date_start': self.date_start,
'date_end': self.date_end,
},
}
return self.env.ref('demo_abstractmodel_tutorial.action_report_abstractmodel').report_action(self, data=data)
......
```
頁面上會有一個按鈕觸發 `download_report`,
`demo_abstractmodel_tutorial.action_report_abstractmodel` 為
addons name + report id (report id 後面會說明) `addons_name.report_id`
`report_action()` 會去 call `_get_report_values()`.
[wizard/model_wizard.xml](wizard/model_wizard.xml)
```xml
Custom Report
report.wizard
form
```
這邊定義了基本的 form, 並且將 menu 設定在 `hr_expense.menu_hr_expense_reports` 之下.


剛剛前面提到 report id `action_report_abstractmodel` 在 [reports/report.xml](reports/report.xml)
```xml
```
分別設定了 template id `report_wizard_template` 以及 report id `action_report_abstractmodel`.
`name` 的部份為 adddons name + template id.
也就是 `adddons_name.template_id`.
接著看 [wizard/model_wizard.py](wizard/model_wizard.py) 的後半段,
```python
......
class ReportExpenseAbstractModel(models.AbstractModel):
_name = 'report.demo_abstractmodel_tutorial.report_wizard_template'
_description = 'Report Expense Wizard'
@api.model
def _get_report_values(self, docids, data=None):
_logger.warning('=== CALL get_report_values ===')
date_start = data['form']['date_start']
date_end = data['form']['date_end']
docs = self.env['hr.expense'].search([
('date', '>=', date_start),
('date', '<=', date_end)], order='date asc')
return {
'doc_ids': data['ids'],
'doc_model': data['model'],
'date_start': date_start,
'date_end': date_end,
'docs': docs,
}
```
前面說過了 `report_action()` 會去 call `_get_report_values()`,
所以這邊定義了 `AbstractModel` 並且實作 `_get_report_values`.
`_name` 這邊的比較特別, 要注意一下, 它的結構是由以下幾部份組成,
report + addons name + template id
`report.addons_name.template_id`
也就是 `report.demo_abstractmodel_tutorial.report_wizard_template`,
report 這個 prefix 很重要, 請不要任意的拿掉 :exclamation: :exclamation:
`_get_report_values` 則是我們額外的邏輯, 最後將資料回傳給
`report_wizard_template` 並且 render.

================================================
FILE: demo_abstractmodel_tutorial/__init__.py
================================================
from . import wizard
================================================
FILE: demo_abstractmodel_tutorial/__manifest__.py
================================================
{
'name': "demo_abstractmodel_tutorial",
'summary': """
AbstractModel report
""",
'description': """
AbstractModel report
""",
'author': "My Company",
'website': "http://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['hr_expense'],
# always loaded
'data': [
'wizard/model_wizard.xml',
'reports/report.xml'
],
'application': True,
}
================================================
FILE: demo_abstractmodel_tutorial/reports/report.xml
================================================
================================================
FILE: demo_abstractmodel_tutorial/wizard/__init__.py
================================================
from . import model_wizard
================================================
FILE: demo_abstractmodel_tutorial/wizard/model_wizard.py
================================================
from odoo import api, fields, models
import logging
_logger = logging.getLogger(__name__)
class ReportWizard(models.TransientModel):
_name = 'report.wizard'
_description = "Report Wizard"
date_start = fields.Date(string="Start Date", required=True, default=fields.Date.today)
date_end = fields.Date(string="End Date", required=True, default=fields.Date.today)
@api.multi
def download_report(self):
_logger.warning('=== CALL get_report ===')
data = {
'ids': self.ids,
'model': self._name,
'form': {
'date_start': self.date_start,
'date_end': self.date_end,
},
}
return self.env.ref('demo_abstractmodel_tutorial.action_report_abstractmodel').report_action(self, data=data)
class ReportExpenseAbstractModel(models.AbstractModel):
_name = 'report.demo_abstractmodel_tutorial.report_wizard_template'
_description = 'Report Expense Wizard'
@api.model
def _get_report_values(self, docids, data=None):
_logger.warning('=== CALL get_report_values ===')
date_start = data['form']['date_start']
date_end = data['form']['date_end']
docs = self.env['hr.expense'].search([
('date', '>=', date_start),
('date', '<=', date_end)], order='date asc')
return {
'doc_ids': data['ids'],
'doc_model': data['model'],
'date_start': date_start,
'date_end': date_end,
'docs': docs,
}
================================================
FILE: demo_abstractmodel_tutorial/wizard/model_wizard.xml
================================================
Custom Report
report.wizard
form
================================================
FILE: demo_abstractmodel_v2_tutorial/README.md
================================================
# 透過 AbstractModel 擴充 Model
建議觀看影片, 會更清楚 :smile:
[Youtube Tutorial - odoo 手把手教學 - 透過 AbstractModel 擴充 Model](https://youtu.be/uW1PsDPcJF4)
之前有介紹過 AbstractModel 的文章
* [介紹 AbstractModel](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_abstractmodel_tutorial) 搭配 report 使用
* [odoo 繼承 - prototype inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_prototype_inheritance) 有提到 MailThread 這個 AbstractModel
今天要來進一步介紹, 如何透過 AbstractModel 擴充 Model :smile:
## 說明
首先, 再提一次
`AbstractModel` AbstractModel = BaseModel,
注意 :exclamation: :exclamation: AbstractModel **不會** 在資料庫中產生對應的 table.
先來看這個範例 [models/models.py](models/models.py)
```python
from odoo import models, fields, api
class DemoMixin(models.AbstractModel):
_name = 'demo.mixin'
_description = 'Demo Mixin'
test_1 = fields.Float(
string='test_1',
default='2.2'
)
test_2 = fields.Float(
string="test_2",
compute="_compute_field",
)
def _compute_field(self):
for record in self:
record.test_2 = 3.0
class DemoModelTutorial(models.Model):
_name = 'demo.model.tutorial'
_inherit = 'demo.mixin'
_description = 'Demo Model Tutorial'
name = fields.Char(required=True, string="名稱")
```
這邊 `demo.model.tutorial` 繼承了 `demo.mixin`, 所以在 db table 中,
會看到 `demo.mixin` 中的 fields.

這邊稍微注意注意一下,
在 db 中只會有 `demo.model.tutorial` 的 table, 不會有 `demo.mixin` 的 table,
但是會有 `demo.mixin` 中的 fields, 也看不到 `test_2` fields, 原因是他是 _compute_field,
如果你想要看到包含 `test_2` fields, 可以到 odoo 的 model 後台觀看

`demo.mixin` 的 model 在 odoo 的後台也可以觀看 (但 db 中不會出現)


在 tree, form ...... 都可以使用 `demo.mixin` 的 fields

因為這個範例剛好只有一個 model 被繼承, 如果有兩個以上的 model 就更適合這樣寫了, 如下
```python
......
class DemoModelTutorial(models.Model):
_name = 'demo.model.tutorial'
_inherit = 'demo.mixin'
_description = 'Demo Model Tutorial'
......
class DemoModelTutorial_v2(models.Model):
_name = 'demo.model.tutorial.v2'
_inherit = 'demo.mixin'
_description = 'Demo Model Tutorial v2'
......
class DemoModelTutorial_v3(models.Model):
_name = 'demo.model.tutorial.v3'
_inherit = 'demo.mixin'
_description = 'Demo Model Tutorial v3'
......
```
這樣每個 model, 都會擁有 `demo.mixin` 的 fields, 不需要把重複的 code
在每個 model 中都寫一遍.
剛剛介紹的 model 是我們新建立的, 假如今天有一個 model 已經存在了,
想要用同樣的方式擴充 model, 可參考 [models/models_v2.py](models/models_v2.py)
```python
from odoo import models, fields, api
class DemoMixin2(models.AbstractModel):
_name = 'demo.mixin2'
_description = 'Demo Mixin2'
test_v2 = fields.Float(
string='test_v2',
default='2.2'
)
class DemoModelTutorial(models.Model):
_name = 'demo.model.tutorial'
_inherit = ['demo.model.tutorial', 'demo.mixin2']
pass
```
這篇文章其實就是將 [odoo 實作 scan barcode](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_sale_scan_barcode) 的概念再說一次.
也可以去了解一下 [什麼是 Mixin in python](https://github.com/twtrubiks/python-notes/tree/master/what_is_the_mixin),
相信這樣大家會更了解他們的概念 :smile:
================================================
FILE: demo_abstractmodel_v2_tutorial/__init__.py
================================================
from . import models
================================================
FILE: demo_abstractmodel_v2_tutorial/__manifest__.py
================================================
{
'name': "demo_abstractmodel_v2_tutorial",
'summary': """
AbstractModel Extend Model
""",
'description': """
AbstractModel Extend Model
""",
'author': "My Company",
'website': "http://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base'],
# always loaded
'data': [
'security/ir.model.access.csv',
'views/menu.xml',
'views/view.xml',
],
'application': True,
}
================================================
FILE: demo_abstractmodel_v2_tutorial/models/__init__.py
================================================
from . import models
# from . import models_v2
================================================
FILE: demo_abstractmodel_v2_tutorial/models/models.py
================================================
from odoo import models, fields, api
class DemoMixin(models.AbstractModel):
_name = 'demo.mixin'
_description = 'Demo Mixin'
test_1 = fields.Float(
string='test_1',
default='2.2'
)
test_2 = fields.Float(
string="test_2",
compute="_compute_field",
)
def _compute_field(self):
for record in self:
record.test_2 = 3.0
class DemoModelTutorial(models.Model):
_name = 'demo.model.tutorial'
_inherit = 'demo.mixin'
_description = 'Demo Model Tutorial'
name = fields.Char(required=True, string="名稱")
================================================
FILE: demo_abstractmodel_v2_tutorial/models/models_v2.py
================================================
from odoo import models, fields, api
class DemoMixin2(models.AbstractModel):
_name = 'demo.mixin2'
_description = 'Demo Mixin2'
test_v2 = fields.Float(
string='test_v2',
default='2.2'
)
class DemoModelTutorial(models.Model):
_name = 'demo.model.tutorial'
_inherit = ['demo.model.tutorial', 'demo.mixin2']
pass
================================================
FILE: demo_abstractmodel_v2_tutorial/security/ir.model.access.csv
================================================
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_demo_model_tutorial_user,Demo Model Tutorial Access,model_demo_model_tutorial,,1,1,1,1
================================================
FILE: demo_abstractmodel_v2_tutorial/views/menu.xml
================================================
================================================
FILE: demo_abstractmodel_v2_tutorial/views/view.xml
================================================
Demo Model Tutorial Form
demo.model.tutorial
Demo Model Tutorial List
demo.model.tutorial
================================================
FILE: demo_actions_singleton/README.md
================================================
# odoo 觀念 - actions 和 singleton
建議觀看影片, 會更清楚 :smile:
[Youtube Tutorial - odoo 手把手教學 - actions and singleton](https://youtu.be/rRD9j4IAHWY)
建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)
[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)
本篇主要介紹 actions and singleton
以下將介紹這個 addons 的結構
## 說明
[data/action_data.xml](data/action_data.xml)
首先, 先來建立一個 action
```xml
......
Action Demo
code
records.action_demo()
Action Other Demo
code
raise Warning('Hello')
```
`binding_model_id` 綁定 model (可以綁定和 `model_id` 不同的 model).
`state` 選擇使用的方式, 這邊使用 python code.
`code` 執行的程式碼, `records` 代表所選的 record, `action_demo()` 代表呼叫的 function.
這邊的設定也可以在 Technical -> Actions -> Server Actions 看到 (如下)

這邊就是剛剛 code 的設定

這邊的 Action To Do 是使用 Execute Python Code, 還有其他的選擇

記得也要將它加入 `__manifest__.py`.
先來看 [models/models.py](models/models.py)
```python
......
class DemoActionsSingleton(models.Model):
_name = 'demo.actions.singleton'
_description = 'Demo Actions Singleton'
name = fields.Char('Description', required=True)
@api.multi
def action_demo(self):
self.ensure_one()
_logger.warning('=== CALL action_demo ===')
```
`action_demo` 裡面就只是單純的 print.
至於要在 odoo 中的那邊呼叫 Action Demo, 請看下圖,
在 record 中的 action

當你點下去, 會觸發你的 logger

接下來說說 `self.ensure_one()`, 這就是確認是否為 `singleton`,
假如跳出 `raise ValueError exception`, 代表它非為 singleton
舉個例子, 像這邊選 兩條 record, 點下 Action Demo,

你會發現跳出 error

原因就是在這邊我們使用了 `self.ensure_one()` 確認 (確保只使用一條 record),
所以選兩條 reocrd 就會錯誤 :exclamation: :exclamation:
所以結論就是 `self.ensure_one()` 是要讓你檢查是否為 `singleton`.
另外, 空的 recordset 行為也像是 singleton, 當你 accessing fields 時,
它不會回傳 error ( 而是會回傳 `False`),
也因為這個特性, 所以我們才可以使用 `.` 歷遍 (traverse) records 而不用擔心錯誤 :smile:
(舉例, 下面的例子)
```python
>>> self.company_id.parent_id
res.company()
>>> self.company_id.parent_id.name
False
```
並不會發生錯誤, 只會回傳 `False`.
關於 `id="action_other_model_demo"` 可以看到他綁定的 model 是 `hr_expense`
``
所以, 要到 `hr_expense` 才可以看到這個 action


================================================
FILE: demo_actions_singleton/__init__.py
================================================
from . import models
================================================
FILE: demo_actions_singleton/__manifest__.py
================================================
{
'name': "demo actions singleton",
'summary': """
tutorial - actions and singleton
""",
'description': """
tutorial - actions and singleton
""",
'author': "My Company",
'website': "http://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base', 'hr_expense'],
# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'data/action_data.xml',
'views/menu.xml',
'views/view.xml',
],
'application': True,
}
================================================
FILE: demo_actions_singleton/data/action_data.xml
================================================
Action Demo
code
records.action_demo()
Action Other Demo
code
raise Warning('Hello')
================================================
FILE: demo_actions_singleton/models/__init__.py
================================================
from . import models
================================================
FILE: demo_actions_singleton/models/models.py
================================================
from odoo import models, fields, api
import logging
_logger = logging.getLogger(__name__)
class DemoActionsSingleton(models.Model):
_name = 'demo.actions.singleton'
_description = 'Demo Actions Singleton'
name = fields.Char('Description', required=True)
@api.multi
def action_demo(self):
self.ensure_one()
_logger.warning('=== CALL action_demo ===')
================================================
FILE: demo_actions_singleton/security/ir.model.access.csv
================================================
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_demo_singleton_user,Demo Actions Singleton User Access,model_demo_actions_singleton,demo_actions_singleton_group_user,1,1,1,0
access_demo_singleton_manager,Demo Actions Singleton Manager Access,model_demo_actions_singleton,demo_actions_singleton_group_manager,1,1,1,1
================================================
FILE: demo_actions_singleton/security/security.xml
================================================
Demo actions singleton category
User
Manager
================================================
FILE: demo_actions_singleton/views/menu.xml
================================================
================================================
FILE: demo_actions_singleton/views/view.xml
================================================
Demo Actions Singleton Form
demo.actions.singleton
Demo Actions Singleton List
demo.actions.singleton
================================================
FILE: demo_activity/README.md
================================================
# odoo 觀念 - activity
建議觀看影片, 會更清楚 :smile:
[Youtube Tutorial - odoo 手把手教學 - activity](https://youtu.be/_i4yLHrXRdg)
建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)
[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)
本篇文章主要介紹 odoo 中的 activity 這部份
## 說明
在 odoo 中, 肯定會常常看到 activity, 也就是如下圖的地方

因為要先定義一個 activity 的 data, 所以先來看 [data/mail_data.xml](data/mail_data.xml)
```xml
......
Activity Approval
fa-dollar
......
```
`name` 定義 activity 的名稱.
`icon` 定義 icon.
`res_model_id` 選擇對應的 model.
這個 activity 的 record 也可以在 odoo 中找到,
路徑為 Technical -> Email -> Activity Types


也可以進去修改相關的設定

再來看 [models/models.py](models/models.py)
```python
......
class DemoActivity(models.Model):
_name = "demo.activity"
_description = "Demo Activity"
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(string='name', required=True)
employee_id = fields.Many2one(
'hr.employee', string="Employee", required=True)
def button_activity_schedule(self):
self.activity_schedule(
'demo_activity.mail_act_approval',
user_id = self.sudo().employee_id.user_id.id,
note = 'my note',
summary = 'my summary')
def button_activity_feedback(self):
self.activity_feedback(
['demo_activity.mail_act_approval'])
def button_activity_unlink(self):
self.activity_unlink(
['demo_activity.mail_act_approval'])
```
注意 `_inherit = ['mail.thread', 'mail.activity.mixin']`
這繼承是必須的哦, 不然你的 activity 是會失效的 :smile:
這是所謂的 prototype inheritance,
可參考之前的文章以及影片 [demo_prototype_inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_prototype_inheritance).
最重要的就是這3個 function,
分別展示 `activity_schedule` `activity_feedback` `activity_unlink`
`activity_schedule`
指定 activity_schedule 給特定的人
```python
self.activity_schedule(
'demo_activity.mail_act_approval',
user_id = self.sudo().employee_id.user_id.id,
note = 'my note',
summary = 'my summary')
```
`demo_activity.mail_act_approval` 代表 activity id.
`user_id` 代表 user.
`note` 代表 note.
`summary` 代表 summary.
當點選範例的 activity_schedule

底下會顯示 activity

狀態列也會顯示有一個 activity

`activity_feedback`
同意(done)這個 activity
當點選範例的 activity_feedback

底下會顯示 activity 狀態

`activity_unlink`
取消 activity

這功能和直接點選 Cancel 是一樣的 ( activity 會消失 )

也請記得設定 security
[security/ir.model.access.csv](security/ir.model.access.csv)
[security/security.xml](security/security.xml)
來看 [views/view.xml](views/view.xml)
```xml
......
demo.activity.form
demo.activity
......
```
``
`name` 就是對應 model 中的 function 的名稱, 像這邊就是對應 `demo.activity` model 中的
`button_activity_schedule` function.
`string` 定義 button 的名稱.
最後的這段之前也說過了,
```xml
```
就是顯示下面的那段

最後記得也要設定 `__manifest__.py` 哦 :smile:
注意需要 depend `mail` :exclamation: :exclamation:
```python
......
{
......
# any module necessary for this one to work correctly
'depends': ['base', 'mail', 'hr'],
# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'data/mail_data.xml',
'views/menu.xml',
'views/view.xml',
],
'application': True,
}
```
================================================
FILE: demo_activity/__init__.py
================================================
from . import models
================================================
FILE: demo_activity/__manifest__.py
================================================
{
'name': "demo activity",
'summary': """
tutorial - demo activity
""",
'description': """
tutorial - demo activity
""",
'author': "My Company",
'website': "http://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base', 'mail', 'hr'],
# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'data/mail_data.xml',
'views/menu.xml',
'views/view.xml',
],
'application': True,
}
================================================
FILE: demo_activity/data/mail_data.xml
================================================
Activity Approval
fa-dollar
================================================
FILE: demo_activity/models/__init__.py
================================================
from . import models
================================================
FILE: demo_activity/models/models.py
================================================
from odoo import models, fields, api
class DemoActivity(models.Model):
_name = "demo.activity"
_description = "Demo Activity"
_inherit = ['mail.thread', 'mail.activity.mixin']
name = fields.Char(string='name', required=True)
employee_id = fields.Many2one(
'hr.employee', string="Employee", required=True)
def button_activity_schedule(self):
self.activity_schedule(
'demo_activity.mail_act_approval',
user_id = self.sudo().employee_id.user_id.id,
note = 'my note',
summary = 'my summary')
def button_activity_feedback(self):
self.activity_feedback(
['demo_activity.mail_act_approval'])
def button_activity_unlink(self):
self.activity_unlink(
['demo_activity.mail_act_approval'])
================================================
FILE: demo_activity/security/ir.model.access.csv
================================================
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_demo_activity_user,Demo Activity User Access,model_demo_activity,demo_activity_group_user,1,0,0,0
access_demo_activity_manager,Demo Activity Manager Access,model_demo_activity,demo_activity_group_manager,1,1,1,1
================================================
FILE: demo_activity/security/security.xml
================================================
Demo Activity Category
User
Manager
================================================
FILE: demo_activity/views/menu.xml
================================================
================================================
FILE: demo_activity/views/view.xml
================================================
demo.activity.form
demo.activity
================================================
FILE: demo_class_inheritance/README.md
================================================
# odoo 繼承 - class inheritance
建議觀看影片, 會更清楚 :smile:
[Youtube Tutorial - odoo 繼承 - class inheritance](https://youtu.be/zgb_0MJ3q9w)
建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)
[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)
本篇文章主要介紹 class inheritance 這部份
## 說明
`_inherit` class inheritance
注意, 還有一個是 `_inherits`, 不要搞錯了哦.
通常 `_inherit` 是去修改或是去擴充既有的 model,
使用情境可能如下,
像是在一個既有的 model 上增加一個 fields.
覆蓋掉一個已經存在的 model 中的 fields 定義.
增加 constraints 到一個既有的 model 上.
增加額外的 method 到一個既有的 model 上.
覆蓋掉一個已經存在的 model 中的 method.
這張圖是 odoo 中繼承的種類, 今天介紹 class inheritance,

先來看 [models/models.py](models/models.py)
```python
......
class ClassInheritance(models.Model):
_name = 'hr.expense' # 可寫可不寫
_inherit = ['hr.expense']
test_field = fields.Char('test_field')
......
```
目標是去繼承 `hr.expense`, 並且增加一個 `fields`.
`_name = 'hr.expense' # 可寫可不寫`
`hr.expense` 是一個既有的 model, 所以在 `__manifest__.py` 中有 depends 關係
(記得一定要寫 depends, 不然會出現錯誤 :exclamation:)
```python
......
'depends': ['hr_expense'],
......
```
`_name` 和 `_inherit` 在這邊的名稱都是一樣的,
注意, 請不要自己定義一個 `_name` (和 `_inherit` 不一致), 因為這是另一個東西(如下圖, 以後說明).

`_inherit = ['hr.expense']`
主要去繼承 `hr.expense`, 所以一定要有 depends :exclamation: :exclamation:
當你安裝好 addons, 我們到資料庫中可以找到剛剛新增的 test_field

簡單說這種繼承的方式就是在繼承的 model 上增加新功能.
[views/views.xml](views/views.xml)
```xml
......
hr.expense.tree.custom
hr.expense
......
```

找 fields 的時候有兩種方式可以找,
第一種, 比較簡單的方法, 直接找到 fields, 然後定義 position 即可
```xml
......
......
```
第二種, 使用 xpath 的語法, 稍微比較複雜一點, 但是當你一個 view
裡面有重複的 fields 時, 就比較適合使用 xpath, 因為如果你使用第
一種方法, 會導致找不到 (有重複它會不知道要找哪一個 :grimacing:)
```xml
......
......
```
這邊補充一下 xpath slash 的不同 `/` `//` :exclamation: :exclamation:
`/` 代表直接去找目前節點的 child, 如果找不到就會發生錯誤. (類似絕對路徑的概念).
`//` 代表去找目前節點的後面全部的節點 (類似相對路徑的概念).
通常用 `/` 找不到的節點, 改用 `//` 都會找的到 :smile:
如果你想要看更詳細的說明, google 關鍵字是 `xpath slash vs double slash` :smirk:
所以可以依照自己的需求下去選擇.
[views/views.xml](views/views.xml)
form 的部份
```xml
......
hr.expense.view.form.custom
hr.expense
......
```

## 延伸閱讀
* [demo_prototype_inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_prototype_inheritance)
* [demo_delegation_inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_delegation_inheritance)
================================================
FILE: demo_class_inheritance/__init__.py
================================================
from . import models
================================================
FILE: demo_class_inheritance/__manifest__.py
================================================
{
'name': "demo_class_inheritance",
'summary': """
demo_class_inheritance,
model, view, form
""",
'description': """
demo_class_inheritance
model, view, form
""",
'author': "My Company",
'website': "http://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['hr_expense'],
# always loaded
'data': [
'views/views.xml',
],
# only loaded in demonstration mode
# 'demo': [
# 'demo/demo.xml',
# ],
'installable': True,
'auto_install': False,
'application': False,
}
================================================
FILE: demo_class_inheritance/models/__init__.py
================================================
from . import model
================================================
FILE: demo_class_inheritance/models/model.py
================================================
from odoo import models, fields
# https://www.odoo.com/forum/help-1/question/whats-the-difference-between-inherit-and-inherits-52205
# class inheritance
class ClassInheritance(models.Model):
_name = 'hr.expense' # 可寫可不寫
_inherit = ['hr.expense']
test_field = fields.Char('test_field')
================================================
FILE: demo_class_inheritance/views/views.xml
================================================
hr.expense.tree.custom
hr.expense
hr.expense.view.form.custom
hr.expense
================================================
FILE: demo_config_settings/README.md
================================================
# 實作 config settings
建議觀看影片, 會更清楚 :smile:
* [實作 config settings](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_config_settings#%E8%AA%AA%E6%98%8E) - [Youtube Tutorial - odoo - 實作 config settings](https://youtu.be/5k_TYBNs_uc)
* [implied_group 用法說明](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_config_settings#implied_group-%E7%94%A8%E6%B3%95%E8%AA%AA%E6%98%8E) - [Youtube Tutorial - odoo - implied_group 進階用法說明](https://youtu.be/FCmRNUSkh10)
建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)
[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)
有時候會需要對 addons 做一些參數的 settings,
所以這篇主要介紹 odoo 中如何實現 config settings 的部份.
## 說明
[models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_config_settings/models/models.py)
```python
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
demo_prefix = fields.Char(
string="Demo Prefix",
# config_parameter='demo_config_settings.config.demo_prefix',
)
def get_values(self):
res = super(ResConfigSettings, self).get_values()
demo_prefix = self.env["ir.config_parameter"].get_param("demo_config_settings.config.demo_prefix", False)
res.update({
'demo_prefix': demo_prefix,
})
return res
def set_values(self):
super(ResConfigSettings, self).set_values()
self.env['ir.config_parameter'].sudo().set_param('demo_config_settings.config.demo_prefix', self.demo_prefix)
```
這邊是使用 TransientModel, 如果不知道這個是甚麼, 建議先了解之前的文章
[demo_odoo_tutorial_wizard](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial_wizard) - TransientModel 範例.
這邊的重點在需要實作 `set_values` 以及 `get_values`,
`set_values`
將 `demo_prefix` 設定到 `ir.config_parameter` model 的 `demo_config_settings.config.demo_prefix` (這個名稱可以自己自訂) 中.
`get_values`
從 `ir.config_parameter` model 的 param 找是否有 `demo_config_settings.config.demo_prefix`.
如果你沒有特殊的邏輯要處理, 可以直接使用 `config_parameter='demo_config_settings.config.demo_prefix'` 代替,
也就是說 `set_values` `get_values` 可以使用 `config_parameter=...` 替代.
views 的部份可參考 [views/view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_config_settings/views/view.xml).
然後不需要 security 資料夾, 因為它是 TransientModel.
裝好 addons, debug developer mode 請打開, 可參考 [odoo12 如何開啟 odoo developer mode](https://github.com/twtrubiks/odoo-docker-tutorial#odoo12-%E5%A6%82%E4%BD%95%E9%96%8B%E5%95%9F-odoo-developer-mode),
Odoo Setup Demo 就是我們加上去的 (在這裡填入 hello123, 記得 Save)

然後到 Technical -> Parameters -> System Parameters

在這裡你會看到剛剛定義的 `demo_config_settings.config.demo_prefix` 為 hello123

這樣子就可以在程式需要取設定值時, 直接到 `ir.config_parameter` model 裡找 :smile:
## implied_group 用法說明
[Youtube Tutorial - odoo - implied_group 用法說明](https://youtu.be/FCmRNUSkh10)
這部份稍微比較進階一點,
原始碼的路徑可參考 `odoo/addons/base/models/res_config.py`
```python
......
class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin):
""" Base configuration wizard for application settings. It provides support for setting
default values, assigning groups to employee users, and installing modules.
To make such a 'settings' wizard, define a model like::
class MyConfigWizard(models.TransientModel):
_name = 'my.settings'
_inherit = 'res.config.settings'
default_foo = fields.type(..., default_model='my.model'),
group_bar = fields.Boolean(..., group='base.group_user', implied_group='my.group'),
module_baz = fields.Boolean(...),
config_qux = fields.Char(..., config_parameter='my.parameter')
other_field = fields.type(...),
......
```
`implied_group` 這個的功能主要是用來管理 user 擁有哪些 groups 的權限.
( 其實他的概念和 `implied_ids` 是一樣的, 但這個用法更進階一點 :smirk: )
詳細說明請看下方的 demo,
[models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_config_settings/models/models.py)
```python
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
......
group_demo_config_setting = fields.Boolean("Demo Config",
group='base.group_user', # default
# group='demo_config_settings.demo_config_settings_tutorial_group',
implied_group='sale.group_delivery_invoice_address',
)
......
```
`group='base.group_user'` 為預設, 如果你不設定, 就是會預設這個值.
`implied_group='sale.group_delivery_invoice_address'` 這邊使用內建的
`sale.group_delivery_invoice_address` 來當作範例.
`sale.group_delivery_invoice_address` 路徑在 `addons/sale/security/sale_security.xml`
```xml
Addresses in Sales Orders
```
當這個 field 為 True 的時候, 所有的 `group='base.group_user'` 都會擁有
`sale.group_delivery_invoice_address` groups 的權限.
(注意 :exclamation: :exclamation: field 命名一定要是 `group_xxx` )
當設定為 True 時

你會發現全部的 `group='base.group_user'` 都擁有 `sale.group_delivery_invoice_address` groups 的權限.

上面註解的 `group='demo_config_settings.demo_config_settings_tutorial_group'`
只針對 admin user, 也就是設定為 True 時, 只會對擁有 admin 的 user 生效.
(請自行修改測試, 這邊就不打字了, 影片內說明, 因為大同小異 :smile:)
記得加入 [security/security.xml](security/security.xml)
```xml
......
Config Settings User
......
```
``
這代表這個 groups 是被隱藏的.
也就是不會出現在 user 設定 groups 的地方.
================================================
FILE: demo_config_settings/__init__.py
================================================
from . import models
================================================
FILE: demo_config_settings/__manifest__.py
================================================
{
'name': "demo odoo config settings",
'summary': """
basic tutorial -
demo odoo config settings
""",
'description': """
basic tutorial -
demo odoo config settings
""",
'author': "My Company",
'website': "http://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base_setup', 'sale'],
# always loaded
'data': [
'security/security.xml',
'views/view.xml',
],
'installable': True,
'auto_install': False,
'application': True,
}
================================================
FILE: demo_config_settings/models/__init__.py
================================================
from . import models
================================================
FILE: demo_config_settings/models/models.py
================================================
from odoo import models, fields, api
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
demo_prefix = fields.Char(
string="Demo Prefix",
# config_parameter='demo_config_settings.config.demo_prefix',
)
group_demo_config_setting = fields.Boolean("Demo Config",
group='base.group_user', # default
# group='demo_config_settings.demo_config_settings_tutorial_group',
implied_group='sale.group_delivery_invoice_address',
)
def get_values(self):
res = super(ResConfigSettings, self).get_values()
demo_prefix = self.env["ir.config_parameter"].get_param("demo_config_settings.config.demo_prefix", False)
res.update({
'demo_prefix': demo_prefix,
})
return res
def set_values(self):
super(ResConfigSettings, self).set_values()
self.env['ir.config_parameter'].sudo().set_param('demo_config_settings.config.demo_prefix', self.demo_prefix)
================================================
FILE: demo_config_settings/security/security.xml
================================================
Config Settings User
================================================
FILE: demo_config_settings/views/view.xml
================================================
res.config.inherit.custom
res.config.settings
================================================
FILE: demo_datetime_tutorial/README.md
================================================
# odoo datetime 教學
建議觀看影片, 會更清楚 :smile:
[Youtube Tutorial - odoo datetime 教學](https://youtu.be/Ha0YNFm6KzI)
建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)
[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)
本篇文章將說明 odoo 中的 datetime 機制.
## 說明
注意 :exclamation:
在 odoo 中所有的 date 和 datetime 都是使用 UTC 的時間 (包含保存進資料庫 db 的資料).
這樣你可能會問我, odoo 不是可以選時區, 這樣頁面上是怎麼呈現時區正確的時間的 :question:
odoo 會透過 session (或是你的設定) 去做 time zone 的轉換.
每個 user 都可以設定自己的時區, 系統會建議你設定和瀏覽器一樣的時區

先來看一下 python 的 datetime
```python
>>> from datetime import datetime
>>> datetime(2020, 1, 10, 0, 0)
datetime.datetime(2020, 1, 10, 0, 0)
# strptime, python 的 str -> datetime
>>> my_datetime = datetime.strptime('2020/11/23', '%Y/%m/%d')
>>> my_datetime
datetime.datetime(2020, 11, 23, 0, 0)
# strftime, python 的 datetime -> str
>>> datetime.strftime(my_datetime, '%Y-%m-%d')
'2020-11-23'
```
python 的 timedelta
```python
>>> from datetime import date
>>> from datetime import timedelta
>>> date.today()
datetime.date(2020, 10, 14)
>>> date.today() + timedelta(days=7)
datetime.date(2020, 10, 21)
```
python 的時區轉換
```python
>>> ## Asia/Taipei -> UTC
>>> from pytz import timezone
>>> from datetime import datetime
>>> today = datetime(2022, 7, 7, 10, 0)
>>> user_tz = timezone('Asia/Taipei')
>>> today = user_tz.localize(today)
>>> today
datetime.datetime(2022, 7, 7, 10, 0, tzinfo=)
>>> today = today.astimezone(timezone('UTC'))
>>> today
datetime.datetime(2022, 7, 7, 2, 0, tzinfo=)
>>> ## UTC -> Asia/Taipei
>>> from pytz import timezone
>>> from datetime import datetime
>>> today = datetime(2022, 7, 7, 10, 0)
>>> user_tz = timezone('UTC')
>>> today = user_tz.localize(today)
>>> today
datetime.datetime(2022, 7, 7, 10, 0, tzinfo=)
>>> today = today.astimezone(timezone('Asia/Taipei'))
>>> today
datetime.datetime(2022, 7, 7, 18, 0, tzinfo=)
```
odoo 中的 odoo.tools.date_utils
`start_of(value, granularity)`
`end_of(value, granularity)`
`add(value, **kwargs)`
`subtract(value, **kwargs)`
```python
from odoo.tools import date_utils
from datetime import date
>>> date.today()
datetime.date(2020, 10, 14)
>>> date_utils.add(date.today(), days=2)
datetime.date(2020, 10, 16)
>>> date_utils.subtract(date.today(), months=2)
datetime.date(2020, 8, 14)
```
odoo 中的 `fields.Date.today()` `fields.Datetime.now()`
```python
>>> from odoo import fields
>>> fields.Date.today()
datetime.date(2020, 10, 14)
>>> fields.Datetime.now()
datetime.datetime(2020, 10, 14, 10, 48, 25)
```
fields.Date `to_date` converts a string into a date object.
fields.Datetime `to_datetime(value)` converts a string into a datetime object.
fields.Date, fields.Datetime `to_string(value)` converts a date or datetime object into a string in the format expected by the Odoo server.
( 其實這個 `to_string(value)` 也只是使用 python 的 `strftime` 去轉換而已, 格式是預設的 DATETIME_FORMAT)
`fields.Date.context_today(record, timestamp=None)`
`fields.Datetime.context_timestamp(record, timestamp)`
```python
>>> from odoo import fields
>>> my_datetime = fields.Datetime.to_datetime('2020-02-10 10 :00:00')
>>> my_datetime
datetime.datetime(2020, 2, 10, 10, 0)
>>> my_datetime = fields.Datetime.from_string('2020-02-10 10 :00:00')
>>> my_datetime
datetime.datetime(2020, 2, 10, 10, 0)
>>> fields.Datetime.to_string(my_datetime)
'2020-02-10 10 :00:00'
```
寫入資料時, 可以直接輸入 string, 會自動轉換成 date / datetime
```python
>>> demo = self.env['demo.datetime'].browse(1)
>>> demo.my_datetime
datetime.datetime(2020, 10, 14, 9, 30, 8)
>>> demo.my_datetime = '2020-01-01 09 :00:00'
>>> demo.my_datetime
datetime.datetime(2020, 1, 1, 9, 0)
```
轉換時區
```python
from pytz import timezone
# Convert to Asia/Taipei time zone
>>> demo = self.env['demo.datetime'].browse(1)
>>> demo.my_datetime
datetime.datetime(2020, 1, 1, 9, 0)
>>> demo.my_datetime.astimezone(timezone('Asia/Taipei'))
datetime.datetime(2020, 1, 1, 17, 0, tzinfo=)
```
將 addons 裝起來之後, 來看 [models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_datetime_tutorial/models/models.py)
```python
......
@api.multi
def demo1(self):
_logger.warning('db datetime')
_logger.warning(self.my_datetime )
_logger.warning('Asia/Taipei datetime')
_logger.warning(self.my_datetime.astimezone(timezone('Asia/Taipei')))
......
```

可以從輸出中看到一個是 db 保存的 utc 時間, 一個是轉換後的 Taipei 時間.

db 中保存的時間

================================================
FILE: demo_datetime_tutorial/__init__.py
================================================
from . import models
================================================
FILE: demo_datetime_tutorial/__manifest__.py
================================================
{
'name': "demo_datetime_tutorial",
'summary': """
demo_datetime_tutorial
""",
'description': """
demo_datetime_tutorial
""",
'author': "My Company",
'website': "http://www.yourcompany.com",
# Categories can be used to filter modules in modules listing
# Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml
# for the full list
'category': 'Uncategorized',
'version': '0.1',
# any module necessary for this one to work correctly
'depends': ['base'],
# always loaded
'data': [
'security/ir.model.access.csv',
'views/menu.xml',
'views/view.xml',
],
'installable': True,
'auto_install': False,
'application': True,
}
================================================
FILE: demo_datetime_tutorial/models/__init__.py
================================================
from . import models
================================================
FILE: demo_datetime_tutorial/models/models.py
================================================
from odoo import models, fields, api
from pytz import timezone
import logging
_logger = logging.getLogger(__name__)
class DemoDatetime(models.Model):
_name = "demo.datetime"
_description = 'Demo Datetime Tutorial'
name = fields.Char('Name', required=True)
my_datetime = fields.Datetime(
'my_datetime', default=fields.Datetime.now())
@api.multi
def demo1(self):
_logger.warning('db datetime')
_logger.warning(self.my_datetime )
_logger.warning('Asia/Taipei datetime')
_logger.warning(self.my_datetime.astimezone(timezone('Asia/Taipei')))
================================================
FILE: demo_datetime_tutorial/security/ir.model.access.csv
================================================
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_demo_datetime,Demo Datetime User Access,model_demo_datetime,,1,1,1,1
================================================
FILE: demo_datetime_tutorial/views/menu.xml
================================================
================================================
FILE: demo_datetime_tutorial/views/view.xml
================================================
demo_datetime_tutorial_form
demo.datetime
demo_datetime_tutorial_tree
demo.datetime
================================================
FILE: demo_delegation_inheritance/README.md
================================================
# odoo 繼承 - delegation inheritance
建議觀看影片, 會更清楚 :smile:
[Youtube Tutorial - odoo 繼承 - delegation inheritance](https://youtu.be/J1-Hg9vrXBs)
建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)
* [odoo 繼承 - class inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_class_inheritance)
* [odoo 繼承 - prototype inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_prototype_inheritance)
本篇文章主要介紹 delegation inheritance 這部份
## 說明
在開始介紹範例之前, 請先看下圖
Model inheritance ( `_inherit` vs `_inherits` ),
[inheritance](https://www.odoo.com/documentation/12.0/howtos/backend.html#inheritance)

`_inherits` 以下為官方說明
```txt
The second inheritance mechanism (delegation) allows to link every record of a model to a record in a parent model, and provides transparent access to the fields of the parent record.
```
```python
class DelegationInheritance(models.Model):
_name = 'new'
_inherits = 'obj1'
```
幾個重點,
Stored in different tables (會儲存在不同的 table 中)
`new` instances contain am embedded.
`obj1` instance with synchronized values.
(同步的意思就是會幫你自動建立, 等等來看實例說明)
先來看 [models/models.py](models/models.py)
```python
class DelegationInheritance(models.Model):
_name = 'demo.delegation'
_description = 'Demo DelegationInheritance'
_inherits = {"res.partner": "partner_id"}
partner_id = fields.Many2one('res.partner', string='Partner', required=True, ondelete="cascade")
first_name = fields.Char('First Name', size=16)
......
```
再來看 [views/view.xml](views/view.xml)
```xml
......
Demo Delegation Tutorial Form
demo.delegation
......
```
其實它有點特殊, 在裡面甚至可以使用 (委派 Delegation) `res.partner` 的欄位.
當建立 `demo.delegation` 時, 也會自動幫你建立 `partner_id`.
你可能會問我為甚麼沒有 `name` field, 但是卻可以使用 `name` :question:
因為這個 `name` 其實是屬於 `res.partner` 的 :smile:
以下操作一遍流程,
在 `demo.delegation` 中建立一筆資料

在 `res.partner` 中也會自動建立一筆資料

接著從 db 中看資料怎麼跑
在 `demo.delegation` 中只紀錄了 `partner_id` 而已, 當然還有 `first_name`.

其他的 `name` `company_id` 都是儲存在 `res.partner` 中的.
(雖然是在 `demo.delegation` 中輸入的, 但這就是委派的概念)

小結論, 父類別`res.partner`的 field 會儲存在父類別`res.partner`的 table 中,
而新的模型`demo.delegation`的 field 則會儲存在新的模型`demo.delegation`的 table 中.
當使用新的模型`demo.delegation`時, 可以看到父類別`res.partner`的資料.
當使用父類別`res.partner`時, **只能看到**父類別`res.partner`的資料.
委派最重要的目的就是避免在很多的 table 中建立重複的資料. (達到共用的效果 :smile:)
在原始碼中, 也有幾個範例可以參考 :smile:
第一個範例為 `res.users` 以及 `res.partner`
```python
# odoo/addons/base/models/res_users.py
class Users(models.Model):
""" User class. A res.users record models an OpenERP user and is different
from an employee.
res.users class now inherits from res.partner. The partner model is
used to store the data related to the partner: lang, name, address,
avatar, ... The user model is now dedicated to technical data.
"""
_name = "res.users"
_description = 'Users'
_inherits = {'res.partner': 'partner_id'}
......
partner_id = fields.Many2one('res.partner', required=True, ondelete='restrict', auto_join=True,
string='Related Partner', help='Partner-related data of the user')
......
# odoo/addons/base/models/res_partner.py
class Partner(models.Model):
_description = 'Contact'
_inherit = ['format.address.mixin']
_name = "res.partner"
_order = "display_name"
......
```
當你建立 `res.users` 時, 也會自動建立一個 `res.partner`. (可搭配 db 觀看結果)
在 `res.users` 底下, 可以任意得使用 `res.partner` field, 但相反過來,
在 `res.partner` 底下, 只可以使用 `res.partner` 自己的 field.
第二個範例為 `product.product` 以及 `product.template`
```python
# addons/product/models/product.py
class ProductProduct(models.Model):
_name = "product.product"
_description = "Product"
_inherits = {'product.template': 'product_tmpl_id'}
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'default_code, name, id'
......
product_tmpl_id = fields.Many2one(
'product.template', 'Product Template',
auto_join=True, index=True, ondelete="cascade", required=True)
......
# addons/product/models/product_template.py
class ProductTemplate(models.Model):
_name = "product.template"
_inherit = ['mail.thread', 'mail.activity.mixin', 'image.mixin']
_description = "Product Template"
_order = "name"
......
```
================================================
FILE: demo_delegation_inheritance/__init__.py
================================================
from . import models
================================================
FILE: demo_delegation_inheritance/__manifest__.py
================================================
{
'name': 'demo_delegation_inheritance',
'version': '12.0.1.0.0',
'summary': 'demo_delegation_inheritance',
'description': '''
demo_delegation_inheritance
''',
'depends': ['base', 'contacts'],
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'views/menu.xml',
'views/view.xml',
],
'license': 'AGPL-3',
'images': [
],
'qweb': [
],
'installable': True,
'auto_install': False,
'application': False,
}
================================================
FILE: demo_delegation_inheritance/models/__init__.py
================================================
from . import model
================================================
FILE: demo_delegation_inheritance/models/model.py
================================================
from odoo import models, fields, api
# ref.
#
# addons/product/models/product.py
# class ProductProduct(models.Model):
# _name = "product.product"
#
# addons/product/models/product_template.py
# class ProductTemplate(models.Model):
# _name = "product.template"
#
class DelegationInheritance(models.Model):
_name = 'demo.delegation'
_description = 'Demo DelegationInheritance'
_inherits = {"res.partner": "partner_id"}
partner_id = fields.Many2one('res.partner', string='Partner', required=True, ondelete="cascade")
first_name = fields.Char('First Name', size=16)
================================================
FILE: demo_delegation_inheritance/security/ir.model.access.csv
================================================
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_demo_delegation_user,Demo Delegation Tutorial User Access,model_demo_delegation,demo_delegation_tutorial_group_user,1,0,0,0
access_demo_delegation_manager,Demo Delegation Tutorial Manager Access,model_demo_delegation,demo_delegation_tutorial_group_manager,1,1,1,1
================================================
FILE: demo_delegation_inheritance/security/security.xml
================================================
Demo Delegation tutorial category
User
Manager
================================================
FILE: demo_delegation_inheritance/views/menu.xml
================================================
================================================
FILE: demo_delegation_inheritance/views/view.xml
================================================
Demo Delegation Tutorial Form
demo.delegation
Demo Delegation Tutorial List
demo.delegation
================================================
FILE: demo_expense_tutorial_v1/README.md
================================================
# odoo 入門篇
建議觀看影片, 會更清楚 :smile:
* [Youtube Tutorial - odoo 手把手教學 - Many2one - part1](https://youtu.be/vb_Z8KCI-wk) - [文章快速連結](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)
* [Youtube Tutorial - odoo 手把手教學 - Many2many - part2](https://youtu.be/QeZfJqTGP-w) - [文章快速連結](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)
* [Youtube Tutorial - odoo 手把手教學 - One2many - part3](https://youtu.be/WiLdXP781N0) - [文章快速連結](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)
* [Youtube Tutorial - odoo 手把手教學 - One2many Editable Bottom and Top - part3-1](https://youtu.be/HJcBAFXQYVc) - [文章快速連結](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)
* [Youtube Tutorial - odoo 手把手教學 - Search Filters - part4](https://youtu.be/zcWMs16p9Xw) - [文章快速連結](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)
* [Youtube Tutorial - odoo 手把手教學 - 說明 noupdate 以及 domain_force - part5](https://youtu.be/twn6zz3OeRs) - [文章快速連結](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)
* [Youtube Tutorial - odoo 手把手教學 - 如何透過 button 呼叫 view, form - part6](https://youtu.be/URxuH2HG44Q) - [文章快速連結](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)
* [Youtube Tutorial - odoo 手把手教學 - 說明 name_get 和 _name_search - part7](https://youtu.be/g-dclCkwY5c) - [文章快速連結](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)
* [Youtube Tutorial - odoo 手把手教學 - 使用 python 增加取代 One2many M2X record - part8](https://youtu.be/GBCGS2znnT8) - [文章快速連結](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)
* [Youtube Tutorial - odoo 手把手教學 - tree create delete edit False - part9](https://youtu.be/0fpA89QcYZM) - [文章快速連結](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)
* [Youtube Tutorial - odoo 手把手教學 - 同一個 model 使用不同的 view_ids - part10](https://youtu.be/YltcAu9OZhc) - [文章快速連結](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%90%8C%E4%B8%80%E5%80%8B-model-%E4%BD%BF%E7%94%A8%E4%B8%8D%E5%90%8C%E7%9A%84-view_ids---part10)
* [Youtube Tutorial - odoo 手把手教學 - widget 介紹 handle 和 many2onebutton - part11](https://youtu.be/zb5fSEtEo_g) - [文章快速連結](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---widget-%E4%BB%8B%E7%B4%B9-handle-%E5%92%8C-many2onebutton---part11)
* [Youtube Tutorial - odoo 手把手教學 - view 搭配 context - part12](https://youtu.be/c-nzbAuaH9I) - [文章快速連結](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---view-%E6%90%AD%E9%85%8D-context---part12)
* [Youtube Tutorial - odoo 手把手教學 - view 搭配 active_test context - part13](https://youtu.be/RR9ycgky444) - [文章快速連結](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---view-%E6%90%AD%E9%85%8D-active_test-context---part13)
* [Youtube Tutorial - odoo 手把手教學 - view 搭配 domain - part14](https://youtu.be/Rh-rmXIHTZo) - [文章快速連結](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---view-%E6%90%AD%E9%85%8D-domain---part14)
* [Youtube Tutorial - odoo 手把手教學 - 如何看到當下 view 繼承頁面 - part15](https://youtu.be/Vs6ScbYuZNs) - [文章快速連結](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%E7%9C%8B%E5%88%B0%E7%95%B6%E4%B8%8B-view-%E7%B9%BC%E6%89%BF%E9%A0%81%E9%9D%A2---part15)
* [Youtube Tutorial - odoo 手把手教學 - odoo rainbow - part16](https://youtu.be/g4vywRLklE0) - [文章快速連結](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---odoo-rainbow---part16)
* [Youtube Tutorial - odoo 手把手教學 - tree decoration - part17](https://youtu.be/tJdw6IEb8UQ) - [文章快速連結](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-decoration---part17)
* [Youtube Tutorial - odoo 手把手教學 - model _rec_name 說明 - part18](https://youtu.be/JtcSnbHNjAU) - [文章快速連結](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---model-_rec_name-%E8%AA%AA%E6%98%8E---part18)
* [Youtube Tutorial - odoo 手把手教學 - copy override 說明 - part19](https://youtu.be/VDnIFb7e7wM) - [文章快速連結](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---copy-override-%E8%AA%AA%E6%98%8E---part19)
* [Youtube Tutorial - odoo 手把手教學 - move position 說明 - part20](https://youtu.be/l-bFOqTYgTA) - [文章快速連結](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---move-position-%E8%AA%AA%E6%98%8E---part20)
* odoo 手把手教學 - ir.actions.act_url 說明 - part21 - [文章快速連結](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---iractionsact_url-%E8%AA%AA%E6%98%8E---part21)
* [Youtube Tutorial - odoo 手把手教學 - Smart Button 說明 - part22](https://youtu.be/fsZK1KRgnF0) - [文章快速連結](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---smart-button-%E8%AA%AA%E6%98%8E---part22)
* [Youtube Tutorial - odoo 手把手教學 - options create_edit 說明 - part23](https://youtu.be/GdPKllI7quI) - [文章快速連結](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---options-create_edit-%E8%AA%AA%E6%98%8E---part23)
* [Youtube Tutorial - odoo 手把手教學 - PostgreSQL ondelete cascade 說明 - part24](https://youtu.be/OTh5R2LrwJE) - [文章快速連結](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---postgresql-ondelete-cascade-%E8%AA%AA%E6%98%8E---part24)
* [Youtube Tutorial - odoo14 手把手教學 - auto_join 說明 - part25](https://youtu.be/OOlPZETkYKw) - [文章快速連結](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)
* [Youtube Tutorial - odoo 手把手教學 - view parent 說明 - part26](https://youtu.be/i_hG4s_YJN0) - [文章快速連結](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---view-parent-%E8%AA%AA%E6%98%8E---part26)
* [Youtube Tutorial - odoo 手把手教學 - domain 搭配 fields 的三種用法 - part27](https://youtu.be/ZUNRoWxVWAE) - [文章快速連結](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---domain-%E6%90%AD%E9%85%8D-fields-%E7%9A%84%E4%B8%89%E7%A8%AE%E7%94%A8%E6%B3%95---part27)
* [Youtube Tutorial - odoo 手把手教學 - form_view_ref 以及 tree_view_ref 說明 - part28](https://youtu.be/_YkrOp3ytlQ) - [文章快速連結](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---form_view_ref-%E4%BB%A5%E5%8F%8A-tree_view_ref-%E8%AA%AA%E6%98%8E---part28)
* odoo 手把手教學 - Message Post 教學 - part29 - [文章快速連結](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---message-post-%E6%95%99%E5%AD%B8---part29)
* [Youtube Tutorial - odoo 手把手教學 - groups 搭配 fields 用法 - part30](https://youtu.be/JyNyg7iHar0) - [文章快速連結](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---groups-%E6%90%AD%E9%85%8D-fields-%E7%94%A8%E6%B3%95---part30)
* [Youtube Tutorial - odoo 手把手教學 - ACID transactions 說明 - part31](https://youtu.be/M36CNiK9xrM) - [文章快速連結](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---acid-transactions-%E8%AA%AA%E6%98%8E---part31)
* [Youtube Tutorial - odoo 手把手教學 - 特殊 groups 應用說明 - part32](https://youtu.be/PSiDfM840NI) - [文章快速連結](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---%E7%89%B9%E6%AE%8A-groups-%E6%87%89%E7%94%A8%E8%AA%AA%E6%98%8E---part32)
建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)
[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)
這篇主要介紹 Many2one, Many2many, One2many 這三個東西,
以下將介紹這個 addons 的結構
## 說明
### odoo 手把手教學 - Many2one - part1
* [Youtube Tutorial - odoo 手把手教學 - Many2one - part1](https://youtu.be/vb_Z8KCI-wk)
先來看 [models/models.py](models/models.py)
`Many2one`
```python
......
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)
......
```

一個 `hr.employee` 可以對到很多個 `demo.expense.tutorial`,
所以是 多(`demo.expense.tutorial`) 對 一(`hr.employee`) 的關係,
來看 db 中的狀況
`demo_expense_tutorial` 會多出一個欄位 ( 對應 `hr_employee` 的 id )

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

因為 One2many 比較特別, 所以我們先介紹 Many2many :laughing:
### odoo 手把手教學 - Many2many - part2
`Many2many`
* [Youtube Tutorial - odoo 手把手教學 - Many2many - part2](https://youtu.be/QeZfJqTGP-w)
要建立 Many2many 之前, 一定要先定義一個 model,
先定義 DemoTag (也請記得設定 [security/ir.model.access.csv](security/ir.model.access.csv) )
[models/models.py](models/models.py)
```python
......
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](models/models.py)
```python
......
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
......
# https://www.odoo.com/documentation/12.0/reference/orm.html#odoo.fields.Many2many
# Many2many(comodel_name=