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']) ``` ![alt tag](https://i.imgur.com/6eyegIE.png) 同等如下 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']) ``` ![alt tag](https://i.imgur.com/BhNR227.png) 同等如下 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) ``` ![alt tag](https://i.imgur.com/IaaFXae.png) 同等如下 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` 這些都是可用的參數. ![alt tag](https://i.imgur.com/cp1zX6P.png) `search_count` ```python >>> self.env['res.partner'].search_count([]) 73 ``` `recordset.ids` 回傳 recordset 全部的 id ```python >>> recordset = self.env['res.partner'].search([]) >>> recordset.ids [14, 26, 33, 27, 10, 35, 18, 19, 11, 20, 22, 31, 23, 15, 34, 12, 21, 25, 37, 24, 36, 30, 38, 13, 29, 28, 9, 17, 32, 16, 1, 39, 40, 8, 7, 3] ``` 繼續使用上面的範例 `recordset.filtered(func)` 和 python 中的 [filter](https://github.com/twtrubiks/python-notes/blob/master/filter.py) 類似 ```python >>> recordset.filtered(lambda r: r.name.startswith('C')) res.partner(33, 39) ``` `recordset.mapped(func)` 和 python 中的 [map](https://github.com/twtrubiks/python-notes/blob/master/map_tutorial.py) 類似 ```python >>> recordset.mapped('name') ['Azure Interior', 'Brandon Freeman', 'Colleen Diaz', 'Nicole Ford', 'Deco Addict', 'Addison Olson', 'Douglas Fletcher', 'Floyd Steward',... ``` `recordset.sorted(func)` 和 python 中的 [sorted](https://github.com/twtrubiks/python-notes/blob/master/sorted.py) 類似 ```python >>> recordset.sorted(key=lambda r: r.id, reverse=True) res.partner(40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, ... ``` `create` ```python >>> partner = self.env['res.partner'] >>> partner.create({'name': 'twtrubiks', 'is_company': True}) res.partner(66) >>> self.env.cr.commit() # 需要特別執行這行才會寫進資料庫中 ``` `write` update data ```python >>> partner = self.env['res.partner'].browse([2]) >>> partner res.partner(2,) >>> partner.name 'OdooBot' >>> partner.write({'name': 'hello'}) True >>> partner.name 'hello' >>> self.env.cr.commit() # 需要特別執行這行才會寫進資料庫中 ``` 當你更新 `One2many` 和 `Many2many` 時, 要使用比較特別的語言, 我之後會補充上來. `copy` 如果 fields 有定義 `copy=False`, 就沒有辦法複製. ```python # odoo/addons/base/data/res_users_demo.xml >>> demo = self.env.ref('base.user_demo') >>> demo.copy({'name': 'twtrubiks', 'login': 'twtrubiks', 'email':''}) >>> self.env.cr.commit() # 需要特別執行這行才會寫進資料庫中 ``` `delete` ```python >>> user = self.env['res.users'].browse([3]) >>> user.unlink() 2020-06-21 06:45:51,958 19735 INFO odoo odoo.models.unlink: User #1 deleted ir.model.data records with IDs: [1884] 2020-06-21 06:45:51,996 19735 INFO odoo odoo.models.unlink: User #1 deleted res.users records with IDs: [3] True >>> self.env.cr.commit() # 需要特別執行這行才會寫進資料庫中 ``` `sudo` * [Youtube Tutorial - odoo 基本教學 - sudo](https://youtu.be/nAmNmPCSbGg) 可參考 odoo 原始碼的 `odoo/models.py` ```python def sudo(self, user=SUPERUSER_ID): """ sudo([user=SUPERUSER]) Returns a new version of this recordset attached to the provided user. By default this returns a ``SUPERUSER`` recordset, where access control and record rules are bypassed. .. note:: Using ``sudo`` could cause data access to cross the boundaries of record rules, possibly mixing records that are meant to be isolated (e.g. records from different companies in multi-company environments). It may lead to un-intuitive results in methods which select one record among many - for example getting the default company, or selecting a Bill of Materials. .. note:: Because the record rules and access control will have to be re-evaluated, the new recordset will not benefit from the current environment's data cache, so later data access may incur extra delays while re-fetching from the database. The returned recordset has the same prefetch object as ``self``. """ return self.with_env(self.env(user=user)) ``` `sudo([user=SUPERUSER])` 如果裡面沒有填入 user id, 預設就是使用 SUPERUSER, 如果 有帶入 user id, 就是使用指定的 user 的權限. 注意 :exclamation: 這是 odoo12 的作法, 從 odoo13 開始切換 user 已經改成 `with_user(user)` :exclamation: 可參考 [odoo13-souece code](https://github.com/odoo/odoo/blob/13.0/odoo/models.py#L5160) `deprecated use of sudo(user), use with_user(user) instead` 來看下面這個例子, 因為沒有指定 user id, 所以是使用 SUPERUSER, 自然可以看到全部的 records, ```python >>> self.env['hr.expense'].sudo().search([]) hr.expense(4, 3, 2, 1) ``` 再來看這個例子, user_id = 6 只能看到自己的 records, 因為他是一般的 user, ```python >>> self.env['hr.expense'].sudo(user=6).search([]) hr.expense(4, 3) ``` 也就是說, 知道這個特性, 我們甚至可以讓沒有權限的人看到 records (請依照自己的需求去調整) :smile: 另外提醒一下, 這個 sudo 除了在 ORM 底下生效外, 在 QWeb 中也會生效, 如下方這段 code 是可行的, ```xml ...... Email ...... ``` `with_context` 可參考 odoo 原始碼的 `odoo/models.py` ```python def with_context(self, *args, **kwargs): """ with_context([context][, **overrides]) -> records Returns a new version of this recordset attached to an extended context. The extended context is either the provided ``context`` in which ``overrides`` are merged or the *current* context in which ``overrides`` are merged e.g.:: # current context is {'key1': True} r2 = records.with_context({}, key2=True) # -> r2._context is {'key2': True} r2 = records.with_context(key2=True) # -> r2._context is {'key1': True, 'key2': True} .. note: The returned recordset has the same prefetch object as ``self``. """ context = dict(args[0] if args else self._context, **kwargs) return self.with_env(self.env(context=context)) ``` `with_context` 可以用在很多地方, 這邊用一個翻譯的舉例, 如果我同時有 `en_US` 和 `zh_TW` 這兩個語言, 可以使用 `with_context`帶入不同的語言, 會自動依照語言進行翻譯, ```python >>> self.env['product.product'].with_context(lang='zh_TW').browse(41).name '飛機票' >>> self.env['product.product'].with_context(lang='en_US').browse(41).name 'Air Flight' ``` `with_context` 也常使用在傳值中, 可參考 [odoo 觀念-TransientModel-Wizard](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial_wizard) ### odoo shell 注意事項 * [Youtube Tutorial - odoo shell 教學 - 注意事項](https://youtu.be/YS6mGE3-y1k) odoo-shell 下 command 無法 save 問題, 當使用 Odoo Shell 測試資料時, 會發現當我們下了指令時, db 裡面的值沒有改變, 這時候必須另外執行 ```cmd self.env.cr.commit() ``` 在非 Odoo Shell 中會自動執行, 在 Odoo Shell 中不會自動執行 (需要手動執行). 除非你不想要把修改資料寫進去資料庫. ## odoo log 說明 ```log 2019-12-05 03:04:15,734 1 INFO localhost werkzeug: 172.18.0.1 - - [05/Dec/2019 03:04:15] "POST /longpolling/poll HTTP/1.0" 200 - {query_count} {query_time} {remaining_time} ``` query_count = query 次數 query_time = query 時間 remaining_time = 剩餘時間 如何透過 elk 搭配 odoo, 請參考 [docker-elk-tutorial 7.6.0](https://github.com/twtrubiks/docker-elk-tutorial/tree/elk-7.6.0) ## odoo 使用 gmail 發信 * [Youtube Tutorial - odoo 教學 - 使用 gmail 發信](https://youtu.be/CkFHCQuzEoo) gmail 需要一些前製作業, 建議先閱讀 [使用 Gmail 寄信 - 前置作業](https://github.com/twtrubiks/Flask-Mail-example#%E4%BD%BF%E7%94%A8-gmail-%E5%AF%84%E4%BF%A1---%E5%89%8D%E7%BD%AE%E4%BD%9C%E6%A5%AD) 這篇的 gmail 設定 Technical -> Email -> Outgoing Mail Servers ![alt tag](https://i.imgur.com/mZpaHWu.png) SMTP Server 填入 `smtp.gmail.com` SMTP Port 填入 `465` Connection Security 填入 `SSL/TLS` 填入自己的 Username 和 Password ![alt tag](https://i.imgur.com/V77o0hY.png) 建議輸入資料後, 可以先點選測試連接 (以下是成功的畫面) ![alt tag](https://i.imgur.com/rIXcdnH.png) 如果出現錯誤, 請確認你的帳密是否有錯誤 ![alt tag](https://i.imgur.com/yMVWVF5.png) 接著可以使用 odoo 內的 email 測試看是否可以成功發信 ![alt tag](https://i.imgur.com/sy1A69K.png) 成功發信 ![alt tag](https://i.imgur.com/CvMuelM.png) ## 如何全域修改時間日期格式 路徑為 Translations -> Languages, 點選語言, 就會看到以下的畫面, 圖片下方有一些參數的說明(可自行依照需求調整) ![alt tag](https://i.imgur.com/Z66LDIC.png) ## 其他注意事項 `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 快取的問題, 可以選這個選項) ![alt tag](https://i.imgur.com/EJTK0KY.png) ## 建議使用繼承 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, ![alt tag](https://i.imgur.com/M6goe84.png) 當你保存是會生效的. 可是當你去更新 `hr_expense` 的時候, 你會發生他被還原了. 所以, 使用 Edit View 選項去修改 view 可以使用在測試時. 正式的修改, 還是推薦使用 addons 繼承的方式 :smile: ## Donation 文章都是我自己研究內化後原創,如果有幫助到您,也想鼓勵我的話,歡迎請我喝一杯咖啡 :laughing: 綠界科技ECPAY ( 不需註冊會員 ) ![alt tag](https://payment.ecpay.com.tw/Upload/QRCode/201906/QRCode_672351b8-5ab3-42dd-9c7c-c24c3e6a10a0.png) [贊助者付款](http://bit.ly/2F7Jrha) 歐付寶 ( 需註冊會員 ) ![alt tag](https://i.imgur.com/LRct9xa.png) [贊助者付款](https://payment.opay.tw/Broadcaster/Donate/9E47FDEF85ABE383A0F5FC6A218606F8) ## 贊助名單 [贊助名單](https://github.com/twtrubiks/Thank-you-for-donate) ## License MIT license ================================================ FILE: demo_abstractmodel_tutorial/README.md ================================================ # 介紹 AbstractModel 建議觀看影片, 會更清楚 :smile: [Youtube Tutorial - odoo 手把手教學 - AbstractModel](https://youtu.be/jsMTVe12vRY) 建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係) [odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial) 主要介紹 demo_abstractmodel_tutorial ## 說明 `AbstractModel` AbstractModel = BaseModel, 注意 :exclamation: :exclamation: AbstractModel **不會** 在資料庫中產生對應的 table. AbstractModel 除了常常使用在之前介紹的 [demo_prototype_inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_prototype_inheritance) 中, 也很常使用在 report 中 (自定義一些額外的邏輯), 可參考 odoo code 中的 `addons/sale/report/sale_report.py`, ```python ...... class SaleOrderReportProforma(models.AbstractModel): _name = 'report.sale.report_saleproforma' _description = 'Proforma Report' def _get_report_values(self, docids, data=None): docs = self.env['sale.order'].browse(docids) return { 'doc_ids': docs.ids, 'doc_model': 'sale.order', 'docs': docs, 'proforma': True } ``` 假如你的 report 有額外的邏輯, 可以將邏輯寫在 `_get_report_values` 中. 請一定要先了解 Transient Model, 如果不了解可參考 [demo_odoo_tutorial_wizard](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial_wizard). 開始今天的介紹 :smile: 先來看 [wizard/model_wizard.py](wizard/model_wizard.py) ```python class ReportWizard(models.TransientModel): _name = 'report.wizard' _description = "Report Wizard" date_start = fields.Date(string="Start Date", required=True, default=fields.Date.today) date_end = fields.Date(string="End Date", required=True, default=fields.Date.today) @api.multi def download_report(self): _logger.warning('=== CALL get_report ===') data = { 'ids': self.ids, 'model': self._name, 'form': { 'date_start': self.date_start, 'date_end': self.date_end, }, } return self.env.ref('demo_abstractmodel_tutorial.action_report_abstractmodel').report_action(self, data=data) ...... ``` 頁面上會有一個按鈕觸發 `download_report`, `demo_abstractmodel_tutorial.action_report_abstractmodel` 為 addons name + report id (report id 後面會說明) `addons_name.report_id` `report_action()` 會去 call `_get_report_values()`. [wizard/model_wizard.xml](wizard/model_wizard.xml) ```xml Custom Report report.wizard form
``` 這邊定義了基本的 form, 並且將 menu 設定在 `hr_expense.menu_hr_expense_reports` 之下. ![alt tag](https://i.imgur.com/BL4en9D.png) ![alt tag](https://i.imgur.com/VnuJXrI.png) 剛剛前面提到 report id `action_report_abstractmodel` 在 [reports/report.xml](reports/report.xml) ```xml ``` 分別設定了 template id `report_wizard_template` 以及 report id `action_report_abstractmodel`. `name` 的部份為 adddons name + template id. 也就是 `adddons_name.template_id`. 接著看 [wizard/model_wizard.py](wizard/model_wizard.py) 的後半段, ```python ...... class ReportExpenseAbstractModel(models.AbstractModel): _name = 'report.demo_abstractmodel_tutorial.report_wizard_template' _description = 'Report Expense Wizard' @api.model def _get_report_values(self, docids, data=None): _logger.warning('=== CALL get_report_values ===') date_start = data['form']['date_start'] date_end = data['form']['date_end'] docs = self.env['hr.expense'].search([ ('date', '>=', date_start), ('date', '<=', date_end)], order='date asc') return { 'doc_ids': data['ids'], 'doc_model': data['model'], 'date_start': date_start, 'date_end': date_end, 'docs': docs, } ``` 前面說過了 `report_action()` 會去 call `_get_report_values()`, 所以這邊定義了 `AbstractModel` 並且實作 `_get_report_values`. `_name` 這邊的比較特別, 要注意一下, 它的結構是由以下幾部份組成, report + addons name + template id `report.addons_name.template_id` 也就是 `report.demo_abstractmodel_tutorial.report_wizard_template`, report 這個 prefix 很重要, 請不要任意的拿掉 :exclamation: :exclamation: `_get_report_values` 則是我們額外的邏輯, 最後將資料回傳給 `report_wizard_template` 並且 render. ![alt tag](https://i.imgur.com/XqmRovl.png) ================================================ FILE: demo_abstractmodel_tutorial/__init__.py ================================================ from . import wizard ================================================ FILE: demo_abstractmodel_tutorial/__manifest__.py ================================================ { 'name': "demo_abstractmodel_tutorial", 'summary': """ AbstractModel report """, 'description': """ AbstractModel report """, 'author': "My Company", 'website': "http://www.yourcompany.com", # Categories can be used to filter modules in modules listing # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml # for the full list 'category': 'Uncategorized', 'version': '0.1', # any module necessary for this one to work correctly 'depends': ['hr_expense'], # always loaded 'data': [ 'wizard/model_wizard.xml', 'reports/report.xml' ], 'application': True, } ================================================ FILE: demo_abstractmodel_tutorial/reports/report.xml ================================================ ================================================ FILE: demo_abstractmodel_tutorial/wizard/__init__.py ================================================ from . import model_wizard ================================================ FILE: demo_abstractmodel_tutorial/wizard/model_wizard.py ================================================ from odoo import api, fields, models import logging _logger = logging.getLogger(__name__) class ReportWizard(models.TransientModel): _name = 'report.wizard' _description = "Report Wizard" date_start = fields.Date(string="Start Date", required=True, default=fields.Date.today) date_end = fields.Date(string="End Date", required=True, default=fields.Date.today) @api.multi def download_report(self): _logger.warning('=== CALL get_report ===') data = { 'ids': self.ids, 'model': self._name, 'form': { 'date_start': self.date_start, 'date_end': self.date_end, }, } return self.env.ref('demo_abstractmodel_tutorial.action_report_abstractmodel').report_action(self, data=data) class ReportExpenseAbstractModel(models.AbstractModel): _name = 'report.demo_abstractmodel_tutorial.report_wizard_template' _description = 'Report Expense Wizard' @api.model def _get_report_values(self, docids, data=None): _logger.warning('=== CALL get_report_values ===') date_start = data['form']['date_start'] date_end = data['form']['date_end'] docs = self.env['hr.expense'].search([ ('date', '>=', date_start), ('date', '<=', date_end)], order='date asc') return { 'doc_ids': data['ids'], 'doc_model': data['model'], 'date_start': date_start, 'date_end': date_end, 'docs': docs, } ================================================ FILE: demo_abstractmodel_tutorial/wizard/model_wizard.xml ================================================ Custom Report report.wizard form
================================================ FILE: demo_abstractmodel_v2_tutorial/README.md ================================================ # 透過 AbstractModel 擴充 Model 建議觀看影片, 會更清楚 :smile: [Youtube Tutorial - odoo 手把手教學 - 透過 AbstractModel 擴充 Model](https://youtu.be/uW1PsDPcJF4) 之前有介紹過 AbstractModel 的文章 * [介紹 AbstractModel](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_abstractmodel_tutorial) 搭配 report 使用 * [odoo 繼承 - prototype inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_prototype_inheritance) 有提到 MailThread 這個 AbstractModel 今天要來進一步介紹, 如何透過 AbstractModel 擴充 Model :smile: ## 說明 首先, 再提一次 `AbstractModel` AbstractModel = BaseModel, 注意 :exclamation: :exclamation: AbstractModel **不會** 在資料庫中產生對應的 table. 先來看這個範例 [models/models.py](models/models.py) ```python from odoo import models, fields, api class DemoMixin(models.AbstractModel): _name = 'demo.mixin' _description = 'Demo Mixin' test_1 = fields.Float( string='test_1', default='2.2' ) test_2 = fields.Float( string="test_2", compute="_compute_field", ) def _compute_field(self): for record in self: record.test_2 = 3.0 class DemoModelTutorial(models.Model): _name = 'demo.model.tutorial' _inherit = 'demo.mixin' _description = 'Demo Model Tutorial' name = fields.Char(required=True, string="名稱") ``` 這邊 `demo.model.tutorial` 繼承了 `demo.mixin`, 所以在 db table 中, 會看到 `demo.mixin` 中的 fields. ![alt tag](https://i.imgur.com/0fYEUiS.png) 這邊稍微注意注意一下, 在 db 中只會有 `demo.model.tutorial` 的 table, 不會有 `demo.mixin` 的 table, 但是會有 `demo.mixin` 中的 fields, 也看不到 `test_2` fields, 原因是他是 _compute_field, 如果你想要看到包含 `test_2` fields, 可以到 odoo 的 model 後台觀看 ![alt tag](https://i.imgur.com/oiASNIP.png) `demo.mixin` 的 model 在 odoo 的後台也可以觀看 (但 db 中不會出現) ![alt tag](https://i.imgur.com/HkftQT3.png) ![alt tag](https://i.imgur.com/3ttRkzP.png) 在 tree, form ...... 都可以使用 `demo.mixin` 的 fields ![alt tag](https://i.imgur.com/hFCf2mR.png) 因為這個範例剛好只有一個 model 被繼承, 如果有兩個以上的 model 就更適合這樣寫了, 如下 ```python ...... class DemoModelTutorial(models.Model): _name = 'demo.model.tutorial' _inherit = 'demo.mixin' _description = 'Demo Model Tutorial' ...... class DemoModelTutorial_v2(models.Model): _name = 'demo.model.tutorial.v2' _inherit = 'demo.mixin' _description = 'Demo Model Tutorial v2' ...... class DemoModelTutorial_v3(models.Model): _name = 'demo.model.tutorial.v3' _inherit = 'demo.mixin' _description = 'Demo Model Tutorial v3' ...... ``` 這樣每個 model, 都會擁有 `demo.mixin` 的 fields, 不需要把重複的 code 在每個 model 中都寫一遍. 剛剛介紹的 model 是我們新建立的, 假如今天有一個 model 已經存在了, 想要用同樣的方式擴充 model, 可參考 [models/models_v2.py](models/models_v2.py) ```python from odoo import models, fields, api class DemoMixin2(models.AbstractModel): _name = 'demo.mixin2' _description = 'Demo Mixin2' test_v2 = fields.Float( string='test_v2', default='2.2' ) class DemoModelTutorial(models.Model): _name = 'demo.model.tutorial' _inherit = ['demo.model.tutorial', 'demo.mixin2'] pass ``` 這篇文章其實就是將 [odoo 實作 scan barcode](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_sale_scan_barcode) 的概念再說一次. 也可以去了解一下 [什麼是 Mixin in python](https://github.com/twtrubiks/python-notes/tree/master/what_is_the_mixin), 相信這樣大家會更了解他們的概念 :smile: ================================================ FILE: demo_abstractmodel_v2_tutorial/__init__.py ================================================ from . import models ================================================ FILE: demo_abstractmodel_v2_tutorial/__manifest__.py ================================================ { 'name': "demo_abstractmodel_v2_tutorial", 'summary': """ AbstractModel Extend Model """, 'description': """ AbstractModel Extend Model """, 'author': "My Company", 'website': "http://www.yourcompany.com", # Categories can be used to filter modules in modules listing # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml # for the full list 'category': 'Uncategorized', 'version': '0.1', # any module necessary for this one to work correctly 'depends': ['base'], # always loaded 'data': [ 'security/ir.model.access.csv', 'views/menu.xml', 'views/view.xml', ], 'application': True, } ================================================ FILE: demo_abstractmodel_v2_tutorial/models/__init__.py ================================================ from . import models # from . import models_v2 ================================================ FILE: demo_abstractmodel_v2_tutorial/models/models.py ================================================ from odoo import models, fields, api class DemoMixin(models.AbstractModel): _name = 'demo.mixin' _description = 'Demo Mixin' test_1 = fields.Float( string='test_1', default='2.2' ) test_2 = fields.Float( string="test_2", compute="_compute_field", ) def _compute_field(self): for record in self: record.test_2 = 3.0 class DemoModelTutorial(models.Model): _name = 'demo.model.tutorial' _inherit = 'demo.mixin' _description = 'Demo Model Tutorial' name = fields.Char(required=True, string="名稱") ================================================ FILE: demo_abstractmodel_v2_tutorial/models/models_v2.py ================================================ from odoo import models, fields, api class DemoMixin2(models.AbstractModel): _name = 'demo.mixin2' _description = 'Demo Mixin2' test_v2 = fields.Float( string='test_v2', default='2.2' ) class DemoModelTutorial(models.Model): _name = 'demo.model.tutorial' _inherit = ['demo.model.tutorial', 'demo.mixin2'] pass ================================================ FILE: demo_abstractmodel_v2_tutorial/security/ir.model.access.csv ================================================ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_demo_model_tutorial_user,Demo Model Tutorial Access,model_demo_model_tutorial,,1,1,1,1 ================================================ FILE: demo_abstractmodel_v2_tutorial/views/menu.xml ================================================ ================================================ FILE: demo_abstractmodel_v2_tutorial/views/view.xml ================================================ Demo Model Tutorial Form demo.model.tutorial
Demo Model Tutorial List demo.model.tutorial
================================================ FILE: demo_actions_singleton/README.md ================================================ # odoo 觀念 - actions 和 singleton 建議觀看影片, 會更清楚 :smile: [Youtube Tutorial - odoo 手把手教學 - actions and singleton](https://youtu.be/rRD9j4IAHWY) 建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係) [odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial) 本篇主要介紹 actions and singleton 以下將介紹這個 addons 的結構 ## 說明 [data/action_data.xml](data/action_data.xml) 首先, 先來建立一個 action ```xml ...... Action Demo code records.action_demo() Action Other Demo code raise Warning('Hello') ``` `binding_model_id` 綁定 model (可以綁定和 `model_id` 不同的 model). `state` 選擇使用的方式, 這邊使用 python code. `code` 執行的程式碼, `records` 代表所選的 record, `action_demo()` 代表呼叫的 function. 這邊的設定也可以在 Technical -> Actions -> Server Actions 看到 (如下) ![alt tag](https://i.imgur.com/RV5ryMj.png) 這邊就是剛剛 code 的設定 ![alt tag](https://i.imgur.com/vfeSIxp.png) 這邊的 Action To Do 是使用 Execute Python Code, 還有其他的選擇 ![alt tag](https://i.imgur.com/NqxJLzu.png) 記得也要將它加入 `__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 ![alt tag](https://i.imgur.com/cf6NeMr.png) 當你點下去, 會觸發你的 logger ![alt tag](https://i.imgur.com/wbkWbDV.png) 接下來說說 `self.ensure_one()`, 這就是確認是否為 `singleton`, 假如跳出 `raise ValueError exception`, 代表它非為 singleton 舉個例子, 像這邊選 兩條 record, 點下 Action Demo, ![alt tag](https://i.imgur.com/HMV3CHS.png) 你會發現跳出 error ![alt tag](https://i.imgur.com/tpti9Lb.png) 原因就是在這邊我們使用了 `self.ensure_one()` 確認 (確保只使用一條 record), 所以選兩條 reocrd 就會錯誤 :exclamation: :exclamation: 所以結論就是 `self.ensure_one()` 是要讓你檢查是否為 `singleton`. 另外, 空的 recordset 行為也像是 singleton, 當你 accessing fields 時, 它不會回傳 error ( 而是會回傳 `False`), 也因為這個特性, 所以我們才可以使用 `.` 歷遍 (traverse) records 而不用擔心錯誤 :smile: (舉例, 下面的例子) ```python >>> self.company_id.parent_id res.company() >>> self.company_id.parent_id.name False ``` 並不會發生錯誤, 只會回傳 `False`. 關於 `id="action_other_model_demo"` 可以看到他綁定的 model 是 `hr_expense` `` 所以, 要到 `hr_expense` 才可以看到這個 action ![alt tag](https://i.imgur.com/zQMwqca.png) ![alt tag](https://i.imgur.com/r5u0Voy.png) ================================================ FILE: demo_actions_singleton/__init__.py ================================================ from . import models ================================================ FILE: demo_actions_singleton/__manifest__.py ================================================ { 'name': "demo actions singleton", 'summary': """ tutorial - actions and singleton """, 'description': """ tutorial - actions and singleton """, 'author': "My Company", 'website': "http://www.yourcompany.com", # Categories can be used to filter modules in modules listing # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml # for the full list 'category': 'Uncategorized', 'version': '0.1', # any module necessary for this one to work correctly 'depends': ['base', 'hr_expense'], # always loaded 'data': [ 'security/security.xml', 'security/ir.model.access.csv', 'data/action_data.xml', 'views/menu.xml', 'views/view.xml', ], 'application': True, } ================================================ FILE: demo_actions_singleton/data/action_data.xml ================================================ Action Demo code records.action_demo() Action Other Demo code raise Warning('Hello') ================================================ FILE: demo_actions_singleton/models/__init__.py ================================================ from . import models ================================================ FILE: demo_actions_singleton/models/models.py ================================================ from odoo import models, fields, api import logging _logger = logging.getLogger(__name__) class DemoActionsSingleton(models.Model): _name = 'demo.actions.singleton' _description = 'Demo Actions Singleton' name = fields.Char('Description', required=True) @api.multi def action_demo(self): self.ensure_one() _logger.warning('=== CALL action_demo ===') ================================================ FILE: demo_actions_singleton/security/ir.model.access.csv ================================================ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_demo_singleton_user,Demo Actions Singleton User Access,model_demo_actions_singleton,demo_actions_singleton_group_user,1,1,1,0 access_demo_singleton_manager,Demo Actions Singleton Manager Access,model_demo_actions_singleton,demo_actions_singleton_group_manager,1,1,1,1 ================================================ FILE: demo_actions_singleton/security/security.xml ================================================ Demo actions singleton category User Manager ================================================ FILE: demo_actions_singleton/views/menu.xml ================================================ ================================================ FILE: demo_actions_singleton/views/view.xml ================================================ Demo Actions Singleton Form demo.actions.singleton
Demo Actions Singleton List demo.actions.singleton
================================================ FILE: demo_activity/README.md ================================================ # odoo 觀念 - activity 建議觀看影片, 會更清楚 :smile: [Youtube Tutorial - odoo 手把手教學 - activity](https://youtu.be/_i4yLHrXRdg) 建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係) [odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial) 本篇文章主要介紹 odoo 中的 activity 這部份 ## 說明 在 odoo 中, 肯定會常常看到 activity, 也就是如下圖的地方 ![alt tag](https://i.imgur.com/AIlIG2b.png) 因為要先定義一個 activity 的 data, 所以先來看 [data/mail_data.xml](data/mail_data.xml) ```xml ...... Activity Approval fa-dollar ...... ``` `name` 定義 activity 的名稱. `icon` 定義 icon. `res_model_id` 選擇對應的 model. 這個 activity 的 record 也可以在 odoo 中找到, 路徑為 Technical -> Email -> Activity Types ![alt tag](https://i.imgur.com/K6mubdq.png) ![alt tag](https://i.imgur.com/X98vjmh.png) 也可以進去修改相關的設定 ![alt tag](https://i.imgur.com/xxToZSP.png) 再來看 [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 ![alt tag](https://i.imgur.com/AD48O0S.png) 底下會顯示 activity ![alt tag](https://i.imgur.com/1af8U1V.png) 狀態列也會顯示有一個 activity ![alt tag](https://i.imgur.com/LYkQdkP.png) `activity_feedback` 同意(done)這個 activity 當點選範例的 activity_feedback ![alt tag](https://i.imgur.com/NXdAALh.png) 底下會顯示 activity 狀態 ![alt tag](https://i.imgur.com/OtNzxqC.png) `activity_unlink` 取消 activity ![alt tag](https://i.imgur.com/IEoHNhc.png) 這功能和直接點選 Cancel 是一樣的 ( activity 會消失 ) ![alt tag](https://i.imgur.com/ZzCNX4p.png) 也請記得設定 security [security/ir.model.access.csv](security/ir.model.access.csv) [security/security.xml](security/security.xml) 來看 [views/view.xml](views/view.xml) ```xml ...... demo.activity.form demo.activity
...... ``` `