Showing preview only (445K chars total). Download the full file or copy to clipboard to get everything.
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
......
<tr>
<td><strong>Email</strong></td>
<td><span t-field="o.sudo().employee_id.identification_id"/></td>
</tr>
......
```
`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
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.ui.view" id="custom_report_wizard">
<field name="name">Custom Report</field>
<field name="model">report.wizard</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Custom Report">
<group>
<group>
<field name="date_start"/>
</group>
<group>
<field name="date_end"/>
</group>
</group>
<footer>
<button name="download_report" string="Download Report" type="object" class="oe_highlight"/>
<button string="Cancel" special="cancel"/>
</footer>
</form>
</field>
</record>
<act_window id="action_custom_report_wizard"
name="Action Custom Report"
res_model="report.wizard"
view_mode="form"
target="new"/>
<menuitem action="action_custom_report_wizard"
id="menu_custom_report_wizard"
parent="hr_expense.menu_hr_expense_reports"/>
</odoo>
```
這邊定義了基本的 form, 並且將 menu 設定在 `hr_expense.menu_hr_expense_reports` 之下.


剛剛前面提到 report id `action_report_abstractmodel` 在 [reports/report.xml](reports/report.xml)
```xml
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_wizard_template">
<t t-call="web.html_container">
<div class="header">
<h3 class="text-center">Expense Wizard Report</h3>
<h4 class="text-center">
<strong>From</strong>:
<t t-esc="date_start"/>
<strong>To</strong>:
<t t-esc="date_end"/>
</h4>
</div>
<div>
<table>
<thead>
<th class="text-center">Name</th>
<th class="text-center">Date</th>
<th class="text-center">Unit_amount</th>
</thead>
<tbody>
<t t-foreach="docs" t-as="doc">
<tr>
<td>
<span t-esc="doc['name']"/>
</td>
<td class="text-center">
<span t-esc="doc['date']"/>
</td>
<td class="text-center">
<span t-esc="doc['unit_amount']"/>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</t>
</template>
<report
id="action_report_abstractmodel"
string="Demo Report"
model="report.wizard"
report_type="qweb-pdf"
name="demo_abstractmodel_tutorial.report_wizard_template"
print_report_name="Demo Report"
/>
</odoo>
```
分別設定了 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
================================================
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_wizard_template">
<t t-call="web.html_container">
<div class="header">
<h3 class="text-center">Expense Wizard Report</h3>
<h4 class="text-center">
<strong>From</strong>:
<t t-esc="date_start"/>
<strong>To</strong>:
<t t-esc="date_end"/>
</h4>
</div>
<div>
<table>
<thead>
<th class="text-center">Name</th>
<th class="text-center">Date</th>
<th class="text-center">Unit_amount</th>
</thead>
<tbody>
<t t-foreach="docs" t-as="doc">
<tr>
<td>
<span t-esc="doc['name']"/>
</td>
<td class="text-center">
<span t-esc="doc['date']"/>
</td>
<td class="text-center">
<span t-esc="doc['unit_amount']"/>
</td>
</tr>
</t>
</tbody>
</table>
</div>
</t>
</template>
<report
id="action_report_abstractmodel"
string="Demo Report"
model="report.wizard"
report_type="qweb-pdf"
name="demo_abstractmodel_tutorial.report_wizard_template"
print_report_name="Demo Report"
/>
</odoo>
================================================
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
================================================
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record model="ir.ui.view" id="custom_report_wizard">
<field name="name">Custom Report</field>
<field name="model">report.wizard</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Custom Report">
<group>
<group>
<field name="date_start"/>
</group>
<group>
<field name="date_end"/>
</group>
</group>
<footer>
<button name="download_report" string="Download Report" type="object" class="oe_highlight"/>
<button string="Cancel" special="cancel"/>
</footer>
</form>
</field>
</record>
<act_window id="action_custom_report_wizard"
name="Action Custom Report"
res_model="report.wizard"
view_mode="form"
target="new"/>
<menuitem action="action_custom_report_wizard"
id="menu_custom_report_wizard"
parent="hr_expense.menu_hr_expense_reports"/>
</odoo>
================================================
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
================================================
<?xml version="1.0"?>
<odoo>
<!-- demo_model_tutorial App Menu -->
<menuitem id="demo_model_tutorial_menu"
name="Demo Model Tutorial" />
<!-- Action to open the demo_model_tutorial -->
<act_window id="action_demo_model_tutorial"
name="Demo Model Tutorial Action"
res_model="demo.model.tutorial"
view_mode="tree,form"/>
<!-- Menu item to open the demo_model_tutorial -->
<menuitem id="menu_demo_model_tutorial"
name="Demo Model Tutorial"
action="action_demo_model_tutorial"
parent="demo_model_tutorial_menu" />
</odoo>
================================================
FILE: demo_abstractmodel_v2_tutorial/views/view.xml
================================================
<?xml version="1.0"?>
<odoo>
<record id="view_form_demo_model_tutorial" model="ir.ui.view">
<field name="name">Demo Model Tutorial Form</field>
<field name="model">demo.model.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Tutorial">
<sheet>
<group>
<field name="name"/>
<field name="test_1"/>
<field name="test_2"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_tree_demo_model_tutorial" model="ir.ui.view">
<field name="name">Demo Model Tutorial List</field>
<field name="model">demo.model.tutorial</field>
<field name="priority" eval="1"/>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="test_1"/>
<field name="test_2"/>
</tree>
</field>
</record>
</odoo>
================================================
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
......
<odoo>
<data>
<record id="action_server_demo" model="ir.actions.server">
<field name="name">Action Demo</field>
<field name="model_id" ref="model_demo_actions_singleton"/>
<field name="binding_model_id" ref="demo_actions_singleton.model_demo_actions_singleton"/>
<field name="state">code</field>
<field name="code">
records.action_demo()
</field>
</record>
<record id="action_other_model_demo" model="ir.actions.server">
<field name="name">Action Other Demo</field>
<field name="model_id" ref="model_demo_actions_singleton"/>
<field name="binding_model_id" ref="hr_expense.model_hr_expense"/>
<field name="state">code</field>
<field name="code">
raise Warning('Hello')
</field>
</record>
</data>
</odoo>
```
`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`
`<field name="binding_model_id" ref="hr_expense.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
================================================
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="action_server_demo" model="ir.actions.server">
<field name="name">Action Demo</field>
<field name="model_id" ref="model_demo_actions_singleton"/>
<field name="binding_model_id" ref="demo_actions_singleton.model_demo_actions_singleton"/>
<field name="state">code</field>
<field name="code">
records.action_demo()
</field>
</record>
<record id="action_other_model_demo" model="ir.actions.server">
<field name="name">Action Other Demo</field>
<field name="model_id" ref="model_demo_actions_singleton"/>
<field name="binding_model_id" ref="hr_expense.model_hr_expense"/>
<field name="state">code</field>
<field name="code">
raise Warning('Hello')
</field>
</record>
</data>
</odoo>
================================================
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
================================================
<?xml version="1.0" ?>
<odoo>
<record id="module_demo_actions_singleton" model="ir.module.category">
<field name="name">Demo actions singleton category</field>
</record>
<record id="demo_actions_singleton_group_user" model="res.groups">
<field name="name">User</field>
<field name="category_id"
ref="module_demo_actions_singleton"/>
<field name="implied_ids"
eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="demo_actions_singleton_group_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id"
ref="module_demo_actions_singleton"/>
<field name="implied_ids"
eval="[(4, ref('demo_actions_singleton_group_user'))]"/>
<field name="users"
eval="[(4, ref('base.user_root')),
(4, ref('base.user_admin'))]"/>
</record>
</odoo>
================================================
FILE: demo_actions_singleton/views/menu.xml
================================================
<?xml version="1.0"?>
<odoo>
<!-- demo_actions_singleton App Menu -->
<menuitem id="demo_actions_singleton_menu"
name="Demo Actions Singleton" />
<!-- Action to open the demo_actions_singleton -->
<act_window id="action_singleton"
name="Demo Actions Singleton Action"
res_model="demo.actions.singleton"
view_mode="tree,form"/>
<!-- Menu item to open the demo_actions_singleton -->
<menuitem id="menu_action_singleton"
name="Demo Actions Singleton"
action="action_singleton"
parent="demo_actions_singleton_menu" />
</odoo>
================================================
FILE: demo_actions_singleton/views/view.xml
================================================
<?xml version="1.0"?>
<odoo>
<record id="view_form_demo_actions_singleton" model="ir.ui.view">
<field name="name">Demo Actions Singleton Form</field>
<field name="model">demo.actions.singleton</field>
<field name="arch" type="xml">
<form string="Demo Actions Singleton">
<sheet>
<group>
<field name="name"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_tree_demo_actions_singleton" model="ir.ui.view">
<field name="name">Demo Actions Singleton List</field>
<field name="model">demo.actions.singleton</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
</tree>
</field>
</record>
</odoo>
================================================
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
......
<data noupdate="0">
<record id="mail_act_approval" model="mail.activity.type">
<field name="name">Activity Approval</field>
<field name="icon">fa-dollar</field>
<field name="res_model_id" ref="demo_activity.model_demo_activity"/>
</record>
</data>
......
```
`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
......
<record id="view_activity_form" model="ir.ui.view">
<field name="name">demo.activity.form</field>
<field name="model">demo.activity</field>
<field eval="25" name="priority"/>
<field name="arch" type="xml">
<form string="Demo Activity">
<header>
<button name="button_activity_schedule" string="activity schedule" type="object" class="oe_highlight"/>
<button name="button_activity_unlink" string="activity unlink" type="object" class="oe_highlight"/>
<button name="button_activity_feedback" string="activity feedback" type="object" class="oe_highlight"/>
</header>
<sheet>
<group>
<field name="name"/>
<field name="employee_id"/>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="activity_ids" widget="mail_activity"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
......
```
`<button name="button_activity_schedule" string="activity schedule" type="object" class="oe_highlight"/>`
`name` 就是對應 model 中的 function 的名稱, 像這邊就是對應 `demo.activity` model 中的
`button_activity_schedule` function.
`string` 定義 button 的名稱.
最後的這段之前也說過了,
```xml
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="activity_ids" widget="mail_activity"/>
<field name="message_ids" widget="mail_thread"/>
</div>
```
就是顯示下面的那段

最後記得也要設定 `__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
================================================
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<record id="mail_act_approval" model="mail.activity.type">
<field name="name">Activity Approval</field>
<field name="icon">fa-dollar</field>
<field name="res_model_id" ref="demo_activity.model_demo_activity"/>
</record>
</data>
</odoo>
================================================
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
================================================
<?xml version="1.0" ?>
<odoo>
<record id="module_demo_activity_category" model="ir.module.category">
<field name="name">Demo Activity Category</field>
</record>
<record id="demo_activity_group_user" model="res.groups">
<field name="name">User</field>
<field name="category_id"
ref="module_demo_activity_category"/>
<field name="implied_ids"
eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="demo_activity_group_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id"
ref="module_demo_activity_category"/>
<field name="implied_ids"
eval="[(4, ref('demo_activity_group_user'))]"/>
<field name="users"
eval="[(4, ref('base.user_root')),
(4, ref('base.user_admin'))]"/>
</record>
</odoo>
================================================
FILE: demo_activity/views/menu.xml
================================================
<?xml version="1.0"?>
<odoo>
<menuitem id="demo_activity_menu"
name="Demo Activity"/>
<act_window id="action_demo_activity"
name="Activity Demo"
res_model="demo.activity"
view_mode="tree,form"/>
<menuitem id="menu_demo_activity"
name="Activity Demo"
action="action_demo_activity"
parent="demo_activity_menu" />
</odoo>
================================================
FILE: demo_activity/views/view.xml
================================================
<?xml version="1.0"?>
<odoo>
<record id="view_activity_form" model="ir.ui.view">
<field name="name">demo.activity.form</field>
<field name="model">demo.activity</field>
<field eval="25" name="priority"/>
<field name="arch" type="xml">
<form string="Demo Activity">
<header>
<button name="button_activity_schedule" string="_activity schedule" type="object" class="oe_highlight"/>
<button name="button_activity_unlink" string="activity unlink" type="object" class="oe_highlight"/>
<button name="button_activity_feedback" string="activity feedback" type="object" class="oe_highlight"/>
</header>
<sheet>
<group>
<field name="name"/>
<field name="employee_id"/>
</group>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="activity_ids" widget="mail_activity"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
</odoo>
================================================
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
......
<record id="view_expenses_tree_custom" model="ir.ui.view">
<field name="name">hr.expense.tree.custom</field>
<field name="model">hr.expense</field>
<field name="inherit_id" ref="hr_expense.view_expenses_tree"/>
<field name="arch" type="xml">
<field name="date" position="after">
<!-- <field name="test_field" groups="product.group_sale_pricelist" readonly="1"/> -->
<field name="test_field"/>
</field>
<!-- xpath the same result -->
<!--views/views.xml
<xpath expr="//field[@name='date']" position="after">
<field name="test_field" />
</xpath>
-->
</field>
</record>
......
```

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

## 延伸閱讀
* [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
================================================
<odoo>
<data>
<record id="view_expenses_tree_custom" model="ir.ui.view">
<field name="name">hr.expense.tree.custom</field>
<field name="model">hr.expense</field>
<field name="inherit_id" ref="hr_expense.view_expenses_tree"/>
<field name="arch" type="xml">
<field name="date" position="after">
<!-- <field name="test_field" groups="product.group_sale_pricelist" readonly="1"/> -->
<field name="test_field"/>
</field>
<!-- xpath the same result -->
<!--views/views.xml
<xpath expr="//field[@name='date']" position="after">
<field name="test_field" />
</xpath>
-->
</field>
</record>
<record id="hr_expense_view_form_custom" model="ir.ui.view">
<field name="name">hr.expense.view.form.custom</field>
<field name="model">hr.expense</field>
<field name="inherit_id" ref="hr_expense.hr_expense_view_form"/>
<field name="arch" type="xml">
<field name="employee_id" position="after">
<field name="test_field"/>
</field>
</field>
</record>
</data>
</odoo>
================================================
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
<record id="group_delivery_invoice_address" model="res.groups">
<field name="name">Addresses in Sales Orders</field>
<field name="category_id" ref="base.module_category_hidden"/>
</record>
```
當這個 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
......
<record id="demo_config_settings_tutorial_group" model="res.groups">
<field name="name">Config Settings User</field>
<field name="category_id" ref="base.module_category_hidden"/>
<field name="users"
eval="[(4, ref('base.user_root')),
(4, ref('base.user_admin'))]"/>
</record>
......
```
`<field name="category_id" ref="base.module_category_hidden"/>`
這代表這個 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
================================================
<?xml version="1.0" ?>
<odoo>
<record id="demo_config_settings_tutorial_group" model="res.groups">
<field name="name">Config Settings User</field>
<field name="category_id" ref="base.module_category_hidden"/>
<field name="users"
eval="[(4, ref('base.user_root')),
(4, ref('base.user_admin'))]"/>
</record>
</odoo>
================================================
FILE: demo_config_settings/views/view.xml
================================================
<?xml version="1.0"?>
<odoo>
<record id="settings_form_inherit_custom" model="ir.ui.view">
<field name="name">res.config.inherit.custom</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('settings')]" position="inside">
<div class="app_settings_block" data-string="Custom" string="Custom" data-key="Custom">
<div id="setup_demo">
<h2>Odoo Setup Demo</h2>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_right_pane">
<label string="Prefixes" for="Prefixes"/>
<span class="fa fa-lg"/>
<div class="text-muted">
Set your demo prefix.
</div>
<div class="content-group">
<div class="mt16 row">
<label for="demo_prefix" class="col-xs-3 col-md-6 o_light_label"/>
<field name="demo_prefix" class="oe_inline"/>
</div>
</div>
<div class="content-group">
<div class="mt16 row">
<label for="group_demo_config_setting" class="col-xs-3 col-md-6 o_light_label"/>
<field name="group_demo_config_setting" class="oe_inline"/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
================================================
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=<DstTzInfo 'Asia/Taipei' CST+8 :00:00 STD>)
>>> today = today.astimezone(timezone('UTC'))
>>> today
datetime.datetime(2022, 7, 7, 2, 0, tzinfo=<UTC>)
>>> ## 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=<UTC>)
>>> today = today.astimezone(timezone('Asia/Taipei'))
>>> today
datetime.datetime(2022, 7, 7, 18, 0, tzinfo=<DstTzInfo 'Asia/Taipei' CST+8 :00:00 STD>)
```
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=<DstTzInfo 'Asia/Taipei' CST+8 :00:00 STD>)
```
將 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
================================================
<?xml version="1.0"?>
<odoo>
<!-- Demo Datetime Menu -->
<menuitem id="demo_datetime"
name="Demo Datetime" />
<!-- Action to open the Demo Datetime list -->
<act_window id="action_demo_datetime"
name="Demo Datetime"
res_model="demo.datetime"
view_mode="tree,form"/>
<!-- Menu item to open the Demo Datetime list -->
<menuitem id="menu_demo_datetime"
name="Demo Datetime"
action="action_demo_datetime"
parent="demo_datetime" />
</odoo>
================================================
FILE: demo_datetime_tutorial/views/view.xml
================================================
<?xml version="1.0"?>
<odoo>
<record id="view_demo_datetime_tutorial_form" model="ir.ui.view">
<field name="name">demo_datetime_tutorial_form</field>
<field name="model">demo.datetime</field>
<field name="arch" type="xml">
<form string="Demo Odoo Tutorial">
<header>
<button name="demo1" string="demo1" type="object"/>
</header>
<sheet>
<group>
<field name="name"/>
<field name="my_datetime"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_demo_datetime_tutorial_tree" model="ir.ui.view">
<field name="name">demo_datetime_tutorial_tree</field>
<field name="model">demo.datetime</field>
<field name="arch" type="xml">
<tree>
<field name="name"/>
<field name="my_datetime"/>
</tree>
</field>
</record>
</odoo>
================================================
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
......
<record id="view_form_demo_delegation_tutorial" model="ir.ui.view">
<field name="name">Demo Delegation Tutorial Form</field>
<field name="model">demo.delegation</field>
<field name="arch" type="xml">
<form string="Demo Delegation Tutorial">
<sheet>
<group>
<!-- res.partner -->
<field name="name"/>
<field name="company_id"/>
<!-- res.partner -->
<!-- demo.delegation -->
<field name="partner_id" invisible="1" attrs="{'required': [('id', '!=', False)]}"/>
<field name="first_name"/>
<!-- demo.delegation -->
</group>
</sheet>
</form>
</field>
</record>
......
```
其實它有點特殊, 在裡面甚至可以使用 (委派 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
================================================
<?xml version="1.0" ?>
<odoo>
<record id="module_demo_delegation_tutorial" model="ir.module.category">
<field name="name">Demo Delegation tutorial category</field>
</record>
<record id="demo_delegation_tutorial_group_user" model="res.groups">
<field name="name">User</field>
<field name="category_id"
ref="module_demo_delegation_tutorial"/>
<field name="implied_ids"
eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="demo_delegation_tutorial_group_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id"
ref="module_demo_delegation_tutorial"/>
<field name="implied_ids"
eval="[(4, ref('demo_delegation_tutorial_group_user'))]"/>
<field name="users"
eval="[(4, ref('base.user_root')),
(4, ref('base.user_admin'))]"/>
</record>
</odoo>
================================================
FILE: demo_delegation_inheritance/views/menu.xml
================================================
<?xml version="1.0"?>
<odoo>
<!-- demo_delegation_tutorial App Menu -->
<menuitem id="demo_delegation_tutorial_menu"
name="Demo Delegation Tutorial" />
<!-- Action to open the demo_delegation_tutorial -->
<act_window id="action_delegation_tutorial"
name="Demo Delegation Tutorial Action"
res_model="demo.delegation"
view_mode="tree,form"/>
<!-- Menu item to open the demo_delegation_tutorial -->
<menuitem id="menu_delegation_tutorial"
name="Demo Delegation Tutorial"
action="action_delegation_tutorial"
parent="demo_delegation_tutorial_menu" />
</odoo>
================================================
FILE: demo_delegation_inheritance/views/view.xml
================================================
<?xml version="1.0"?>
<odoo>
<record id="view_form_demo_delegation_tutorial" model="ir.ui.view">
<field name="name">Demo Delegation Tutorial Form</field>
<field name="model">demo.delegation</field>
<field name="arch" type="xml">
<form string="Demo Delegation Tutorial">
<sheet>
<group>
<!-- res.partner -->
<field name="name"/>
<field name="company_id"/>
<!-- res.partner -->
<!-- demo.delegation -->
<field name="partner_id" invisible="1" attrs="{'required': [('id', '!=', False)]}"/>
<field name="first_name"/>
<!-- demo.delegation -->
</group>
</sheet>
</form>
</field>
</record>
<record id="view_tree_demo_delegation_tutorial" model="ir.ui.view">
<field name="name">Demo Delegation Tutorial List</field>
<field name="model">demo.delegation</field>
<field name="arch" type="xml">
<tree>
<!-- res.partner -->
<field name="partner_id" invisible="1" attrs="{'required': [('id', '!=', False)]}"/>
<field name="name"/>
<field name="company_id"/>
<!-- res.partner -->
<!-- demo.delegation -->
<field name="first_name"/>
<!-- demo.delegation -->
</tree>
</field>
</record>
</odoo>
================================================
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=<object object>, relation=<object object>, column1=<object object>, column2=<object object>, string=<object object>, **kwargs)
#
# relation: database table name
#
# By default, the relationship table name is the two table names
# joined with an underscore and _rel appended at the end.
# In the case of our books or authors relationship, it should be named demo_expense_tutorial_demo_tag_rel.
### odoo 手把手教學 - Many2many - part2
tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id', string='Tges')
......
```
Many2many 比較多欄位, 我來說明一下,
`comodel_name` 為 `demo.tag` (需要對應的 model)
`relation` 為 `demo_expense_tag` (table 名稱),
Many2many 會多出一個 table, 這邊是針對 table 命名,
也就是 db 中的 table 名稱,

如果你沒填 `relation` 這個值, 預設的 table 名稱會是 model名稱 + comodel_name + `_rel`,
所以也就會是 `demo_expense_tutorial_demo_tag_rel`.
`column1` 為 `demo_expense_id`, `demo.expense.tutorial` table 中對應的 id.
`column2` 為 `tag_id`, `demo.tag` table 中對應的 id.
繼續看 [models/models.py](models/models.py)
```python
......
# Related (Reference) fields (不會存在 db)
# readonly default 為 True
# store default 為 False
gender = fields.Selection('Gender', related='employee_id.gender')
......
```
`fields.Selection` 就只是下拉選單而已, 比較特別的是 `related` 這個,
`related='employee_id.gender'` 這邊的意思是, 會自己去找 employee_id 中的 gender,
到 employee 中找到 gender 為 Male

DemoExpenseTutorial 中的 `gender` 自然會是 Male,

但要注意幾件事情,
`related` 預設的 field 是不會儲存在 db 中的, store default 為 False,
你在 table 中是找不到 `gender` field 的 (如下圖),
如果你想要儲存在 db 中的, 請另外設定 `store=Ture`,

然後 readonly default 為 True, 也就是說你是不可以去修改的,
( 如果要修改請去 employee 中找到 gender 修改 )

接著來看最後一個
### odoo 手把手教學 - One2many - part3
`One2many`
* [Youtube Tutorial - odoo 手把手教學 - One2many - part3](https://youtu.be/WiLdXP781N0)

[models/models.py](models/models.py)
一個 `demo.expense.sheet.tutorial` 可以對應很多個 `demo.expense.tutorial`
所以是 一(`demo.expense.sheet.tutorial`) 對 多(`demo.expense.tutorial`) 的關係,
```python
......
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'
name = fields.Char('Expense Demo Report Summary', required=True)
# One2many is a virtual relationship, there must be a Many2one field in the other_model,
# and its name must be related_field
expense_line_ids = fields.One2many(
'demo.expense.tutorial', # related model
'sheet_id', # field for "this" on related model
string='Expense Lines')
......
```
說明 expense_line_ids 裡面的參數意義,
`demo.expense.tutorial` 代表關連的 model (必填)
`sheet_id` 代表所關連 model 的 field (必填)
也就是說如果你要建立 One2many, 一定也要有一個 Many2one,
但如果建立 Many2one 則不一定要建立 One2many.
One2many 是一個虛擬的欄位, 你在資料庫中是看不到它的存在(如下圖)

你只會看到 Many2one 中的 sheet_id

[models/models.py](models/models.py), `demo.expense.tutorial` 中的 sheet_id
```python
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
......
sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report")
......
```
記得也要設定對應的 [security/ir.model.access.csv](security/ir.model.access.csv) 和 [security/security.xml](security/security.xml).
[views/view.xml](views/view.xml)
```xml
......
<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial Form</field>
......
<!-- <field name="tag_ids"/> -->
<field name="tag_ids" widget="many2many_tags"/> <!-- widget -->
<field name="sheet_id"/>
......
```
在 odoo 中很有多 widget, 大家可以改成其他的 widget 試試看, 像是 many2many_tags 的 widget

[views/view.xml](views/view.xml)
```xml
......
<record id="view_form_demo_expense_sheet_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Sheet Tutorial Form</field>
<field name="model">demo.expense.sheet.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Sheet Tutorial">
<sheet>
<group>
<field name="name"/>
</group>
<notebook>
<page string="Expense">
<field name="expense_line_ids">
<tree>
<field name="name"/>
<field name="employee_id"/>
<field name="tag_ids" widget="many2many_tags"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
......
```
在 `view_form_demo_expense_sheet_tutorial` 裡的 One2many 中的 expense_line_ids fields,
就把需要的欄位填進去即可,

### odoo 手把手教學 - One2many Editable Bottom and Top - part3-1
這邊補充一下 One2many 中的 Editable Bottom 和 Top
* [Youtube Tutorial - odoo 手把手教學 - One2many Editable Bottom and Top - part3-1](https://youtu.be/HJcBAFXQYVc)
[views/view.xml](views/view.xml)
```xml
<record id="view_form_demo_expense_sheet_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Sheet Tutorial Form</field>
<field name="model">demo.expense.sheet.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Sheet Tutorial">
<sheet>
......
<notebook>
<page string="Expense">
<field name="expense_line_ids" >
<tree>
<!-- <tree editable="top"> --> <!-- <<<<<<<<<<<< -->
<!-- <tree editable="bottom"> --> <!-- <<<<<<<<<<<< -->
<field name="name"/>
<field name="employee_id"/>
<field name="tag_ids" widget="many2many_tags"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
```
如果你加上 `editable` 這個參數, 當你新增 record 的時候, 就不會整個跳出視窗, 可以直接在裡面輸入
(或許比較好看 :smile:)

至於 `editable="bottom"` 和 `editable="top"` 的差別如下
`editable="top"` 一個新增的 record 會顯示在最上面

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

### odoo 手把手教學 - Search Filters - part4
接著來看 filter 的功能
* [Youtube Tutorial - odoo 手把手教學 - Search Filters - part4](https://youtu.be/zcWMs16p9Xw)
```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"/>
<field name="employee_id" filter_domain="['|', ('employee_id', 'ilike', self), ('user_id', 'ilike', self)]" string="User"/>
<filter name="filter_inactive" domain="[('active','=',False)]" string="Inactive"/>
<filter name="gender" domain="[('gender','=','male')]" string="Male"/>
<separator/>
<filter name="name" domain="[('name', 'ilike', 'a')]" string="Name_2"/>
<group expand="0" string="Group By">
<filter string="Sheet" name="sheet" domain="[]" context="{'group_by': 'sheet_id'}"/>
<filter string="Employee" name="employee" domain="[]" context="{'group_by': 'employee_id'}"/>
</group>
</search>
</field>
</record>
......
```
主要都是在 tree 中搜尋, 可參考上面的 code 去看對應的邏輯


`<field name="employee_id" filter_domain="['|', ('employee_id', 'ilike', self), ('user_id', 'ilike', self)]" string="User"/>`
特別說明一下這個, `self` 代表使用者輸入的內容.
`<separator>` 代表 `and`, 如果沒寫則代表 `or`.
`and`

`or`

`<filter string="Sheet" name="sheet" domain="[]" context="{'group_by': 'sheet_id'}"/>`
`<filter string="Employee" name="employee" domain="[]" context="{'group_by': 'employee_id'}"/>`
依照特定的 fields 分組

點選後的狀態

### odoo 手把手教學 - 說明 noupdate 以及 domain_force - part5
再來看看
[security/ir_rule.xml](security/ir_rule.xml)
* [Youtube Tutorial - odoo 手把手教學 - 說明 noupdate 以及 domain_force - part5](https://youtu.be/twn6zz3OeRs)
```xml
......
<data noupdate="1">
<record id="ir_rule_demo_expense_user" model="ir.rule">
<field name="name">Demo Expense User</field>
<field name="model_id" ref="model_demo_expense_tutorial"/>
<field name="domain_force">[('employee_id.user_id.id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('demo_expense_tutorial_group_user'))]"/>
</record>
<record id="ir_rule_demo_expense_manager" model="ir.rule">
<field name="name">Demo Expense Manager</field>
<field name="model_id" ref="model_demo_expense_tutorial"/>
<field name="domain_force">[(1, '=', 1)]</field>
<field name="groups" eval="[(4, ref('demo_expense_tutorial_group_manager'))]"/>
</record>
</data>
......
```
`noupdate="1"` (代表更新 addons 時, 不再更新, 除非你刪掉 record)
假如我們在安裝完 addons 之後, 已經存在 record 了, 這時候我們手動更新這些 record 的資料,
再去更新 addons, 這些 record 是不會有任何改變的.
(但是, 假如你刪掉 record, 再去更新 addons, record 會重新被安裝回來.)
`noupdate="0"` (代表更新 addons 時, 會保持更新, 也就是會被還原)
假如我們在安裝完 addons 之後, 已經存在 record 了, 這時候我們手動更新這些 record 的資料,
再去更新 addons, 這些 record 是會被改回原本的.
(但是, 假如你刪掉 record, 再去更新 addons, record 會重新被安裝回來.)
`id="ir_rule_demo_expense_user"` 第一段為針對 `demo_expense_tutorial_group_user`
限制 `domain_force`, 規則很簡單, 這類的 user 只能看到自己的單子, 也就是
`[('employee_id.user_id.id', '=', user.id)]`.
`id="ir_rule_demo_expense_manager"` 針對 `demo_expense_tutorial_group_manager`
限制 `domain_force`, 這邊比較特別 `[(1, '=', 1)]`, 代表沒有限制, 也就是全部的單子都
可以看到.
`demo` 用戶為 User, 所以只能看到自己的單子

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

接著補充說明一下, 在 [security/ir_rule.xml](security/ir_rule.xml) 中可以設定更細的權限管理
```xml
<record id="ir_rule_demo_expense_user" model="ir.rule">
<field name="name">Demo Expense User</field>
<field name="model_id" ref="model_demo_expense_tutorial"/>
<field name="domain_force">[('employee_id.user_id.id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('demo_expense_tutorial_group_user'))]"/>
<!-- Groups (no group = global) -->
<!-- <field name="global" eval="True"/> -->
<field eval="0" name="perm_unlink"/>
<field eval="1" name="perm_write"/>
<field eval="1" name="perm_read"/>
<field eval="1" name="perm_create"/>
</record>
```
預設的 rule 如果沒有特別設定權限, CRUD 都會是 true,
但也可以去分別設定 (如上教學),
像這邊給了 read, write, create 的權限 (沒給 delete 權限)

`<field name="global" eval="True"/>` 則代表 global,
基本上, no group = global.
### odoo 手把手教學 - 如何透過 button 呼叫 view, form - part6
接下來介紹前面跳過的部份, 也就是透過 button 的方式呼叫 view, form,
* [Youtube Tutorial - odoo 手把手教學 - 如何透過 button 呼叫 view, form - part6](https://youtu.be/URxuH2HG44Q)
[models/models.py](models/models.py)
```python
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
......
@api.multi
def button_sheet_id(self):
return {
'view_mode': 'form',
'res_model': 'demo.expense.sheet.tutorial',
'res_id': self.sheet_id.id,
'type': 'ir.actions.act_window'
}
```
透過前端呼叫 `button_sheet_id`, 會回傳屬於它的 sheet_id

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

[views/view.xml](views/view.xml)
前端的部份就是呼叫 `button_sheet_id`
```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>
```
既然找了 sheet_id, 也來做一個反查回來的, 也就是透過 sheet_id 找到 `demo.expense.tutorial`,
[models/models.py](models/models.py)
```python
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'
......
@api.multi
def button_line_ids(self):
return {
'name': 'Demo Expense Line IDs',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': 'demo.expense.tutorial',
'view_id': False,
'type': 'ir.actions.act_window',
'domain': [('sheet_id', '=', self.id)],
}
......
```
`res_model` 為目標的 model `demo.expense.tutorial`.
`domain` 稍微說明一下 `[('sheet_id', '=', self.id)],`,
`sheet_id` 是指目標 model `demo.expense.tutorial` 的 sheet_id,
`self.id` 是指當下 model `demo.expense.sheet.tutorial` 的 id.

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

[views/view.xml](views/view.xml) 的部份
```xml
......
<record id="view_form_demo_expense_sheet_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Sheet Tutorial Form</field>
<field name="model">demo.expense.sheet.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Sheet Tutorial">
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button" name="button_line_ids"
string="SHEET IDs" type="object"
attrs="{'invisible':[('expense_line_ids','=', False)]}" icon="fa-bars"/>
</div>
......
</sheet>
</form>
</field>
</record>
```
### odoo 手把手教學 - 說明 name_get 和 _name_search - part7
最後來看 [models/models.py](models/models.py) 中比較特殊的部份,
* [Youtube Tutorial - odoo 手把手教學 - 說明 name_get 和 _name_search - part7](https://youtu.be/g-dclCkwY5c)
分別是 `name_get` 和 `_name_search`,
```python
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'
......
@api.multi
def name_get(self):
names = []
for record in self:
name = '%s-%s' % (record.create_date.date(), record.name)
names.append((record.id, name))
return names
# odoo12/odoo/odoo/addons/base/models/ir_model.py
@api.model
def _name_search(self, name='', args=None, operator='ilike', limit=100):
if args is None:
args = []
domain = args + ['|', ('id', operator, name), ('name', operator, name)]
# domain = args + [ ('name', operator, name)]
# domain = args + [ ('id', operator, name)]
return super(DemoExpenseSheetTutorial, self).search(domain, limit=limit).name_get()
```
首先是 `name_get`
這個的功能主要是去修改 name 的名稱, 在這邊我們加上當下的時間
(可以依照自己的需求下去修改)

Many2one 時也會看到自己定義的 `name_get`
注意 :exclamation: 這些增加的值是不會儲存進 db 中的, db 中還是儲存的是 name 的內容而已
(概念和 compute field 一樣 :smile:)

再來要來說明 `_name_search`,
如果沒有它, 假設我知道某個資料的 id 是 4, 在搜尋的地方打上 id,
你會發現找不到資料 :joy:

但今天如果有了 `_name_search` 並實作它,
你會發現這次你打 id 會才成功找到需要的資料 :satisfied:
我在 code 中有放幾個範例註解, 大家可以自行玩玩看 :smile:

### odoo 手把手教學 - 使用 python 增加取代 One2many M2X record - part8
* [Youtube Tutorial - odoo 手把手教學 - 使用 python 增加取代 One2many M2X record - part8](https://youtu.be/GBCGS2znnT8)
參考 [models/models.py](models/models.py)
這邊只需要注意3個 function,
`add_demo_expense_record` `link_demo_expense_record` `replace_demo_expense_record`
分別對應的 button 為下圖
參考 [views/view.xml](views/view.xml)

```python
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'
name = fields.Char('Expense Demo Report Summary', required=True)
# One2many is a virtual relationship, there must be a Many2one field in the other_model,
# and its name must be related_field
expense_line_ids = fields.One2many(
'demo.expense.tutorial', # related model
'sheet_id', # field for "this" on related model
string='Expense Lines')
@api.multi
def add_demo_expense_record(self):
# (0, _ , {'field': value}) creates a new record and links it to this one.
data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
tag_data_1 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_1')
tag_data_2 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_2')
for record in self:
# creates a new record
val = {
'name': 'test_data',
'employee_id': data_1.employee_id,
'tag_ids': [(6, 0, [tag_data_1.id, tag_data_2.id])]
}
self.expense_line_ids = [(0, 0, val)]
@api.multi
def link_demo_expense_record(self):
# (4, id, _) links an already existing record.
data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
for record in self:
# link already existing record
self.expense_line_ids = [(4, data_1.id, 0)]
@api.multi
def replace_demo_expense_record(self):
# (6, _, [ids]) replaces the list of linked records with the provided list.
data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
data_2 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_2')
for record in self:
# replace multi record
self.expense_line_ids = [(6, 0, [data_1.id, data_2.id])]
```
說明 `add_demo_expense_record`
```python
......
@api.multi
def add_demo_expense_record(self):
# (0, _ , {'field': value}) creates a new record and links it to this one.
data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
tag_data_1 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_1')
tag_data_2 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_2')
for record in self:
# creates a new record
val = {
'name': 'test_data',
'employee_id': data_1.employee_id,
'tag_ids': [(6, 0, [tag_data_1.id, tag_data_2.id])]
}
self.expense_line_ids = [(0, 0, val)]
......
```
`(0, _ , {'field': value})` 新建一筆 record 並且連接它.
`self.env.ref(......)` 這個的用法是去取得既有的資料, 路徑在 [data/demo_expense_tutorial_data.xml](data/demo_expense_tutorial_data.xml).
當你點選按鈕, 下面就會一直新增資料

說明 `link_demo_expense_record`
```python
......
@api.multi
def link_demo_expense_record(self):
# (4, id, _) links an already existing record.
data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
for record in self:
# link already existing record
self.expense_line_ids = [(4, data_1.id, 0)]
......
```
`(4, id, _)` 連接已經存在的 record.
當你點選按鈕, 下面會直接連接一比資料, 如果已經連接就不會有動作,

說明 `replace_demo_expense_record`
```python
......
@api.multi
def replace_demo_expense_record(self):
# (6, _, [ids]) replaces the list of linked records with the provided list.
data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')
data_2 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_2')
for record in self:
# replace multi record
self.expense_line_ids = [(6, 0, [data_1.id, data_2.id])]
......
```
`(6, _, [ids])` 使用 list 取代既有的 records.
當你點選按鈕, 會使用你定義的 list 取代全部的 records.

### odoo 手把手教學 - tree create delete edit False - part9
* [Youtube Tutorial - odoo 手把手教學 - tree create delete edit False - part9](https://youtu.be/0fpA89QcYZM)
通常管理一個使用者可不可以建立 records, 是根據 security 資料夾裡面的檔案,
也就是 `security.xml` `ir_rule.xml` `ir.model.access.csv`.
記住 :exclamation: odoo 可以從 model 層(db層) 或權限下手, 也可以從 view 那層下手,
當然, 如果是從安全性的角度來看 從 model 層(db層) 或權限下手 是比較高全的 :smile:
今天就是要來介紹 從 view 那層下手,
增加一個 tree [views/view.xml](views/view.xml)
```xml
......
<record id="view_tree_demo_expense_tutorial_no_create" model="ir.ui.view">
<field name="name">Demo Expense Tutorial List No Create</field>
<field name="model">demo.expense.tutorial</field>
<field name="arch" type="xml">
<tree string="no_create_tree" create="0" delete="false" edit="1" editable="top">
<field name="name"/>
<field name="employee_id"/>
</tree>
</field>
</record>
......
```
重點在 `<tree string="no_create_tree" create="0" delete="false" edit="1" editable="top">`
這段, 裡面增加了一下 tag, 允許就是 `1` 或 `True`, 不允許就是 `0` 或 `False`.
儘管你有權限建立 records, 如果你設定了 `create="0"`, 你還是沒辦法建立 records.
也記得在 [views/menu.xml](views/menu.xml) 增加 action,
並且要指定 `view_id` (也就是剛剛建立出來的那個)
```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>
......
```
你會發現 create delete 的按鈕都消失了

### odoo 手把手教學 - 同一個 model 使用不同的 view_ids - part10
* [Youtube Tutorial - odoo 手把手教學 - 同一個 model 使用不同的 view_ids - part10](https://youtu.be/YltcAu9OZhc)
一般來說, 在定義一個 model 時, 通常會搭配一個 form 的 view 以及 tree 的 view, 或是特別指定一個 view,
像是前面介紹到的 view_id, 但有時候會有這種情況, 也就是一個 model, 在兩個不同的地方, 分別顯示不同的
form 的 view 以及 tree 的 view, 這時候就要使用 view_ids 分別下去定義.
現在 meun 上會多出 Demo Expense Tutorial View ids, 點下去分別有
Demo Expense Tutorial View id 1 以及 Demo Expense Tutorial View id 2
他們都是屬於 `demo.expense.tutorial` model, 只不過使用了不同的 view 和 form,

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

view 2

可參考 [views/menu.xml](views/menu.xml)
```xml
......
<!-- Action to open the menu_expense_tutorial_view_id_1 -->
<record id="action_expense_tutorial_view_id_1" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action View id 1</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.tree_expense_view_id_1')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.form_expense_view_id_1')})]"/>
</record>
......
<!-- Action to open the menu_expense_tutorial_view_id_2 -->
<record id="action_expense_tutorial_view_id_2" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action View id 2</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.tree_expense_view_id_2')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.form_expense_view_id_2')})]"/>
</record>
......
```
看起來雖然很複雜, 但其實不難, 設定都和之前的一樣, 只是將 view_id 換成了 view_ids, 然後分別設定不同的 view_id,
這邊只有分別設定 tree 和 form, 如果你想要定義新的 kanban 或其他的 view_mode 也都是可以的.
`eval="[(5, 0, 0)` 的意思是清除所有和它有關的 record (因為我們重新定義了需要的 view),
相關說明可參考
```xml
(0, 0, { values }) link to a new record that needs to be created with the given values dictionary
(1, ID, { values }) update the linked record with id = ID (write *values* on it)
(2, ID) remove and delete the linked record with id = ID (calls unlink on ID, that will delete the object completely, and the link to it as well)
(3, ID) cut the link to the linked record with id = ID (delete the relationship between the two objects but does not delete the target object itself)
(4, ID) link to existing record with id = ID (adds a relationship)
(5) unlink all (like using (3,ID) for all linked records)
(6, 0, [IDs]) replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)
```
你也可以在 Technical -> Actions -> Window Actions 看到你所設定的 view_ids

這些就是剛剛的設定

這邊補充一個小技巧, 在定義 view 時, 有一個參數是 `<field name="priority" eval="1"/>`, 如果你只有一個 view 不需要特別設定,
但如果你有很多個, 你可以透過這個 priority 去決定顯示 view 的優先權.
### odoo 手把手教學 - widget 介紹 handle 和 many2onebutton - part11
* [Youtube Tutorial - odoo 手把手教學 - widget 介紹 handle 和 many2onebutton - part11](https://youtu.be/zb5fSEtEo_g)
在 odoo 中很非常多的 widget 可以使用, 除了像前面介紹的 `widget="many2many_tags"` 之外, 這邊再介紹另外兩個,
首先是 handle widget,
這個比較常和 sequence 搭配一起使用,
可參考 [models/models.py](models/models.py)
```python
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
_order = "sequence, id desc"
......
sequence = fields.Integer(index=True, help="Gives the sequence order", default=1)
```
定義了一個 sequence fields, 然後排序使用 sequence.
在 tree view 中加入 `widget="handle"`,
可參考 [views/view.xml](views/view.xml)
```xml
<record id="view_tree_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial List</field>
<field name="model">demo.expense.tutorial</field>
<field name="priority" eval="1"/>
<field name="arch" type="xml">
<tree>
<!-- <tree default_order="sequence, id desc"> -->
<field name="sequence" widget="handle"/>
......
</tree>
</field>
</record>
```
這邊補充一下, 除了在 model 中定義 order 之外, 也可以在 tree, kanban 上定義,
像是 `<tree default_order="sequence, id desc">`.
這樣就完成了, 你會發現 tree 可以排序了 :smile:

再來是 many2onebutton widget,
通常如果一個 tree view 上有 many2one 的 fields, 如果想看這個 fields 的資料,
必須要點進去 form, 再點進去 many2one 的 fields 才看的到資料,
如果在 tree 的 many2one fields 上加入 `widget="many2onebutton"`,
就可以直接點進去觀看該 fields 的資料.
可參考 [views/view.xml](views/view.xml)
```xml
<record id="view_tree_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial List</field>
<field name="model">demo.expense.tutorial</field>
<field name="priority" eval="1"/>
<field name="arch" type="xml">
<tree>
......
<field name="sheet_id" widget="many2onebutton"/>
</tree>
</field>
</record>
```
你會發現 many2one fields 變藍色的了, 直接點選即可.

### odoo 手把手教學 - view 搭配 context - part12
* [Youtube Tutorial - odoo 手把手教學 - view 搭配 context - part12](https://youtu.be/c-nzbAuaH9I)
這部份將介紹 view 搭配 context 的使用,
可參考 [views/menu.xml](views/menu.xml)
```xml
<!-- Action to open the demo_expense_tutorial_context -->
<record id="action_expense_tutorial_context" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action Context</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<!-- init search default -->
<field name="context">{'search_default_name': 'test123'}</field>
<!-- init create default name 'test123'-->
<!-- <field name="context">{'default_name': 'test123'}</field> -->
</record>
```
來說明一下 `<field name="context">{'search_default_name': 'test123'}</field>`
這段程式碼, `name` 是我定義的 fields, 格式是 `search_default + fields`,
也就是進入這個 view 的時候, 預設會幫你搜尋 `name` 吻合 `test123`.

接著來看另一個, `<field name="context">{'default_name': 'test123'}</field>`
這段程式碼, 格式是 `default + fields`, 注意哦, 這次沒有 search,
那這個和剛剛的有什麼不同呢 :question:
當你建立一個 records 的時候, 他預設會幫你的 `name` fields 自動帶入 `test123`.

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

### odoo 手把手教學 - view 搭配 active_test context - part13
* [Youtube Tutorial - odoo 手把手教學 - view 搭配 active_test context - part13](https://youtu.be/RR9ycgky444)
這部份延續上一次的介紹, 來看看 `active_test` 這個東西,
這部份建議大家看影片會比較清楚 :smile:
先來看 [models/models.py](models/models.py)
```python
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
_order = "sequence, id desc"
......
active = fields.Boolean(default=True, help="Set active.")
```
通常如果有定義 active fields,
預設的情況下, 你的 view 就是會顯示只有 `active=True` 的 records,
如果你要顯示 `active=False` 的 records,
則必須另外去 filter 出來, 如下圖

這樣你可能會問我, 為什麼會這樣呢 :question:
原因是 odoo 原始碼內的 `odoo/models.py` 這段
```python
@api.model
def _where_calc(self, domain, active_test=True):
......
if 'active' in self._fields and active_test and self._context.get('active_test', True):
# the item[0] trick below works for domain items and '&'/'|'/'!'
# operators too
if not any(item[0] == 'active' for item in domain):
domain = [('active', '=', 1)] + domain
```
預設如果沒有特別指定, 邏輯就是會跑 `active = 1` 也就是 True.
那如果我今天希望預設顯示 active 為 True 和 False 同時都顯示, 這樣要如何實作 :question:
搭配 `<field name="context">{'active_test':False}</field>` 這段程式碼,
可參考 [views/menu.xml](views/menu.xml)
```xml
<!-- Action to open the demo_expense_tutorial_test_active -->
<record id="action_expense_tutorial_test_active" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action Test Active</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<!-- init show all (active True False) record -->
<field name="context">{'active_test':False}</field>
<!-- init show only (active True) record -->
<!-- <field name="context">{}</field> -->
</record>
```
這樣子預設就會把全部的 records (不管 active 狀態) 都顯示出來.
context 同樣也可以在 developer mode 中的 Edit Action 看到,

### odoo 手把手教學 - view 搭配 domain - part14
* [Youtube Tutorial - odoo 手把手教學 - view 搭配 domain - part14](https://youtu.be/Rh-rmXIHTZo)
這部份將介紹 view 搭配 domain 的使用,
使用方法和 context 差不多 :smile:
可參考 [views/menu.xml](views/menu.xml)
```xml
<!-- Action to open the demo_expense_tutorial_domain -->
<record id="action_expense_tutorial_domain" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action Domain</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('name', 'like', 'test')]</field>
<field name="context">{}</field>
</record>
```
說明 `<field name="domain">[('name', 'like', 'test')]</field>` 這段程式碼,
只會顯示 name fields like test 的內容,
注意 :exclamation: 和 `search_default_name` 不一樣的地方是, 他不會顯示 search 的東西,
使用者也不能自行修改

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

### odoo 手把手教學 - 如何看到當下 view 繼承頁面 - part15
* [Youtube Tutorial - odoo 手把手教學 - 如何看到當下 view 繼承頁面 - part15](https://youtu.be/Vs6ScbYuZNs)
有時候當我們寫了很多的繼承 ( tree 或 form), 在當下的頁面, 會不知道是否有被繼承過,
這時候推薦大家一個小技巧 :smile:
使用 [demo_class_inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_class_inheritance) 這個 addons 當作範例.
首先, 先進去你想要查看的頁面, 這邊進入 hr_expnese

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)
點選 Edit View: List

點選 Inherited Views 這個 tab

你就可以很清楚的看到這個頁面被 `demo_class_inheritance` 繼承 :smile:
像是 form 或其他的 view_type 也都是同樣的方法哦 :smirk:
### odoo 手把手教學 - odoo rainbow - part16
* [Youtube Tutorial - odoo 手把手教學 - odoo rainbow - part16](https://youtu.be/g4vywRLklE0)
在 odoo 中也有特效這個東西
```python
@api.multi
def button_rainbow_man(self):
return {
'effect': {
'fadeout': 'slow',
'message': 'hello',
'type': 'rainbow_man',
}
}
```

### odoo 手把手教學 - tree decoration - part17
* [Youtube Tutorial - odoo 手把手教學 - tree decoration - part17](https://youtu.be/tJdw6IEb8UQ)
在 odoo 中有很多的 decoration 可以使用, 通常是搭配 tree 顯示特殊的資料.
使用方法非常的簡單, 直接加上需要顯示的邏輯即可,
可參考 [views/view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/views/view.xml)
```xml
......
<record id="view_tree_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial List</field>
<field name="model">demo.expense.tutorial</field>
<field name="priority" eval="1"/>
<field name="arch" type="xml">
<tree decoration-info="'info' in name" decoration-muted="'muted' in name" decoration-danger="'danger' in name" decoration-bf="'bf' in name" decoration-warning="'warning' in name" decoration-success="'success' in name">
......
</tree>
</field>
</record>
......
```

關於 `decoration-{$name}` 的詳細說明, 可參考官方文件 [Advanced Views](https://www.odoo.com/documentation/12.0/developer/howtos/backend.html#advanced-views)
### odoo 手把手教學 - model _rec_name 說明 - part18
* [Youtube Tutorial - odoo 手把手教學 - model _rec_name 說明 - part18](https://youtu.be/JtcSnbHNjAU)
今天要和大家介紹在 model 中有時會看到的 `_rec_name`,
[models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py)
```python
......
class DemoTag(models.Model):
_name = 'demo.tag'
_description = 'Demo Tags'
_rec_name = 'complete_name'
name = fields.Char(string='Tag Name', index=True, required=True)
complete_name = fields.Char('Complete Name', compute='_compute_complete_name')
active = fields.Boolean(default=True, help="Set active.")
@api.depends('name')
def _compute_complete_name(self):
for record in self:
record.complete_name = 'hello world - {}'.format(record.name)
......
```
首先, 你需要知道一件事, 如果你建立一個 model, 該 model 沒有特別定義 `name`, 且沒另外指定 `_rec_name`
(如果沒特別指定 `_rec_name`, default 就是使用 `name`)
通常這時候 odoo 的訊息會提醒你建議你設定 `name` field 或是指定 `_rec_name`.

但這只是 WARNING, 你也可以不要理他.
但我的建議是, 如果你不指定 `name`, 就請特別去指定 `_rec_name`.
當然, 如果你有設定了 `name`, 你也可以特別去指定 `_rec_name` 為其他的 field.
這個 `name` 和 `_rec_name` 只是指定顯示的名稱而已.
詳細的 demo 差異可以看影片的說明.
### odoo 手把手教學 - copy override 說明 - part19
* [Youtube Tutorial - odoo 手把手教學 - copy override 說明 - part19](https://youtu.be/VDnIFb7e7wM)
在 odoo 中有幾個比較特殊的 function, 分別是 `create` `write` `copy` `unlink`,
`create` 建立一比 record 時.
`write` 更新一比 record 時.
`copy` 複製一比 record 時.
`unlink` 刪除一比 record 時.
今天來介紹 `copy` 當作範例, 其他的大家可以以此類推 :smile:
```python
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
_order = "sequence, id desc"
name = fields.Char('Description', required=True)
......
tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id', string='Tges', copy=False)
......
@api.multi
def copy(self, default=None):
default = dict(default or {})
if not default.get('name'):
default['name'] = '{} copy'.format(self.name)
return super(DemoExpenseTutorial, self).copy(default)
```
當點選 Duplicate 時, 會觸發這個 `copy`
(在這邊做的事情是 override, 和之前介紹的繼承觀念其實是差不多的)

你會發現 name 被加上 copy 了.

然後預設的 field 是 `copy=True`, 如果不想要他被 copy, 可以直接在 fields
上設定 `copy=False`.
最後要提醒大家, 在 odoo 中少用/小心使用 duplicate, 不然就是你要非常清楚裡面寫了甚麼,
因為我很常用到複製出來的 record 有問題, 可能是翻譯, 又可能是複製出來的這比很奇怪,
唯一的可能就是他的 copy 沒有寫好, 特殊的邏輯沒有補上去, 導致你複製出來的 record 行為很怪.
基本上在要修改 `create` `write` `copy` `unlink` 時, 可以先想想有沒有比較簡單的方式能
改動你的需求, 如果真的沒有, 才選擇改他 :smirk:
## odoo 手把手教學 - move position 說明 - part20
* [Youtube Tutorial - odoo 手把手教學 - move position 說明 - part20](https://youtu.be/l-bFOqTYgTA)
在 odoo 中常常容易使用到繼承的方式改寫 view, 最常見的 position 就是 after, before, replace,
但有時候會有一種狀況, 就是單純想要交換兩個既有的 fields 位置 (不使用直接在 odoo 上面改, 不推薦),
例如, 下面這個已經存在的 view, 希望交換 Employee 和 Description 的位置,

假設這個 view 只能使用繼承的方式修改, 這時候通常很麻煩, 因為有可能你的作法是把整個 tree 去 replace 掉,
再自己去排版, 但為了幾個 fields 就去 replace 掉整個 view, 真得有點麻煩 :expressionless:
所以, 今天來認識 move position 這個東西, 寫法可參考 [view.xml](views/view.xml)
```xml
<!-- change name, employee_id fields-->
<record id="view_tree_demo_expense_tutorial_move" model="ir.ui.view">
<field name="name">view_tree_demo_expense_tutorial_move</field>
<field name="model">demo.expense.tutorial</field>
<field name="inherit_id" ref="demo_expense_tutorial_v1.view_tree_demo_expense_tutorial"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="before">
<field name="employee_id" position="move"/>
</xpath>
</field>
</record>
```
基本上就是在要移動的 fields 上的後面加上 `position="move"` 即可, 效果如下

有了這個東西, 以後單純想要交換兩個 fields, 只需要使用 move 即可, 不需要再整個 replace 了 :smile:
## odoo 手把手教學 - ir.actions.act_url 說明 - part21
這部份很簡單, 只是要和大家說可以透過 `ir.actions.act_url` 來開始 url
可參考 [models/models.py](models/models.py)
```python
@api.multi
def button_act_url(self):
self.ensure_one()
return {
'type': 'ir.actions.act_url',
'target': 'new',
# 'target': 'self',
'url': 'https://github.com/twtrubiks/odoo-demo-addons-tutorial',
}
```
## odoo 手把手教學 - Smart Button 說明 - part22
* [Youtube Tutorial - odoo 手把手教學 - Smart Button 說明 - part22](https://youtu.be/fsZK1KRgnF0)
甚麼是 Smart Button :question: 如果你常用 odoo, 你一定常看到這個東西,
如下, 這就是所謂的 Smart Button

其實之前就有介紹過 Smart Button 了, 今天就再順便說明要如何設計底下的那個數字
首先, 你需要透過 compute 這個參數建立一個 fields (用來計算出這個數字)
[models/models.py](models/models.py)
```python
......
demo_expenses_count = fields.Integer(
compute='_compute_demo_expenses_count',
string='Demo Expenses Count')
......
def _compute_demo_expenses_count(self):
# usually used read_group
for record in self:
record.demo_expenses_count = len(self.expense_line_ids)
......
```
在這邊只是簡單的算出數量而已(為了demo), 註解在這邊的意思是說, 通常都會使用 read_group
來做計算 (可自行參考 odoo source code)
最後, 自行將這個 fields 放到 view 中即可, 可參考 [views/view.xml](views/view.xml)
```xml
......
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button"
name="button_line_ids"
type="object"
attrs="{'invisible':[('expense_line_ids','=', False)]}"
icon="fa-bars">
<field name="demo_expenses_count" widget="statinfo" string="Counts"/>
</button>
</div>
......
```
效果如下

## odoo 手把手教學 - options create_edit 說明 - part23
* [Youtube Tutorial - odoo 手把手教學 - options create_edit 說明 - part23](https://youtu.be/GdPKllI7quI)
今天要來介紹 `options` 這個參數,
請參考 [view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/views/view.xml)
```xml
<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial Form</field>
<field name="model">demo.expense.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Tutorial">
......
<field name="name"/>
<field name="employee_id"/>
<!-- <field name="employee_id" options="{'no_quick_create': True}"/> -->
<!-- <field name="employee_id" options="{'no_create_edit': True}"/> -->
<!-- <field name="employee_id" options="{'no_create': True}"/> -->
<!-- <field name="employee_id" options="{'no_open': True}"/> -->
......
</form>
</field>
</record>
```
在一般的情況下 (不設定任何的 `option`), 顯示如下,


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

`no_create_edit`

`no_create`

`no_open`

當然, 如果你的需求是多個組合, 也可以多個一起使用.
## odoo 手把手教學 - PostgreSQL ondelete cascade 說明 - part24
* [Youtube Tutorial - odoo 手把手教學 - PostgreSQL ondelete cascade 說明 - part24](https://youtu.be/OTh5R2LrwJE)
今天要介紹 Many2one fields 中的 `ondelete="cascade"` 參數代表的意思,
這邊要先說明一下, `ondelete='cascade'` 這個東西並不是 odoo 的, 它是 PostgreSQL 的特性 :exclamation:
使用方法很簡單, 如下, 可參考 [models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py)
```python
......
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
......
sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report", ondelete='cascade')
......
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
......
expense_line_ids = fields.One2many(
'demo.expense.tutorial', # related model
'sheet_id', # field for "this" on related model
string='Expense Lines')
......
```
在開始介紹之前, 大致上除了 `'cascade'` 之外, 還有其他幾個選項, 如下分別是
說明 `ondelete='set null'`
這個是 default, 可參考 odoo `fields.py` 中的 Many2one 說明(如下).
```python
......
class Many2one(_Relational):
""" The value of such a field is a recordset of size 0 (no
......
:param ondelete: what to do when the referred record is deleted;
possible values are: ``'set null'``, ``'restrict'``, ``'cascade'``
......
"""
......
_slots = {
'ondelete': 'set null', # what to do when value is deleted
'auto_join': False, # whether joins are generated upon search
'delegate': False, # whether self implements delegation
}
......
```
( 如果文章看的不是很清楚, 請參考影片中的說明, 我會一個一個說明 :sunglasses: )
說明 `ondelete='set null'` (預設行為)
如果直接刪除 `sheet_id` ( 底下有很多`expense_line_ids`), 可以成功刪除 `sheet_id`, 但你會發現
`expense_line_ids` 並沒有被刪除 ( `sheet_id` 變為 `null`).
說明 `ondelete='cascade'`
如果直接刪除 `sheet_id` ( 底下有很多`expense_line_ids`), 可以成功刪除 `sheet_id`, 且你會發現
`expense_line_ids` 也自動都被移除了.
說明 `ondelete='restrict'`
如果直接刪除 `sheet_id` ( 底下有很多`expense_line_ids`), 無法刪除 `sheet_id`,

你必需先移除 `sheet_id` 底下的 `expense_line_ids`, 才可以刪除 `sheet_id`.
其實, 你可以把他們想成是 child 和 parent 的關係即可 :smile:
要如何知道 fields 有 `ondelete='....'` 之類的特性呢 :question:
除了可以透過 code 或 odoo 的 model fields 中查看之外,

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

## odoo 手把手教學 - view parent 說明 - part26
* [Youtube Tutorial - odoo 手把手教學 - view parent 說明 - part26](https://youtu.be/i_hG4s_YJN0)
在 view 中可以透過 `parent` 這個值, 拿到 `parent` 的 `fields` 內容 (可能有點繞口 :smile:)
不懂沒關係, 請看以下的說明 :smile:
通常會使用在 view 中的 domain 或是 attrs,
首先, 先看一下 [models/models.py](models/models.py)
```python
......
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
_order = "sequence, id desc"
name = fields.Char('Description', required=True)
......
sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report", ondelete='restrict')
......
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'
name = fields.Char('Expense Demo Report Summary', required=True)
# One2many is a virtual relationship, there must be a Many2one field in the other_model,
# and its name must be related_field
expense_line_ids = fields.One2many(
'demo.expense.tutorial', # related model
'sheet_id', # field for "this" on related model
string='Expense Lines')
......
```
接著看 [views/view.xml](views/view.xml)
```xml
......
<record id="view_form_demo_expense_sheet_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Sheet Tutorial Form</field>
<field name="model">demo.expense.sheet.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Sheet Tutorial">
......
<group>
<field name="name"/>
</group>
<notebook>
<page string="Expense">
<field name="expense_line_ids" >
<!-- <tree> -->
<tree editable="top"> <!-- <<<<<<<<<<<< -->
<!-- <tree editable="bottom"> --> <!-- <<<<<<<<<<<< -->
<field name="name"/>
<field name="employee_id"/>
<field name="tag_ids" widget="many2many_tags" attrs="{'readonly': [('parent.name', '=', 'test-readonly')]}"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
......
```
主要請看 `<field name="tag_ids" widget="many2many_tags" attrs="{'readonly': [('parent.name', '=', 'test-readonly')]}"/>`
當 sheet 的 `name` 為 `test-readonly` 的時候, `tag_ids` 這個 fields 會變成 `readonly`.

請注意 :exclamation: 我們並沒有 `parent` 這個欄位, 但是在 view 中可以透過這種方式使用 parent (也就是 sheet ) 的東西.
當 sheet 的 `name` 不是 `test-readonly` 時, `tag_ids` 這個 fields 會變成可以 edit ( 不是`readonly` ).

另外一點要注意的是, 請搭配 `<tree editable="top">` 或 `<tree editable="bottom">`, 單純使用 `<tree>` 不會生效 :exclamation:
`editable` 的效果可參考之前的介紹 [odoo 手把手教學 - One2many Editable Bottom and Top](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)
## odoo 手把手教學 - domain 搭配 fields 的三種用法 - part27
* [Youtube Tutorial - odoo 手把手教學 - domain 搭配 fields 的三種用法 - part27](https://youtu.be/ZUNRoWxVWAE)
在 odoo 中 domain 幾乎無所不在 :smile: 今天和大家介紹三種 domain 搭配 fields 的用法,
第一種 - 直接在 model 中的 fileds 定義
[model/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py)
```python
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
......
employee_id = fields.Many2one('hr.employee', string="Employee", required=True,
domain=[('active', '=', True)] )
......
```
開啟 [odoo12 如何開啟 odoo developer mode](https://github.com/twtrubiks/odoo-docker-tutorial#odoo12-%E5%A6%82%E4%BD%95%E9%96%8B%E5%95%9F-odoo-developer-mode), 並且到 fields 上觀看, 會看到我們定義的 domain

第二種 - 直接在 view 中的 fileds 定義
[views/view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/views/view.xml)
```xml
<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial Form</field>
<field name="model">demo.expense.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Tutorial">
......
<field name="employee_id" domain="[('user_id', '=', user_id)]"/>
......
</group>
</sheet>
</form>
</field>
</record>
```
開啟 [odoo12 如何開啟 odoo developer mode](https://github.com/twtrubiks/odoo-docker-tutorial#odoo12-%E5%A6%82%E4%BD%95%E9%96%8B%E5%95%9F-odoo-developer-mode), 並且到 fields 上觀看, 會看到我們定義的 domain

:exclamation:這邊要注意的是, 如果第一種和第二種同時寫, 以第二種在 view 上定義的為主 :exclamation:
第三種 - 透過 `onchange` 的方法增加 domain
這種方法蠻酷的, 所以我留到最後來講 :smile:
首先, 如果不了解 `onchange` 可參考 [介紹 model](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#%E4%BB%8B%E7%B4%B9-model).
請看下面的範例 [model/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py)
```python
......
@api.onchange('user_id')
def onchange_user_id(self):
# domain
result = dict()
result['domain'] = {
'employee_id': [('user_id', '=', self.user_id.id)]
}
# equal
# self.env['hr.employee'].search([('user_id', '=', self.user_id.id)])
return result
......
```
當改變 `user_id` 時, 會增加對應的 domain, 需要回傳一個 dict,
這個 dict 包含 fields, 也就是 `employee_id`, 後面則是我們所需要的 domain.
## odoo 手把手教學 - form_view_ref 以及 tree_view_ref 說明 - part28
* [Youtube Tutorial - odoo 手把手教學 - form_view_ref 以及 tree_view_ref 說明 - part28](https://youtu.be/_YkrOp3ytlQ)
還記得 [odoo 手把手教學 - 同一個 model 使用不同的 view_ids - part10](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) 這篇教學嗎 :question:
那時候是使用 `ir.actions.act_window` 也就是 action 的方式定義不同的 view_ids,
今天如果想單獨針對 fields 定義 view 時, 就需要使用 `form_view_ref` `tree_view_ref` :exclamation:
使用方法也很簡單, 請參考 [views/view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/views/view.xml)
```xml
<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial Form</field>
<field name="model">demo.expense.tutorial</field>
<field name="arch" type="xml">
<form string="Demo Expense Tutorial">
......
<field name="sheet_id" context="{'form_view_ref':'demo_expense_tutorial_v1.view_form_demo_expense_sheet_tutorial'}"/>
<!-- <field name="sheet_id" context="{'form_view_ref':'demo_expense_tutorial_v1.custom_view_form_demo_sheet'}"/> -->
......
</group>
</sheet>
</form>
</field>
</record>
......
<record id="custom_view_form_demo_sheet" model="ir.ui.view">
<field name="name">Custim Demo Sheet Form</field>
<field name="model">demo.expense.sheet.tutorial</field>
<field name="arch" type="xml">
<form string="custom_view_form_demo_sheet">
<sheet>
......
</sheet>
</form>
</field>
</record>
```
在需要的 fields 上, 加上 `context="{'form_view_ref':......}"`, 然後再定義你的 view 即可,
`tree_view_ref` 也是一樣的概念 :smile:
注意 :exclamation:, 在這裡只要你有定義一個以上的 `demo.expense.sheet.tutorial` form view 時,
記得一定要使用 `form_view_ref` ( 否則它會自動選最後一個 ).
## odoo 手把手教學 - Message Post 教學 - part29
可參考 [models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py)
```python
......
@api.multi
def btn_message_post(self):
for rec in self:
if rec.user_id:
rec.user_id.partner_id.message_post(body="test body", subject="test subject")
else:
raise UserError('請選擇使用者(user_id)')
......
```
透過 `partner_id.message_post(....")`
可以完成 Message Post, 資訊要到 Contacts (`res.partner`) 底下看,

## odoo 手把手教學 - groups 搭配 fields 用法 - part30
* [Youtube Tutorial - odoo 手把手教學 - groups 搭配 fields 用法 - part30](https://youtu.be/JyNyg7iHar0)
這邊的用法和 [odoo 手把手教學 - domain 搭配 fields 的三種用法 - part27](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) 是類似的,
只不過對象換成了 groups,
寫法如下, 請參考 [models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py) 中,
```python
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
......
tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id',
string='Tges', copy=False,
groups='demo_expense_tutorial_v1.demo_expense_tutorial_group_manager'
)
......
```
`tag_ids` field 增加了 `groups='demo_expense_tutorial_v1.demo_expense_tutorial_group_manager'`,
代表的意思是只有 Manager 可以看到這個 field(擁有權限),
假如今天一個 User 權限的人, 不管在 tree 或是 form 都看不到 `tag_ids`.
但這不只是隱藏起來, 也就是說如果你強制去取值, 還是無法拿到資料的(因為權限不夠),
建議可以用 shell 模式下去嘗試取值.
然後另一種寫法如下,
請參考 [views/view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/views/view.xml),
```xml
<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
......
<field name="tag_ids" widget="many2many_tags" groups="demo_expense_tutorial_v1.demo_expense_tutorial_group_manager"/>
......
</record>
```
但這只是**隱藏**起來, 也就是說如果你強制去取值, 還是可以拿到資料的(沒有權限限制),
差別在於,
如果你是寫在 model, 會自動幫你產生到全部的 view 上(並且需要對應的權限).
如果你單獨寫在 view 上, 是針對個別的 view (像這邊就是只在 form 上) 生效,
(但不需要對應的權限).
## odoo 手把手教學 - ACID transactions 說明 - part31
* [Youtube Tutorial - odoo 手把手教學 - ACID transactions 說明 - part31](https://youtu.be/M36CNiK9xrM)
這邊先說結論, 下面會再說明, 如果你遵守 odoo 的 ORM,
你是不需要另外去處理 ACID transactions 的問題.
相關的 odoo source code 可參考 `odoo/odoo/sql_db.py`
```python
class Cursor(object):
"""Represents an open transaction to the PostgreSQL DB backend,
acting as a lightweight wrapper around psycopg2's
``cursor`` objects.
``Cursor`` is the object behind the ``cr`` variable used all
over the OpenERP code.
.. rubric:: Transaction Isolation
......
```
如果你不知道甚麼是 ACID, 可參考 [Transaction 概念簡介](https://github.com/twtrubiks/django-transactions-tutorial#transaction).
這邊就用一個例子來說明 Atomicity (原子性),
可參考 [models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py)
```python
......
@api.multi
def btn_test_acid_atomicity(self):
for index in range(3):
self.create({
'name': index,
'employee_id': 1
})
if index == 1:
raise UserError('error - auto rollback')
......
```
嘗試上面的 code.
假設今天有3筆資料,
只有兩種結果, 3筆全都寫入成功, 或是3筆全都失敗未寫入,
不會有1筆成功寫入, 2筆失敗的狀況.
## odoo 手把手教學 - 特殊 groups 應用說明 - part32
* [Youtube Tutorial - odoo 手把手教學 - 特殊 groups 應用說明 - part32](https://youtu.be/PSiDfM840NI)
這邊介紹幾個比較特殊的 groups 給大家,
首先是 `base.group_no_one`,
它的 groups 定義在原始碼中的 `/odoo/addons/base/security/base_groups.xml`
```xml
......
<record model="res.groups" id="group_no_one">
<field name="name">Technical Features</field>
</record>
......
```
這個 groups 只有在你打開 [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) 的時候才看的到.
接著是 `base.group_erp_manager` 和 `base.group_system`,
這些 groups 則是當你擁有 `Access Rights` 和 `Settings` 權限的時候你才看的到.
它的 groups 定義在原始碼中的 `/odoo/addons/base/security/base_groups.xml`
```xml
......
<record model="res.groups" id="group_erp_manager">
<field name="name">Access Rights</field>
</record>
<record model="res.groups" id="group_system">
<field name="name">Settings</field>
<field name="implied_ids" eval="[(4, ref('group_erp_manager'))]"/>
<field name="users" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
</record>
......
```
當然除了使用 groups 定義之外, 也可以直接指定 user,
像是 `base.user_admin` 就是只有 admin user 才看的到.
它的定義在原始碼中的 `/odoo/addons/base/data/res_users_data.xml`
```xml
......
<!-- user 2 is the human admin user -->
<record id="user_admin" model="res.users">
<field name="login">admin</field>
<field name="password">admin</field>
<field name="partner_id" ref="base.partner_admin"/>
<field name="company_id" ref="main_company"/>
<field name="company_ids" eval="[(4, ref('main_company'))]"/>
<field name="groups_id" eval="[(6,0,[])]"/>
<field name="signature"><![CDATA[<span>-- <br/>
Administrator</span>]]></field>
</record>
......
```
使用方法其實之前都說明過了, 可參考 [views/view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/views/view.xml)
```xml
<record id="view_form_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial Form</field>
<field name="model">demo.expense.tutorial</field>
......
<field name="debug_field" groups="base.group_no_one"/>
<field name="admin_field" groups="base.user_admin"/>
......
```
基本上原生的都是定義在原始碼中的 `odoo/addons/base/security/base_groups.xml`
像是基本的 user groups 種類,
```xml
......
<record model="res.groups" id="group_user">
<field name="name">Internal User</field>
</record>
......
<record id="group_portal" model="res.groups">
<field name="name">Portal</field>
......
</record>
......
<record id="group_public" model="res.groups">
<field name="name">Public</field>
......
</record>
......
```
透過定義 user 以及 groups, 可以組合出更靈活的架構.
================================================
FILE: demo_expense_tutorial_v1/__init__.py
================================================
# from . import controllers
from . import models
================================================
FILE: demo_expense_tutorial_v1/__manifest__.py
================================================
{
'name': "demo expense tutorial v1",
'summary': """
tutorial - Many2one, Many2many, One2many
demo expense tutorial v1
""",
'description': """
tutorial - Many2one, Many2many, One2many
demo expense tutorial v1
""",
'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_contract'],
# always loaded
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'security/ir_rule.xml',
'data/demo_expense_tutorial_data.xml',
'views/view.xml',
'views/menu.xml',
],
# only loaded in demonstration mode
# 'demo': [
# 'demo/demo.xml',
# ],
'application': True,
}
================================================
FILE: demo_expense_tutorial_v1/controllers/__init__.py
================================================
from . import controllers
================================================
FILE: demo_expense_tutorial_v1/controllers/controllers.py
================================================
# from odoo import http
================================================
FILE: demo_expense_tutorial_v1/data/demo_expense_tutorial_data.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<!-- demo.expense.tutorial -->
<record id="demo_expense_tutorial_data_1" model="demo.expense.tutorial">
<field name="name">demo_expense_tutorial_data_1</field>
<field name="employee_id" ref="hr.employee_admin"></field>
</record>
<record id="demo_expense_tutorial_data_2" model="demo.expense.tutorial">
<field name="name">demo_expense_tutorial_data_2</field>
<field name="employee_id" ref="hr.employee_admin"></field>
</record>
<!-- demo.tag -->
<record id="demo_tag_data_1" model="demo.tag">
<field name="name">demo_tag_data_1</field>
</record>
<record id="demo_tag_data_2" model="demo.tag">
<field name="name">demo_tag_data_2</field>
</record>
</data>
</odoo>
================================================
FILE: demo_expense_tutorial_v1/demo/demo.xml
================================================
<odoo>
<data>
</data>
</odoo>
================================================
FILE: demo_expense_tutorial_v1/models/__init__.py
================================================
from . import models
================================================
FILE: demo_expense_tutorial_v1/models/models.py
================================================
from odoo import models, fields, api
from odoo.exceptions import UserError
class DemoTag(models.Model):
_name = 'demo.tag'
_description = 'Demo Tags'
_rec_name = 'complete_name'
name = fields.Char(string='Tag Name', index=True, required=True)
complete_name = fields.Char('Complete Name', compute='_compute_complete_name')
active = fields.Boolean(default=True, help="Set active.")
@api.depends('name')
def _compute_complete_name(self):
for record in self:
record.complete_name = 'hello world - {}'.format(record.name)
class DemoExpenseTutorial(models.Model):
_name = 'demo.expense.tutorial'
_description = 'Demo Expense Tutorial'
_order = "sequence, id desc"
name = fields.Char('Description', required=True)
# employee_id = fields.Many2one('hr.employee', string="Employee", required=True,
# domain=[('active', '=', True)] )
employee_id = fields.Many2one('hr.employee', string="Employee", required=True)
user_id = fields.Many2one('res.users', default=lambda self: self.env.user)
# https://www.odoo.com/documentation/12.0/reference/orm.html#odoo.fields.Many2many
# Many2many(comodel_name=<object object>, relation=<object object>, column1=<object object>, column2=<object object>, string=<object object>, **kwargs)
#
# relation: database table name
#
# By default, the relationship table name is the two table names
# joined with an underscore and _rel appended at the end.
# In the case of our books or authors relationship, it should be named demo_expense_tutorial_demo_tag_rel.
tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id',
string='Tges', copy=False,
groups='demo_expense_tutorial_v1.demo_expense_tutorial_group_manager'
)
sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string="Expense Report", ondelete='restrict')
# Related (Reference) fields (不會存在 db)
# readonly default 為 True
# store default 為 False
gender = fields.Selection('Gender', related='employee_id.gender')
sequence = fields.Integer(index=True, help="Gives the sequence order", default=1)
active = fields.Boolean(default=True, help="Set active.")
debug_field = fields.Char('debug_field')
admin_field = fields.Char('admin_field')
@api.multi
def copy(self, default=None):
default = dict(default or {})
if not default.get('name'):
default['name'] = '{} copy'.format(self.name)
return super(DemoExpenseTutorial, self).copy(default)
@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'
}
@api.multi
def button_rainbow_man(self):
return {
'effect': {
'fadeout': 'slow',
'message': 'hello',
'type': 'rainbow_man',
}
}
@api.multi
def btn_test_acid_atomicity(self):
for index in range(3):
self.create({
'name': index,
'employee_id': 1
})
if index == 1:
raise UserError('error - auto rollback')
@api.multi
def button_act_url(self):
self.ensure_one()
return {
'type': 'ir.actions.act_url',
'target': 'new',
# 'target': 'self',
'url': 'https://github.com/twtrubiks/odoo-demo-addons-tutorial',
}
@api.multi
def btn_message_post(self):
for rec in self:
if rec.user_id:
rec.user_id.partner_id.message_post(body="test body", subject="test subject")
else:
raise UserError('請選擇使用者(user_id)')
@api.onchange('user_id')
def onchange_user_id(self):
# domain
result = dict()
result['domain'] = {
'employee_id': [('user_id', '=', self.user_id.id)]
}
# equal
# self.env['hr.employee'].search([('user_id', '=', self.user_id.id)])
return result
class DemoExpenseSheetTutorial(models.Model):
_name = 'demo.expense.sheet.tutorial'
_description = 'Demo Expense Sheet Tutorial'
name = fields.Char('Expense Demo Report Summary', required=True)
# One2many is a virtual relationship, there must be a Many2one field in the other_model,
# and its name must be related_field
expense_line_ids = fields.One2many(
'demo.expense.tutorial', # related model
'sheet_id', # field for "this" on related model
string='Expense Lines')
demo_expenses_count = fields.Integer(
compute='_compute_demo_expenses_count',
string='Demo Expenses Count')
@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])]
@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)],
}
def _compute_demo_expenses_count(self):
# usually used read_group
for record in self:
record.demo_expenses_count = len(self.expense_line_ids)
@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()
================================================
FILE: demo_expense_tutorial_v1/security/ir.model.access.csv
================================================
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_demo_expense_user,Demo Expense Tutorial User Access,model_demo_expense_tutorial,demo_expense_tutorial_group_user,1,1,1,0
access_demo_expense_manager,Demo Expense Tutorial Manager Access,model_demo_expense_tutorial,demo_expense_tutorial_group_manager,1,1,1,1
access_demo_expense_user_tag,Demo Expense Tutorial Tag User Access,model_demo_tag,demo_expense_tutorial_group_user,1,0,0,0
access_demo_expense_manager_tag,Demo Expense Tutorial Tag Manager Access,model_demo_tag,demo_expense_tutorial_group_manager,1,1,1,1
access_demo_expense_sheet_user,Demo Expense Sheet User Tutorial Access,model_demo_expense_sheet_tutorial,demo_expense_tutorial_group_user,1,1,1,0
access_demo_expense_sheet_manager,Demo Expense Sheet Manager Tutorial Access,model_demo_expense_sheet_tutorial,demo_expense_tutorial_group_manager,1,1,1,1
================================================
FILE: demo_expense_tutorial_v1/security/ir_rule.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="ir_rule_demo_expense_user" model="ir.rule">
<field name="name">Demo Expense User</field>
<field name="model_id" ref="model_demo_expense_tutorial"/>
<field name="domain_force">[('employee_id.user_id.id', '=', user.id)]</field>
<field name="groups" eval="[(4, ref('demo_expense_tutorial_group_user'))]"/>
<!-- Groups (no group = global) -->
<!-- <field name="global" eval="True"/> -->
<field eval="0" name="perm_unlink"/>
<field eval="1" name="perm_write"/>
<field eval="1" name="perm_read"/>
<field eval="1" name="perm_create"/>
</record>
<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>
</odoo>
================================================
FILE: demo_expense_tutorial_v1/security/security.xml
================================================
<?xml version="1.0" ?>
<odoo>
<record id="module_demo_expense_tutorial" model="ir.module.category">
<field name="name">Demo expense tutorial category</field>
</record>
<record id="demo_expense_tutorial_group_user" model="res.groups">
<field name="name">User</field>
<field name="category_id"
ref="module_demo_expense_tutorial"/>
<field name="implied_ids"
eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="demo_expense_tutorial_group_manager" model="res.groups">
<field name="name">Manager</field>
<field name="category_id"
ref="module_demo_expense_tutorial"/>
<field name="implied_ids"
eval="[(4, ref('demo_expense_tutorial_group_user'))]"/>
<field name="users"
eval="[(4, ref('base.user_root')),
(4, ref('base.user_admin'))]"/>
</record>
</odoo>
================================================
FILE: demo_expense_tutorial_v1/views/menu.xml
================================================
<?xml version="1.0"?>
<odoo>
<!-- demo_expense_tutorial App Menu -->
<menuitem id="demo_expense_tutorial_menu"
name="Demo Expense Tutorial" />
<!-- Action to open the demo_expense_tutorial -->
<act_window id="action_expense_tutorial"
name="Demo Expense Tutorial Action"
res_model="demo.expense.tutorial"
view_mode="tree,form"/>
<!-- Menu item to open the demo_expense_tutorial -->
<menuitem id="menu_expense_tutorial"
name="Demo Expense Tutorial"
action="action_expense_tutorial"
parent="demo_expense_tutorial_menu" />
<!-- Action to open the demo_expense_sheet_tutorial -->
<!-- <act_window id="action_expense_sheet_tutorial"
name="Demo Expense Sheet Tutorial Action"
res_model="demo.expense.sheet.tutorial"
view_mode="tree,form"/> -->
<record id="action_expense_sheet_tutorial" model="ir.actions.act_window">
<field name="name">Demo Expense Sheet Tutorial Action</field>
<field name="res_model">demo.expense.sheet.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.view_tree_demo_expense_sheet_tutorial')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.view_form_demo_expense_sheet_tutorial')})]"/>
</record>
<!-- Menu item to open the demo_expense_sheet_tutorial -->
<menuitem id="menu_expense_sheet_tutorial"
name="Demo Expense Sheet Tutorial"
action="action_expense_sheet_tutorial"
parent="demo_expense_tutorial_menu" />
<!-- 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>
<!-- Menu item to open the demo_expense_tutorial_no_craete -->
<menuitem id="menu_expense_tutorial_no_craete"
name="Demo Expense Tutorial No Create"
action="action_expense_tutorial_no_craete"
parent="demo_expense_tutorial_menu" />
<!-- Menu item to open the demo_expense_view_ids -->
<menuitem id="menu_expense_tutorial_view_ids"
name="Demo Expense Tutorial View ids"
parent="demo_expense_tutorial_menu" />
<!-- Action to open the menu_expense_tutorial_view_id_1 -->
<record id="action_expense_tutorial_view_id_1" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action View id 1</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.tree_expense_view_id_1')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.form_expense_view_id_1')})]"/>
</record>
<!-- Menu item to open the demo_expense_view_id_1 -->
<menuitem id="menu_expense_tutorial_view_id_1"
name="Demo Expense Tutorial View id 1"
sequence="1"
action="action_expense_tutorial_view_id_1"
parent="menu_expense_tutorial_view_ids" />
<!-- Action to open the menu_expense_tutorial_view_id_2 -->
<record id="action_expense_tutorial_view_id_2" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action View id 2</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.tree_expense_view_id_2')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.form_expense_view_id_2')})]"/>
</record>
<!-- Menu item to open the demo_expense_view_id_2 -->
<menuitem id="menu_expense_tutorial_view_id_2"
name="Demo Expense Tutorial View id 2"
sequence="2"
action="action_expense_tutorial_view_id_2"
parent="menu_expense_tutorial_view_ids" />
<!-- Menu item to open the menu_expense_tutorial_context-->
<menuitem id="menu_expense_tutorial_root_context"
name="Demo Expense Tutorial View context"
parent="demo_expense_tutorial_menu" />
<!-- Action to open the demo_expense_tutorial_context -->
<record id="action_expense_tutorial_context" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action Context</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<!-- init search default -->
<field name="context">{'search_default_name': 'test123'}</field>
<!-- init create default name 'test123'-->
<!-- <field name="context">{'default_name': 'test123'}</field> -->
</record>
<!-- Action to open the demo_expense_tutorial_domain -->
<record id="action_expense_tutorial_domain" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action Domain</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('name', 'like', 'test')]</field>
<field name="context">{}</field>
</record>
<!-- Action to open the demo_expense_tutorial_test_active -->
<record id="action_expense_tutorial_test_active" model="ir.actions.act_window">
<field name="name">Demo Expense Tutorial Action Test Active</field>
<field name="res_model">demo.expense.tutorial</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<!-- init show all (active True False) record -->
<field name="context">{'active_test':False}</field>
<!-- init show only (active True) record -->
<!-- <field name="context">{}</field> -->
</record>
<!-- Menu item to open the demo_expense_tutorial_context -->
<menuitem id="menu_expense_tutorial_context"
name="Demo Expense Tutorial context"
action="action_expense_tutorial_context"
parent="menu_expense_tutorial_root_context" />
<!-- Menu item to open the demo_expense_tutorial_domain -->
<menuitem id="menu_expense_tutorial_domain"
name="Demo Expense Tutorial domain"
action="action_expense_tutorial_domain"
parent="menu_expense_tutorial_root_context" />
<!-- Menu item to open the demo_expense_tutorial_domain -->
<menuitem id="menu_expense_tutorial_active_test"
name="Demo Expense Tutorial active_test"
action="action_expense_tutorial_test_active"
parent="menu_expense_tutorial_root_context" />
</odoo>
================================================
FILE: demo_expense_tutorial_v1/views/view.xml
================================================
<?xml version="1.0"?>
<odoo>
<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">
<header>
<button name="button_rainbow_man" string="Test Rainbow" type="object" class="oe_highlight o_expense_sheet_submit"/>
<button name="button_act_url" string="Test Act Url" type="object" class="oe_highlight o_expense_sheet_submit"/>
<button name="btn_message_post" string="Message Post" type="object" class="oe_highlight o_expense_sheet_submit"/>
<button name="btn_test_acid_atomicity" string="Test acid atomicity" type="object" class="oe_highlight o_expense_sheet_submit"/>
</header>
<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>
<group>
<field name="name"/>
<!-- <field name="employee_id" domain="[('user_id', '=', user_id)]"/> -->
<field name="employee_id"/>
<!-- <field name="employee_id" options="{'no_quick_create': True}"/> -->
<!-- <field name="employee_id" options="{'no_create_edit': True}"/> -->
<!-- <field name="employee_id" options="{'no_create': True}"/> -->
<!-- <field name="employee_id" options="{'no_open': True}"/> -->
<field name="user_id"/>
<!-- <field name="tag_ids"/> -->
<field name="tag_ids" widget="many2many_tags"/> <!-- widget -->
<!-- <field name="tag_ids" widget="many2many_tags" groups="demo_expense_tutorial_v1.demo_expense_tutorial_group_manager"/> -->
<field name="sheet_id" context="{'form_view_ref':'demo_expense_tutorial_v1.view_form_demo_expense_sheet_tutorial'}"/>
<!-- <field name="sheet_id" context="{'form_view_ref':'demo_expense_tutorial_v1.custom_view_form_demo_sheet'}"/> -->
<field name="gender"/>
<field name="debug_field" groups="base.group_no_one"/>
<field name="admin_field" groups="base.user_admin"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_tree_demo_expense_tutorial" model="ir.ui.view">
<field name="name">Demo Expense Tutorial List</field>
<field name="model">demo.expense.tutorial</field>
<field name="priority" eval="1"/>
<field name="arch" type="xml">
<tree decoration-info="'info' in name" decoration-muted="'muted' in name" decoration-danger="'danger' in name" decoration-bf="'bf' in name" decoration-warning="'warning' in name" decoration-success="'success' in name">
<!-- <tree default_order="sequence, id desc"> -->
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="employee_id"/>
<field name="user_id"/>
<field name="tag_ids"/>
<field name="sheet_id" widget="many2onebutton"/>
</tree>
</field>
</record>
<!-- change name, employee_id fields-->
<record id="view_tree_demo_expense_tutorial_move" model="ir.ui.view">
<field name="name">view_tree_demo_expense_tutorial_move</field>
<field name="model">demo.expense.tutorial</field>
<field name="inherit_id" ref="demo_expense_tutorial_v1.view_tree_demo_expense_tutorial"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="before">
<field name="employee_id" position="move"/>
</xpath>
</field>
</record>
<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">
<header>
<button name="add_demo_expense_record" string="add demo expense record" type="object"/>
<button name="link_demo_expense_record" string="link demo expense record" type="object"/>
<button name="replace_demo_expense_record" string="replace expense record" type="object"/>
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button class="oe_stat_button"
name="button_line_ids"
type="object"
attrs="{'invisible':[('expense_line_ids','=', False)]}"
icon="fa-bars">
<field name="demo_expenses_count" widget="statinfo" string="Counts"/>
</button>
</div>
<group>
<field name="name"/>
</group>
<notebook>
<page string="Expense">
<field name="expense_line_ids" >
<!-- <tree> -->
<tree editable="top"> <!-- <<<<<<<<<<<< -->
<!-- <tree editable="bottom"> --> <!-- <<<<<<<<<<<< -->
<field name="name"/>
<field name="employee_id"/>
<field name="tag_ids" widget="many2many_tags" attrs="{'readonly': [('parent.name', '=', 'test-readonly')]}"/>
</tree>
</field>
</page>
</notebook>
</sheet>
</form>
</field>
</rec
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
SYMBOL INDEX (113 symbols across 27 files)
FILE: demo_abstractmodel_tutorial/wizard/model_wizard.py
class ReportWizard (line 6) | class ReportWizard(models.TransientModel):
method download_report (line 14) | def download_report(self):
class ReportExpenseAbstractModel (line 28) | class ReportExpenseAbstractModel(models.AbstractModel):
method _get_report_values (line 33) | def _get_report_values(self, docids, data=None):
FILE: demo_abstractmodel_v2_tutorial/models/models.py
class DemoMixin (line 3) | class DemoMixin(models.AbstractModel):
method _compute_field (line 17) | def _compute_field(self):
class DemoModelTutorial (line 21) | class DemoModelTutorial(models.Model):
FILE: demo_abstractmodel_v2_tutorial/models/models_v2.py
class DemoMixin2 (line 3) | class DemoMixin2(models.AbstractModel):
class DemoModelTutorial (line 12) | class DemoModelTutorial(models.Model):
FILE: demo_actions_singleton/models/models.py
class DemoActionsSingleton (line 6) | class DemoActionsSingleton(models.Model):
method action_demo (line 13) | def action_demo(self):
FILE: demo_activity/models/models.py
class DemoActivity (line 3) | class DemoActivity(models.Model):
method button_activity_schedule (line 12) | def button_activity_schedule(self):
method button_activity_feedback (line 19) | def button_activity_feedback(self):
method button_activity_unlink (line 23) | def button_activity_unlink(self):
FILE: demo_class_inheritance/models/model.py
class ClassInheritance (line 7) | class ClassInheritance(models.Model):
FILE: demo_config_settings/models/models.py
class ResConfigSettings (line 3) | class ResConfigSettings(models.TransientModel):
method get_values (line 17) | def get_values(self):
method set_values (line 25) | def set_values(self):
FILE: demo_datetime_tutorial/models/models.py
class DemoDatetime (line 7) | class DemoDatetime(models.Model):
method demo1 (line 16) | def demo1(self):
FILE: demo_delegation_inheritance/models/model.py
class DelegationInheritance (line 14) | class DelegationInheritance(models.Model):
FILE: demo_expense_tutorial_v1/models/models.py
class DemoTag (line 4) | class DemoTag(models.Model):
method _compute_complete_name (line 14) | def _compute_complete_name(self):
class DemoExpenseTutorial (line 18) | class DemoExpenseTutorial(models.Model):
method copy (line 59) | def copy(self, default=None):
method button_sheet_id (line 66) | def button_sheet_id(self):
method button_rainbow_man (line 75) | def button_rainbow_man(self):
method btn_test_acid_atomicity (line 85) | def btn_test_acid_atomicity(self):
method button_act_url (line 95) | def button_act_url(self):
method btn_message_post (line 105) | def btn_message_post(self):
method onchange_user_id (line 113) | def onchange_user_id(self):
class DemoExpenseSheetTutorial (line 123) | class DemoExpenseSheetTutorial(models.Model):
method add_demo_expense_record (line 141) | def add_demo_expense_record(self):
method link_demo_expense_record (line 160) | def link_demo_expense_record(self):
method replace_demo_expense_record (line 170) | def replace_demo_expense_record(self):
method button_line_ids (line 181) | def button_line_ids(self):
method _compute_demo_expenses_count (line 192) | def _compute_demo_expenses_count(self):
method name_get (line 198) | def name_get(self):
method _name_search (line 207) | def _name_search(self, name='', args=None, operator='ilike', limit=100):
FILE: demo_fields_view_get_tutorial/models/account_invoice.py
class AccountInvoice (line 7) | class AccountInvoice(models.Model):
method fields_view_get (line 11) | def fields_view_get(self, view_id=None, view_type='form', toolbar=Fals...
FILE: demo_hierarchy_tutorial/models/models.py
class DemoHierarchyTutorial (line 3) | class DemoHierarchyTutorial(models.Model):
FILE: demo_hook_tutorial/__init__.py
function pre_init_hook (line 7) | def pre_init_hook(cr):
function post_init_hook (line 13) | def post_init_hook(cr, registry):
function uninstall_hook (line 19) | def uninstall_hook(cr, registry):
function post_load_hook (line 25) | def post_load_hook():
FILE: demo_inherit_controller/controllers/portal.py
class TutorialPortal (line 6) | class TutorialPortal(CustomerPortal):
method portal_my_quotes (line 9) | def portal_my_quotes(self, page=1, date_begin=None, date_end=None, sor...
FILE: demo_multi_company/models/model.py
class DemoCompany (line 6) | class DemoCompany(models.Model):
method action_get_default_account (line 20) | def action_get_default_account(self):
FILE: demo_odoo_tutorial/controllers/controllers.py
class DemoOdoo (line 3) | class DemoOdoo(http.Controller):
method list (line 6) | def list(self, **kwargs):
FILE: demo_odoo_tutorial/models/models.py
class DemoOdooTutorial (line 5) | class DemoOdooTutorial(models.Model):
method _check_date (line 37) | def _check_date(self):
method _get_field_compute (line 45) | def _get_field_compute(self):
method _set_input_number (line 49) | def _set_input_number(self):
method _search_upper (line 53) | def _search_upper(self, operator, value):
method onchange_demo (line 57) | def onchange_demo(self):
method demo_raw_sql (line 69) | def demo_raw_sql(self):
method get_custom_portal_date (line 110) | def get_custom_portal_date(self):
class DemoOdooTutorialStatistics (line 114) | class DemoOdooTutorialStatistics(models.Model):
method init (line 123) | def init(self):
FILE: demo_odoo_tutorial/tests/test_demo_odoo_singletransactioncase.py
class TestDemoOdooSingleTransactionCase (line 4) | class TestDemoOdooSingleTransactionCase(SingleTransactionCase):
method setUp (line 6) | def setUp(self, *args, **kwargs):
method test_hello_world (line 11) | def test_hello_world(self):
method test_datetime_validation (line 15) | def test_datetime_validation(self):
method test_field_compute_demo (line 25) | def test_field_compute_demo(self):
FILE: demo_odoo_tutorial/tests/test_demo_odoo_transactioncase.py
class TestDemoOdooTransactionCase (line 5) | class TestDemoOdooTransactionCase(TransactionCase):
method setUp (line 7) | def setUp(self, *args, **kwargs):
method test_hello_world (line 12) | def test_hello_world(self):
method test_datetime_validation (line 16) | def test_datetime_validation(self):
method test_field_compute_demo (line 26) | def test_field_compute_demo(self):
FILE: demo_odoo_tutorial_wizard/models/models.py
class DemoOdooWizardTutorial (line 4) | class DemoOdooWizardTutorial(models.Model):
method action_context_demo (line 12) | def action_context_demo(self):
method action_button (line 19) | def action_button(self):
FILE: demo_odoo_tutorial_wizard/wizard/model_wizard.py
class DemoWizard (line 6) | class DemoWizard(models.TransientModel):
method default_get (line 14) | def default_get(self, fields):
method btn_validate (line 26) | def btn_validate(self):
FILE: demo_orm_cache/models/model.py
class DemoCache (line 7) | class DemoCache(models.Model):
method demo_ormcache (line 14) | def demo_ormcache(self):
method demo_ormcache_by_env (line 20) | def demo_ormcache_by_env(self):
method demo_ormcache_context (line 26) | def demo_ormcache_context(self):
method demo_clear_cache (line 31) | def demo_clear_cache(self):
FILE: demo_prototype_inheritance/models/model.py
class PrototypeInheritance (line 4) | class PrototypeInheritance(models.Model):
FILE: demo_sale_scan_barcode/models/models.py
class SaleOrderBarcodes (line 3) | class SaleOrderBarcodes(models.Model):
method on_barcode_scanned (line 10) | def on_barcode_scanned(self, barcode):
FILE: demo_scheduler/models/scheduler.py
class DemoScheduler (line 6) | class DemoScheduler(models.Model):
method action_schedule (line 10) | def action_schedule(self):
FILE: demo_sequence/models/model.py
class DemoSequence (line 3) | class DemoSequence(models.Model):
method create (line 10) | def create(self, vals):
FILE: xml-rpc-odoo/demo.py
function common_version (line 14) | def common_version():
function get_uid (line 21) | def get_uid():
function endpoint_object (line 28) | def endpoint_object():
function call_check_access_rights (line 32) | def call_check_access_rights():
function list_all_records (line 41) | def list_all_records():
function list_records (line 49) | def list_records():
function count_records (line 58) | def count_records():
function read_records (line 67) | def read_records():
function read_all_field (line 78) | def read_all_field():
function read_need_field (line 88) | def read_need_field():
function listing_record_fields_attributes (line 99) | def listing_record_fields_attributes():
function search_and_read (line 109) | def search_and_read():
function create_reads (line 120) | def create_reads():
function update_records (line 135) | def update_records():
function delete_record (line 155) | def delete_record():
function many2one_create (line 170) | def many2one_create():
function many2many_add_record (line 193) | def many2many_add_record():
function many2many_add_mutil_record (line 216) | def many2many_add_mutil_record():
function many2many_update_record (line 239) | def many2many_update_record():
function many2many_delete_record_2 (line 267) | def many2many_delete_record_2():
function many2many_delete_record_3 (line 306) | def many2many_delete_record_3():
function many2many_delete_record_5 (line 346) | def many2many_delete_record_5():
Condensed preview — 197 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (438K chars).
[
{
"path": ".gitignore",
"chars": 1723,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": "README.md",
"chars": 18999,
"preview": "# odoo-demo-addons-tutorial-odoo-12\n\n此版本為 odoo12,\n\nodoo14 版本請參考 [odoo14](https://github.com/twtrubiks/odoo-demo-addons-t"
},
{
"path": "demo_abstractmodel_tutorial/README.md",
"chars": 7027,
"preview": "# 介紹 AbstractModel\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - AbstractModel](https://youtu.be/jsMTVe12vRY)\n"
},
{
"path": "demo_abstractmodel_tutorial/__init__.py",
"chars": 20,
"preview": "from . import wizard"
},
{
"path": "demo_abstractmodel_tutorial/__manifest__.py",
"chars": 706,
"preview": "{\n 'name': \"demo_abstractmodel_tutorial\",\n 'summary': \"\"\"\n AbstractModel report\n \"\"\",\n 'description':"
},
{
"path": "demo_abstractmodel_tutorial/reports/report.xml",
"chars": 1774,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n\n <template id=\"report_wizard_template\">\n <t t-call=\"web.html_co"
},
{
"path": "demo_abstractmodel_tutorial/wizard/__init__.py",
"chars": 27,
"preview": "from . import model_wizard\n"
},
{
"path": "demo_abstractmodel_tutorial/wizard/model_wizard.py",
"chars": 1550,
"preview": "from odoo import api, fields, models\nimport logging\n\n_logger = logging.getLogger(__name__)\n\nclass ReportWizard(models.Tr"
},
{
"path": "demo_abstractmodel_tutorial/wizard/model_wizard.xml",
"chars": 1155,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n <record model=\"ir.ui.view\" id=\"custom_report_wizard\">\n <field name="
},
{
"path": "demo_abstractmodel_v2_tutorial/README.md",
"chars": 3294,
"preview": "# 透過 AbstractModel 擴充 Model\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - 透過 AbstractModel 擴充 Model](https://y"
},
{
"path": "demo_abstractmodel_v2_tutorial/__init__.py",
"chars": 20,
"preview": "from . import models"
},
{
"path": "demo_abstractmodel_v2_tutorial/__manifest__.py",
"chars": 742,
"preview": "{\n 'name': \"demo_abstractmodel_v2_tutorial\",\n 'summary': \"\"\"\n AbstractModel Extend Model\n \"\"\",\n 'desc"
},
{
"path": "demo_abstractmodel_v2_tutorial/models/__init__.py",
"chars": 46,
"preview": "from . import models\n# from . import models_v2"
},
{
"path": "demo_abstractmodel_v2_tutorial/models/models.py",
"chars": 594,
"preview": "from odoo import models, fields, api\n\nclass DemoMixin(models.AbstractModel):\n _name = 'demo.mixin'\n _description ="
},
{
"path": "demo_abstractmodel_v2_tutorial/models/models_v2.py",
"chars": 358,
"preview": "from odoo import models, fields, api\n\nclass DemoMixin2(models.AbstractModel):\n _name = 'demo.mixin2'\n _description"
},
{
"path": "demo_abstractmodel_v2_tutorial/security/ir.model.access.csv",
"chars": 171,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_model_tutorial_user,Demo Model "
},
{
"path": "demo_abstractmodel_v2_tutorial/views/menu.xml",
"chars": 648,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <!-- demo_model_tutorial App Menu -->\n <menuitem id=\"demo_model_tutorial_menu\"\n "
},
{
"path": "demo_abstractmodel_v2_tutorial/views/view.xml",
"chars": 896,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <record id=\"view_form_demo_model_tutorial\" model=\"ir.ui.view\">\n <field name=\"name\">Dem"
},
{
"path": "demo_actions_singleton/README.md",
"chars": 3319,
"preview": "# odoo 觀念 - actions 和 singleton\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - actions and singleton](https://y"
},
{
"path": "demo_actions_singleton/__init__.py",
"chars": 20,
"preview": "from . import models"
},
{
"path": "demo_actions_singleton/__manifest__.py",
"chars": 826,
"preview": "{\n 'name': \"demo actions singleton\",\n 'summary': \"\"\"\n tutorial - actions and singleton\n \"\"\",\n 'descri"
},
{
"path": "demo_actions_singleton/data/action_data.xml",
"chars": 959,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n <data>\n <record id=\"action_server_demo\" model=\"ir.actions.serve"
},
{
"path": "demo_actions_singleton/models/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_actions_singleton/models/models.py",
"chars": 390,
"preview": "from odoo import models, fields, api\nimport logging\n\n_logger = logging.getLogger(__name__)\n\nclass DemoActionsSingleton(m"
},
{
"path": "demo_actions_singleton/security/ir.model.access.csv",
"chars": 353,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_singleton_user,Demo Actions Sin"
},
{
"path": "demo_actions_singleton/security/security.xml",
"chars": 885,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n\n <record id=\"module_demo_actions_singleton\" model=\"ir.module.category\">\n <field name="
},
{
"path": "demo_actions_singleton/views/menu.xml",
"chars": 652,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <!-- demo_actions_singleton App Menu -->\n <menuitem id=\"demo_actions_singleton_menu\""
},
{
"path": "demo_actions_singleton/views/view.xml",
"chars": 739,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <record id=\"view_form_demo_actions_singleton\" model=\"ir.ui.view\">\n <field name=\"name\">"
},
{
"path": "demo_activity/README.md",
"chars": 5548,
"preview": "# odoo 觀念 - activity\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - activity](https://youtu.be/_i4yLHrXRdg)\n\n建議"
},
{
"path": "demo_activity/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_activity/__manifest__.py",
"chars": 799,
"preview": "{\n 'name': \"demo activity\",\n 'summary': \"\"\"\n tutorial - demo activity\n \"\"\",\n 'description': \"\"\"\n "
},
{
"path": "demo_activity/data/mail_data.xml",
"chars": 362,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n <data noupdate=\"0\">\n <record id=\"mail_act_approval\" model=\"mail"
},
{
"path": "demo_activity/models/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_activity/models/models.py",
"chars": 820,
"preview": "from odoo import models, fields, api\n\nclass DemoActivity(models.Model):\n _name = \"demo.activity\"\n _description = \""
},
{
"path": "demo_activity/security/ir.model.access.csv",
"chars": 295,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_activity_user,Demo Activity Use"
},
{
"path": "demo_activity/security/security.xml",
"chars": 849,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n\n <record id=\"module_demo_activity_category\" model=\"ir.module.category\">\n <field name="
},
{
"path": "demo_activity/views/menu.xml",
"chars": 377,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <menuitem id=\"demo_activity_menu\"\n name=\"Demo Activity\"/>\n\n <act_window id=\"act"
},
{
"path": "demo_activity/views/view.xml",
"chars": 1296,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <record id=\"view_activity_form\" model=\"ir.ui.view\">\n <field name=\"name\">demo.a"
},
{
"path": "demo_class_inheritance/README.md",
"chars": 3699,
"preview": "# odoo 繼承 - class inheritance\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 繼承 - class inheritance](https://youtu.be/z"
},
{
"path": "demo_class_inheritance/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_class_inheritance/__manifest__.py",
"chars": 867,
"preview": "{\n 'name': \"demo_class_inheritance\",\n 'summary': \"\"\"\n demo_class_inheritance,\n model, view, form\n "
},
{
"path": "demo_class_inheritance/models/__init__.py",
"chars": 20,
"preview": "from . import model\n"
},
{
"path": "demo_class_inheritance/models/model.py",
"chars": 302,
"preview": "from odoo import models, fields\n\n# https://www.odoo.com/forum/help-1/question/whats-the-difference-between-inherit-and-i"
},
{
"path": "demo_class_inheritance/views/views.xml",
"chars": 1333,
"preview": "<odoo>\n <data>\n <record id=\"view_expenses_tree_custom\" model=\"ir.ui.view\">\n <field name=\"name\">hr.e"
},
{
"path": "demo_config_settings/README.md",
"chars": 6116,
"preview": "# 實作 config settings\n\n建議觀看影片, 會更清楚 :smile:\n\n* [實作 config settings](https://github.com/twtrubiks/odoo-demo-addons-tutoria"
},
{
"path": "demo_config_settings/__init__.py",
"chars": 20,
"preview": "from . import models"
},
{
"path": "demo_config_settings/__manifest__.py",
"chars": 819,
"preview": "{\n 'name': \"demo odoo config settings\",\n 'summary': \"\"\"\n basic tutorial -\n demo odoo config settings"
},
{
"path": "demo_config_settings/models/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_config_settings/models/models.py",
"chars": 1029,
"preview": "from odoo import models, fields, api\n\nclass ResConfigSettings(models.TransientModel):\n _inherit = 'res.config.setting"
},
{
"path": "demo_config_settings/security/security.xml",
"chars": 360,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n\n <record id=\"demo_config_settings_tutorial_group\" model=\"res.groups\">\n <field name=\"n"
},
{
"path": "demo_config_settings/views/view.xml",
"chars": 2114,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <record id=\"settings_form_inherit_custom\" model=\"ir.ui.view\">\n <field name=\"name\">re"
},
{
"path": "demo_datetime_tutorial/README.md",
"chars": 4726,
"preview": "# odoo datetime 教學\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo datetime 教學](https://youtu.be/Ha0YNFm6KzI)\n\n建議在閱讀這篇文章"
},
{
"path": "demo_datetime_tutorial/__init__.py",
"chars": 20,
"preview": "from . import models"
},
{
"path": "demo_datetime_tutorial/__manifest__.py",
"chars": 779,
"preview": "{\n 'name': \"demo_datetime_tutorial\",\n 'summary': \"\"\"\n demo_datetime_tutorial\n \"\"\",\n 'description': \"\""
},
{
"path": "demo_datetime_tutorial/models/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_datetime_tutorial/models/models.py",
"chars": 607,
"preview": "from odoo import models, fields, api\nfrom pytz import timezone\nimport logging\n\n_logger = logging.getLogger(__name__)\n\ncl"
},
{
"path": "demo_datetime_tutorial/security/ir.model.access.csv",
"chars": 152,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_datetime,Demo Datetime User Acc"
},
{
"path": "demo_datetime_tutorial/views/menu.xml",
"chars": 537,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <!-- Demo Datetime Menu -->\n <menuitem id=\"demo_datetime\"\n name=\"Demo D"
},
{
"path": "demo_datetime_tutorial/views/view.xml",
"chars": 996,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <record id=\"view_demo_datetime_tutorial_form\" model=\"ir.ui.view\">\n <field name=\""
},
{
"path": "demo_delegation_inheritance/README.md",
"chars": 5117,
"preview": "# odoo 繼承 - delegation inheritance\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 繼承 - delegation inheritance](https://"
},
{
"path": "demo_delegation_inheritance/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_delegation_inheritance/__manifest__.py",
"chars": 520,
"preview": "{\n 'name': 'demo_delegation_inheritance',\n 'version': '12.0.1.0.0',\n 'summary': 'demo_delegation_inheritance',\n"
},
{
"path": "demo_delegation_inheritance/models/__init__.py",
"chars": 20,
"preview": "from . import model\n"
},
{
"path": "demo_delegation_inheritance/models/model.py",
"chars": 591,
"preview": "from odoo import models, fields, api\n\n# ref.\n#\n# addons/product/models/product.py\n# class ProductProduct(models.Model):\n"
},
{
"path": "demo_delegation_inheritance/security/ir.model.access.csv",
"chars": 348,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_delegation_user,Demo Delegation"
},
{
"path": "demo_delegation_inheritance/security/security.xml",
"chars": 900,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n\n <record id=\"module_demo_delegation_tutorial\" model=\"ir.module.category\">\n <field nam"
},
{
"path": "demo_delegation_inheritance/views/menu.xml",
"chars": 683,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <!-- demo_delegation_tutorial App Menu -->\n <menuitem id=\"demo_delegation_tutorial_m"
},
{
"path": "demo_delegation_inheritance/views/view.xml",
"chars": 1341,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n\n <record id=\"view_form_demo_delegation_tutorial\" model=\"ir.ui.view\">\n <field name=\"nam"
},
{
"path": "demo_expense_tutorial_v1/README.md",
"chars": 69891,
"preview": "# odoo 入門篇\n\n建議觀看影片, 會更清楚 :smile:\n\n* [Youtube Tutorial - odoo 手把手教學 - Many2one - part1](https://youtu.be/vb_Z8KCI-wk) - ["
},
{
"path": "demo_expense_tutorial_v1/__init__.py",
"chars": 48,
"preview": "# from . import controllers\nfrom . import models"
},
{
"path": "demo_expense_tutorial_v1/__manifest__.py",
"chars": 1042,
"preview": "{\n 'name': \"demo expense tutorial v1\",\n 'summary': \"\"\"\n tutorial - Many2one, Many2many, One2many\n de"
},
{
"path": "demo_expense_tutorial_v1/controllers/__init__.py",
"chars": 25,
"preview": "from . import controllers"
},
{
"path": "demo_expense_tutorial_v1/controllers/controllers.py",
"chars": 24,
"preview": "# from odoo import http\n"
},
{
"path": "demo_expense_tutorial_v1/data/demo_expense_tutorial_data.xml",
"chars": 890,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n <data noupdate=\"0\">\n <!-- demo.expense.tutorial -->\n <re"
},
{
"path": "demo_expense_tutorial_v1/demo/demo.xml",
"chars": 38,
"preview": "<odoo>\n <data>\n\n </data>\n</odoo>"
},
{
"path": "demo_expense_tutorial_v1/models/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_expense_tutorial_v1/models/models.py",
"chars": 7553,
"preview": "from odoo import models, fields, api\nfrom odoo.exceptions import UserError\n\nclass DemoTag(models.Model):\n _name = 'de"
},
{
"path": "demo_expense_tutorial_v1/security/ir.model.access.csv",
"chars": 900,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_expense_user,Demo Expense Tutor"
},
{
"path": "demo_expense_tutorial_v1/security/ir_rule.xml",
"chars": 1149,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n <data noupdate=\"1\">\n\n <record id=\"ir_rule_demo_expense_user\" mo"
},
{
"path": "demo_expense_tutorial_v1/security/security.xml",
"chars": 878,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n\n <record id=\"module_demo_expense_tutorial\" model=\"ir.module.category\">\n <field name=\""
},
{
"path": "demo_expense_tutorial_v1/views/menu.xml",
"chars": 7596,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <!-- demo_expense_tutorial App Menu -->\n <menuitem id=\"demo_expense_tutorial_menu\"\n "
},
{
"path": "demo_expense_tutorial_v1/views/view.xml",
"chars": 10676,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n\n <record id=\"view_form_demo_expense_tutorial\" model=\"ir.ui.view\">\n <field name=\"name\">"
},
{
"path": "demo_fields_view_get_tutorial/README.md",
"chars": 3311,
"preview": "# odoo fields_view_get 介紹教學\n\n建議觀看影片, 會更清楚 :smile:\n\n* [Youtube Tutorial - odoo fields_view_get 介紹教學](https://youtu.be/TpE"
},
{
"path": "demo_fields_view_get_tutorial/__init__.py",
"chars": 20,
"preview": "from . import models"
},
{
"path": "demo_fields_view_get_tutorial/__manifest__.py",
"chars": 728,
"preview": "{\n 'name': \"demo_fields_view_get_tutorial\",\n 'summary': 'tutorial - fields_view_get',\n 'description': \"\"\"\n "
},
{
"path": "demo_fields_view_get_tutorial/models/__init__.py",
"chars": 30,
"preview": "from . import account_invoice\n"
},
{
"path": "demo_fields_view_get_tutorial/models/account_invoice.py",
"chars": 1524,
"preview": "import json\nfrom odoo import api, exceptions, fields, models, _\nfrom lxml import etree\nfrom odoo.exceptions import UserE"
},
{
"path": "demo_hierarchy_tutorial/README.md",
"chars": 5610,
"preview": "# odoo hierarchy 實作\n\n建議觀看影片, 會更清楚 :smile:\n\n* [Youtube Tutorial - odoo 手把手教學 hierarchy - part1](https://youtu.be/O_ch9553"
},
{
"path": "demo_hierarchy_tutorial/__init__.py",
"chars": 20,
"preview": "from . import models"
},
{
"path": "demo_hierarchy_tutorial/__manifest__.py",
"chars": 814,
"preview": "{\n 'name': \"demo hierarchy tutorial\",\n 'summary': \"\"\"\n demo_hierarchy_tutorial\n \"\"\",\n 'description': "
},
{
"path": "demo_hierarchy_tutorial/models/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_hierarchy_tutorial/models/models.py",
"chars": 543,
"preview": "from odoo import models, fields, api\n\nclass DemoHierarchyTutorial(models.Model):\n _name = 'demo.hierarchy'\n _descr"
},
{
"path": "demo_hierarchy_tutorial/security/ir.model.access.csv",
"chars": 303,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_hierarchy_user,Demo Hierarchy U"
},
{
"path": "demo_hierarchy_tutorial/security/security.xml",
"chars": 829,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n\n <record id=\"module_demo_hierarchy\" model=\"ir.module.category\">\n <field name=\"name\">D"
},
{
"path": "demo_hierarchy_tutorial/views/menu.xml",
"chars": 534,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <!-- demo_hierarchy App Menu -->\n <menuitem id=\"demo_hierarchy_menu\"\n name=\"Dem"
},
{
"path": "demo_hierarchy_tutorial/views/view.xml",
"chars": 1409,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <record id=\"view_form_demo_hierarchy\" model=\"ir.ui.view\">\n <field name=\"name\">Demo Hie"
},
{
"path": "demo_hook_tutorial/README.md",
"chars": 2316,
"preview": "# odoo 觀念 - 實作 init hook\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - 實作 init hook](https://youtu.be/2ZmfH3wB"
},
{
"path": "demo_hook_tutorial/__init__.py",
"chars": 662,
"preview": "from odoo import api, SUPERUSER_ID\n\nimport logging\n\n_logger = logging.getLogger(__name__)\n\ndef pre_init_hook(cr):\n en"
},
{
"path": "demo_hook_tutorial/__manifest__.py",
"chars": 711,
"preview": "{\n 'name': \"demo hook tutorial\",\n\n 'summary': \"\"\"demo hook tutorial\"\"\",\n\n 'description': \"\"\"demo hook tutorial\""
},
{
"path": "demo_i18n_expense_tutorial/README.md",
"chars": 3582,
"preview": "# odoo 觀念 - Translating 翻譯教學 i18n\n\n建議觀看影片, 會更清楚 :smile:\n\n* [Youtube Tutorial - odoo 觀念 - Translating 翻譯教學 i18n - part1]("
},
{
"path": "demo_i18n_expense_tutorial/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "demo_i18n_expense_tutorial/__manifest__.py",
"chars": 628,
"preview": "{\n 'name': \"demo i18n expense tutorial\",\n 'summary': \"\"\"\n i18n tutorial\n \"\"\",\n 'description': \"\"\"\n "
},
{
"path": "demo_i18n_expense_tutorial/i18n/zh_TW.po",
"chars": 708,
"preview": "# Translation of Odoo Server.\n# This file contains the translation of the following modules:\n# * hr_expense\n#\n\nmsgid \"\"\n"
},
{
"path": "demo_inherit_controller/README.md",
"chars": 4403,
"preview": "# odoo 觀念 - 如何繼承 inherit controller\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - 如何繼承 inherit controller](htt"
},
{
"path": "demo_inherit_controller/__init__.py",
"chars": 25,
"preview": "from . import controllers"
},
{
"path": "demo_inherit_controller/__manifest__.py",
"chars": 622,
"preview": "{\n 'name': \"demo inherit controller\",\n 'summary': \"\"\"demo inherit controller\"\"\",\n 'description': \"\"\"demo inheri"
},
{
"path": "demo_inherit_controller/controllers/__init__.py",
"chars": 21,
"preview": "from . import portal\n"
},
{
"path": "demo_inherit_controller/controllers/portal.py",
"chars": 2900,
"preview": "from odoo import http, _\nfrom odoo.http import request\nfrom odoo.addons.portal.controllers.portal import pager as portal"
},
{
"path": "demo_multi_company/README.md",
"chars": 4590,
"preview": "# odoo 觀念 - multi company\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - multi company - part1](https://youtu.b"
},
{
"path": "demo_multi_company/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_multi_company/__manifest__.py",
"chars": 689,
"preview": "{\n 'name': \"demo multi company\",\n\n 'summary': \"\"\"\"\"\",\n\n 'description': \"\"\"\"\"\",\n\n 'author': \"My Company\",\n "
},
{
"path": "demo_multi_company/models/__init__.py",
"chars": 20,
"preview": "from . import model\n"
},
{
"path": "demo_multi_company/models/model.py",
"chars": 991,
"preview": "from odoo import models, fields\nimport logging\n\n_logger = logging.getLogger(__name__)\n\nclass DemoCompany(models.Model):\n"
},
{
"path": "demo_multi_company/security/ir.model.access.csv",
"chars": 188,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_company_user,Demo Company Tutor"
},
{
"path": "demo_multi_company/security/security.xml",
"chars": 571,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n\n <record id=\"module_demo_company_tutorial\" model=\"ir.module.category\">\n <field name=\""
},
{
"path": "demo_multi_company/views/menu.xml",
"chars": 502,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <!-- Demo Company Menu -->\n <menuitem id=\"demo_company_menu\"\n name=\"Demo Compan"
},
{
"path": "demo_multi_company/views/view.xml",
"chars": 914,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <record id=\"view_form_demo_company\" model=\"ir.ui.view\">\n <field name=\"name\">Demo Compa"
},
{
"path": "demo_odoo_tutorial/README.md",
"chars": 33364,
"preview": "# odoo 手把手建立第一個 addons\n\n* [Youtube Tutorial - odoo 手把手建立第一個 addons - part1](https://youtu.be/GMrPakLNh8g) - 介紹 model - ["
},
{
"path": "demo_odoo_tutorial/__init__.py",
"chars": 46,
"preview": "from . import controllers\nfrom . import models"
},
{
"path": "demo_odoo_tutorial/__manifest__.py",
"chars": 1066,
"preview": "{\n 'name': \"demo odoo tutorial\",\n 'summary': \"\"\"\n basic tutorial -\n demo odoo tutorial\n \"\"\",\n "
},
{
"path": "demo_odoo_tutorial/controllers/__init__.py",
"chars": 25,
"preview": "from . import controllers"
},
{
"path": "demo_odoo_tutorial/controllers/controllers.py",
"chars": 370,
"preview": "from odoo import http\n\nclass DemoOdoo(http.Controller):\n\n @http.route('/demo/odoo', auth='user', website=True)\n de"
},
{
"path": "demo_odoo_tutorial/data/data_demo_odoo.xml",
"chars": 508,
"preview": "<odoo noupdate=\"1\">\n <record id=\"demo_odoo_1\" model=\"demo.odoo.tutorial\">\n <field name=\"name\">demo_odoo_1</field>\n"
},
{
"path": "demo_odoo_tutorial/demo/demo.xml",
"chars": 38,
"preview": "<odoo>\n <data>\n\n </data>\n</odoo>"
},
{
"path": "demo_odoo_tutorial/models/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_odoo_tutorial/models/models.py",
"chars": 4652,
"preview": "from odoo import models, fields, api, tools\nfrom odoo.exceptions import ValidationError\nfrom datetime import datetime\n\nc"
},
{
"path": "demo_odoo_tutorial/reports/report.xml",
"chars": 1702,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n <template id=\"report_demo_odoo_tutorial\">\n <t t-call=\"web.html_"
},
{
"path": "demo_odoo_tutorial/security/ir.model.access.csv",
"chars": 450,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_odoo_user,Demo Odoo Tutorial Us"
},
{
"path": "demo_odoo_tutorial/security/security.xml",
"chars": 857,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n\n <record id=\"module_demo_odoo_tutorial\" model=\"ir.module.category\">\n <field name=\"nam"
},
{
"path": "demo_odoo_tutorial/tests/__init__.py",
"chars": 96,
"preview": "from . import test_demo_odoo_transactioncase\nfrom . import test_demo_odoo_singletransactioncase\n"
},
{
"path": "demo_odoo_tutorial/tests/test_demo_odoo_singletransactioncase.py",
"chars": 1099,
"preview": "from odoo.exceptions import UserError, AccessError, ValidationError\nfrom odoo.tests.common import SingleTransactionCase\n"
},
{
"path": "demo_odoo_tutorial/tests/test_demo_odoo_transactioncase.py",
"chars": 1114,
"preview": "from odoo.exceptions import UserError, AccessError, ValidationError\nfrom odoo.tests.common import TransactionCase, tagge"
},
{
"path": "demo_odoo_tutorial/views/demo_odoo_template.xml",
"chars": 487,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n\n<template id=\"demo_odoo_template\" name=\"Demo odoo List\">\n <t t-call=\"web"
},
{
"path": "demo_odoo_tutorial/views/menu.xml",
"chars": 975,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <!-- demo_odoo_tutorial App Menu -->\n <menuitem id=\"demo_odoo_tutorial_menu\"\n "
},
{
"path": "demo_odoo_tutorial/views/view.xml",
"chars": 2177,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n\n <record id=\"view_form_demo_odoo_tutorial\" model=\"ir.ui.view\">\n <field name=\"name\">Dem"
},
{
"path": "demo_odoo_tutorial_wizard/README.md",
"chars": 8243,
"preview": "# odoo 觀念 - TransientModel - Wizard\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 觀念 - TransientModel - Wizard](https:"
},
{
"path": "demo_odoo_tutorial_wizard/__init__.py",
"chars": 41,
"preview": "from . import models\nfrom . import wizard"
},
{
"path": "demo_odoo_tutorial_wizard/__manifest__.py",
"chars": 908,
"preview": "{\n 'name': \"demo odoo tutorial wizard\",\n 'summary': \"\"\"\n wizard tutorial -\n demo odoo tutorial wizar"
},
{
"path": "demo_odoo_tutorial_wizard/models/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_odoo_tutorial_wizard/models/models.py",
"chars": 727,
"preview": "from odoo import models, fields, api\nfrom odoo.exceptions import ValidationError\n\nclass DemoOdooWizardTutorial(models.Mo"
},
{
"path": "demo_odoo_tutorial_wizard/security/ir.model.access.csv",
"chars": 373,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_odoo_wizard_user,Demo Odoo Wiza"
},
{
"path": "demo_odoo_tutorial_wizard/security/security.xml",
"chars": 906,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n\n <record id=\"module_demo_odoo_wizard_tutorial\" model=\"ir.module.category\">\n <field na"
},
{
"path": "demo_odoo_tutorial_wizard/views/menu.xml",
"chars": 593,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <!-- demo_odoo_tutorial App Menu -->\n <menuitem id=\"demo_odoo_tutorial_menu\"\n "
},
{
"path": "demo_odoo_tutorial_wizard/views/view.xml",
"chars": 1405,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n\n <record id=\"view_form_demo_odoo_tutorial\" model=\"ir.ui.view\">\n <field name=\"name\">Dem"
},
{
"path": "demo_odoo_tutorial_wizard/wizard/__init__.py",
"chars": 27,
"preview": "from . import model_wizard\n"
},
{
"path": "demo_odoo_tutorial_wizard/wizard/model_wizard.py",
"chars": 1338,
"preview": "from odoo import api, fields, models\nimport logging\n\n_logger = logging.getLogger(__name__)\n\nclass DemoWizard(models.Tran"
},
{
"path": "demo_odoo_tutorial_wizard/wizard/model_wizard.xml",
"chars": 1516,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n <record id=\"demo_wizard_view_form\" model=\"ir.ui.view\">\n <field "
},
{
"path": "demo_orm_cache/README.md",
"chars": 2289,
"preview": "# odoo 觀念 - orm cache 說明\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - orm cache 說明](https://youtu.be/AXi7c4EQ"
},
{
"path": "demo_orm_cache/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_orm_cache/__manifest__.py",
"chars": 682,
"preview": "{\n 'name': \"demo orm cache\",\n\n 'summary': \"\"\"\"\"\",\n\n 'description': \"\"\"\"\"\",\n\n 'author': \"My Company\",\n 'we"
},
{
"path": "demo_orm_cache/models/__init__.py",
"chars": 20,
"preview": "from . import model\n"
},
{
"path": "demo_orm_cache/models/model.py",
"chars": 985,
"preview": "from odoo import models, fields, tools\nfrom odoo.exceptions import ValidationError\nimport logging\n\n_logger = logging.get"
},
{
"path": "demo_orm_cache/security/ir.model.access.csv",
"chars": 179,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_cache_user,Demo Cache Tutorial "
},
{
"path": "demo_orm_cache/security/security.xml",
"chars": 563,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n\n <record id=\"module_demo_cache_tutorial\" model=\"ir.module.category\">\n <field name=\"na"
},
{
"path": "demo_orm_cache/views/menu.xml",
"chars": 478,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <!-- Demo Cache Menu -->\n <menuitem id=\"demo_cache_menu\"\n name=\"Demo Cache\" />\n"
},
{
"path": "demo_orm_cache/views/view.xml",
"chars": 1156,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <record id=\"view_form_demo_cache\" model=\"ir.ui.view\">\n <field name=\"name\">Demo Cache F"
},
{
"path": "demo_prototype_inheritance/README.md",
"chars": 3298,
"preview": "# odoo 繼承 - prototype inheritance\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 繼承 - prototype inheritance](https://yo"
},
{
"path": "demo_prototype_inheritance/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_prototype_inheritance/__manifest__.py",
"chars": 488,
"preview": "{\n 'name': 'demo_prototype_inheritance',\n 'version': '12.0.1.0.0',\n 'summary': 'demo_prototype_inheritance',\n "
},
{
"path": "demo_prototype_inheritance/models/__init__.py",
"chars": 20,
"preview": "from . import model\n"
},
{
"path": "demo_prototype_inheritance/models/model.py",
"chars": 345,
"preview": "from odoo import models, fields, api\n\n# prototype inheritance\nclass PrototypeInheritance(models.Model):\n _name = 'dem"
},
{
"path": "demo_prototype_inheritance/security/ir.model.access.csv",
"chars": 340,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_prototype_user,Demo Prototype T"
},
{
"path": "demo_prototype_inheritance/security/security.xml",
"chars": 893,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n\n <record id=\"module_demo_prototype_tutorial\" model=\"ir.module.category\">\n <field name"
},
{
"path": "demo_prototype_inheritance/views/menu.xml",
"chars": 686,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <!-- demo_prototype_tutorial App Menu -->\n <menuitem id=\"demo_prototype_tutorial_men"
},
{
"path": "demo_prototype_inheritance/views/view.xml",
"chars": 899,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <record id=\"view_form_demo_prototype_tutorial\" model=\"ir.ui.view\">\n <field name=\"name\""
},
{
"path": "demo_recruitment_website_form/README.md",
"chars": 10127,
"preview": "# odoo 觀念 - recruitment_website_form 介紹\n\n[Youtube Tutorial - odoo 觀念 - recruitment_website_form 介紹](https://youtu.be/FDv"
},
{
"path": "demo_sale_scan_barcode/README.md",
"chars": 4315,
"preview": "# odoo 實作 scan barcode\n\n建議觀看影片, 因為 scanner 會看的更清楚 :smile:\n\n[Youtube Tutorial - odoo - 實作 scan barcode](https://youtu.be/"
},
{
"path": "demo_sale_scan_barcode/__init__.py",
"chars": 20,
"preview": "from . import models"
},
{
"path": "demo_sale_scan_barcode/__manifest__.py",
"chars": 744,
"preview": "{\n 'name': \"demo_sale_scan_barcode\",\n 'summary': \"\"\"\n demo_sale_scan_barcode\n \"\"\",\n 'description': \"\""
},
{
"path": "demo_sale_scan_barcode/models/__init__.py",
"chars": 21,
"preview": "from . import models\n"
},
{
"path": "demo_sale_scan_barcode/models/models.py",
"chars": 616,
"preview": "from odoo import models, fields, api\n\nclass SaleOrderBarcodes(models.Model):\n _name = \"sale.order\"\n _inherit = [\"s"
},
{
"path": "demo_sale_scan_barcode/views/view.xml",
"chars": 535,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <record id=\"view_order_form_scan_barcode\" model=\"ir.ui.view\">\n <field name=\"name"
},
{
"path": "demo_scheduler/README.md",
"chars": 2585,
"preview": "# odoo 觀念 - scheduler\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - scheduler](https://youtu.be/uvQTHsKu3Ic)\n\n"
},
{
"path": "demo_scheduler/__init__.py",
"chars": 20,
"preview": "from . import models"
},
{
"path": "demo_scheduler/__manifest__.py",
"chars": 662,
"preview": "{\n 'name': \"demo scheduler\",\n\n 'summary': \"\"\"\"\"\",\n\n 'description': \"\"\"\"\"\",\n\n 'author': \"My Company\",\n 'we"
},
{
"path": "demo_scheduler/models/__init__.py",
"chars": 24,
"preview": "from . import scheduler\n"
},
{
"path": "demo_scheduler/models/scheduler.py",
"chars": 300,
"preview": "from odoo import models, fields, api\nimport logging\n\n_logger = logging.getLogger(__name__)\n\nclass DemoScheduler(models.M"
},
{
"path": "demo_scheduler/security/ir.model.access.csv",
"chars": 322,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_scheduler_user,Demo Scheduler T"
},
{
"path": "demo_scheduler/security/security.xml",
"chars": 865,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n\n <record id=\"module_demo_scheduler_tutorial\" model=\"ir.module.category\">\n <field name"
},
{
"path": "demo_scheduler/views/scheduler.xml",
"chars": 589,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n <data noupdate=\"0\">\n <record id=\"demo_scheduler\" model=\"ir.cron\">\n <field name=\""
},
{
"path": "demo_sequence/README.md",
"chars": 2407,
"preview": "# odoo 觀念 - sequence\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - sequence](https://youtu.be/u8v0hzEXwpc)\n\n建議"
},
{
"path": "demo_sequence/__init__.py",
"chars": 20,
"preview": "from . import models"
},
{
"path": "demo_sequence/__manifest__.py",
"chars": 715,
"preview": "{\n 'name': \"demo sequence\",\n 'summary': \"\"\"\"\"\",\n\n 'description': \"\"\"\"\"\",\n\n 'author': \"My Company\",\n 'webs"
},
{
"path": "demo_sequence/data/sequence_data.xml",
"chars": 534,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n\n <!-- demo_sequence -->\n <data noupdate=\"0\">\n <record id=\"demo_sequence_id\" model=\"ir"
},
{
"path": "demo_sequence/models/__init__.py",
"chars": 19,
"preview": "from . import model"
},
{
"path": "demo_sequence/models/model.py",
"chars": 433,
"preview": "from odoo import models, fields, api\n\nclass DemoSequence(models.Model):\n _name = 'demo.sequence'\n _description = '"
},
{
"path": "demo_sequence/security/ir.model.access.csv",
"chars": 295,
"preview": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_sequence_user,Demo Sequence Use"
},
{
"path": "demo_sequence/security/security.xml",
"chars": 840,
"preview": "<?xml version=\"1.0\" ?>\n<odoo>\n\n <record id=\"module_demo_sequence_category\" model=\"ir.module.category\">\n <field name="
},
{
"path": "demo_sequence/views/menu.xml",
"chars": 514,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n <!-- Demo Sequence Menu -->\n <menuitem id=\"demo_sequence_menu\"\n name=\"Demo Sequ"
},
{
"path": "demo_sequence/views/view.xml",
"chars": 625,
"preview": "<?xml version=\"1.0\"?>\n<odoo>\n\n\n <record id=\"view_form_demo_sequence\" model=\"ir.ui.view\">\n <field name=\"name\">Demo Se"
},
{
"path": "domain_operator_tutorial/README.md",
"chars": 1577,
"preview": "# Odoo Domain Operator 教學\n\n[Youtube Tutorial - odoo 手把手教學 - Odoo Domain Operator 教學](https://youtu.be/E0LrhmozZQI)\n\n建議在閱"
},
{
"path": "odoo_domain_tutorial/README.md",
"chars": 3373,
"preview": "# Odoo Domain 教學\n\n[Youtube Tutorial - odoo 手把手教學 - Odoo Domain 教學](https://youtu.be/Gr8eXYRSrtM)\n\n先附上 odoo [domains](htt"
},
{
"path": "odoo_index_tutorial/README.md",
"chars": 2003,
"preview": "# odoo index 教學\n\n* [Youtube Tutorial - odoo index 教學](https://youtu.be/S9jahU9aG-s)\n\nodoo 的 index 主要就是 postgresql 的 inde"
},
{
"path": "session_redis_tutorial/README.md",
"chars": 3859,
"preview": "# odoo session_redis 教學\n\n[Youtube Tutorial - odoo 手把手教學 - session_redis 教學](https://youtu.be/WD7W9RwusS0)\n\n## 簡介\n\n這篇主要是教"
},
{
"path": "session_redis_tutorial/docker-compose.yml",
"chars": 339,
"preview": "version: '3.5'\n\nservices:\n cache:\n image: redis\n restart: always\n ports:\n - '6379:6379'\n# command: re"
},
{
"path": "xml-rpc-odoo/README.md",
"chars": 3270,
"preview": "# 如何使用 python xmlrpc 連接 odoo-12\n\n此版本為 odoo12, odoo14 版本請參考 [odoo14](https://github.com/twtrubiks/odoo-demo-addons-tutori"
},
{
"path": "xml-rpc-odoo/demo.py",
"chars": 11545,
"preview": "import xmlrpc.client\nfrom pprint import pprint\n\n# use xmlrpc 時, 建議回傳 true\n# The reason is that not all client implementa"
}
]
About this extraction
This page contains the full source code of the twtrubiks/odoo-demo-addons-tutorial GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 197 files (357.9 KB), approximately 117.0k tokens, and a symbol index with 113 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.