[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\npip-wheel-metadata/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/"
  },
  {
    "path": "README.md",
    "content": "# odoo-demo-addons-tutorial-odoo-12\n\n此版本為 odoo12,\n\nodoo14 版本請參考 [odoo14](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/14.0) 分支.\n\nodoo15 版本請參考 [odoo15](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/15.0) 分支.\n\nodoo16 版本請參考 [odoo16](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/16.0) 分支.\n\nodoo17 版本請參考 [odoo17](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/17.0) 分支.\n\nodoo18 版本請參考 [odoo18](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/18.0) 分支.\n\nodoo19 版本請參考 [odoo19](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/19.0) 分支.\n\n本文章會持續更新 :smile:\n\n這邊文章主要是會手把手教大家撰寫 odoo 的 addons, 建議再閱讀這篇文章之前, 你已經看過以下的文章\n\n[odoo-development-environment-tutorial](https://github.com/twtrubiks/odoo-development-environment-tutorial) - 建立 odoo 開發環境 ( source code )\n\n[odoo-docker-tutorial](https://github.com/twtrubiks/odoo-docker-tutorial) - 利用 docker 快速建立 odoo 環境\n\n## 前言\n\n為甚麼我要寫一堆 addons, 因為其實 odoo 和 django 一樣的點是都很麻煩, 要寫個範例超級麻煩的,\n\n因為一個小地方錯可能就會造成錯誤之類的 :sweat:\n\n## addons 目錄\n\n非常建議按照順序看, 因為會一步一步帶大家 :smile:\n\n1. [odoo 手把手建立第一個 addons](demo_odoo_tutorial)\n\n2. [odoo 入門篇](demo_expense_tutorial_v1)\n\n3. [odoo 繼承 - class inheritance](demo_class_inheritance)\n\n4. [odoo 繼承 - prototype inheritance](demo_prototype_inheritance)\n\n5. [odoo 繼承 - delegation inheritance](demo_delegation_inheritance)\n\n6. [odoo 觀念 - actions 和 singleton](demo_actions_singleton)\n\n7. [odoo 觀念 - scheduler](demo_scheduler)\n\n8. [odoo 觀念 - sequence](demo_sequence)\n\n9. [odoo 觀念 - activity](demo_activity)\n\n10. [odoo 觀念 - TransientModel-Wizard](demo_odoo_tutorial_wizard)\n\n11. [odoo 觀念 - AbstractModel](demo_abstractmodel_tutorial)\n\n12. [odoo 觀念 - 實作 config settings](demo_config_settings)\n\n13. [odoo 觀念 - datetime 教學](demo_datetime_tutorial)\n\n14. [odoo 觀念 - 實作 scan barcode](demo_sale_scan_barcode)\n\n15. [odoo 觀念 - 實作 hierarchy](demo_hierarchy_tutorial)\n\n16. [odoo 觀念 - 如何使用 python xmlrpc 連接 odoo](xml-rpc-odoo)\n\n17. [odoo 觀念 - Translating 翻譯教學 i18n](demo_i18n_expense_tutorial)\n\n18. [odoo 觀念 - recruitment_website_form 介紹](demo_recruitment_website_form)\n\n19. [odoo 觀念 - 實作 init hook](demo_hook_tutorial)\n\n20. [odoo 教學 - 如何繼承 inherit controller](demo_inherit_controller)\n\n21. [odoo 教學 - fields_view_get 介紹教學](demo_fields_view_get_tutorial)\n\n22. [odoo 教學 - multi company](demo_multi_company)\n\n23. [odoo 教學 - testing 教學](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#odoo-testing-%E6%95%99%E5%AD%B8)\n\n24. [odoo 觀念 - orm cache 說明](demo_orm_cache)\n\n25. [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)\n\n26. [odoo 14 觀念 - image mixin 教學](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/14.0/demo_image_mixin)\n\n27. [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)\n\n28. [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)\n\n29. [odoo domain 教學](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/odoo_domain_tutorial)\n\n30. [odoo domain operator 教學](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/domain_operator_tutorial)\n\n31. [odoo index 教學](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/odoo_index_tutorial)\n\n32. [odoo 觀念 - odoo12 和 odoo14 的 ORM Write 差異](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/14.0/odoo_write_tutorial)\n\n33. [odoo 14 教學 - 透過 controller 建立簡單 api](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/14.0/demo_controller_json)\n\n34. [odoo 教學 - 透過 AbstractModel 擴充 Model](demo_abstractmodel_v2_tutorial)\n\n35. [odoo 教學 - odoo session_redis 教學](session_redis_tutorial)\n\n36. [Odoo 15 中的 LISTEN/NOTIFY 運作原理](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/15.0/odoo-pg-listen-notify)\n\n37. [Odoo 15 建立簡易 REST API](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/15.0/demo_controller_api)\n\n38. [odoo 18 OWL 範例 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/18.0/demo_owl_tutorial)\n\n## 其他\n\n* [Youtube Tutorial - 使用 CLI 安裝,更新 addons](https://youtu.be/k19N2x8f4gw)\n\n建立 addons 模組\n\n```cmd\n./odoo-bin scaffold your_addons_name my-addons/\n```\n\n在介紹如何透過 cli 安裝 addons 之前, 請先知道一件事情,\n\n就是你可以選擇將指令全部放到 cli 中, 或是在 `odoo.conf` 設定,\n\n像是如果有設定 `odoo.conf`\n\n```cmd\n[options]\n......\ndb_user = odoo\ndb_password = odoo\ndb_port = 5432\n```\n\n這樣我們直接執行以下指令即可\n\n```cmd\npython3 odoo-bin -d odoo -c /home/twtrubiks/work/odoo12/odoo/config/odoo.conf\n```\n\n如果你沒有設定 `odoo.conf` , 也可以在 cli 中設定\n\n```cmd\npython3 odoo-bin -r odoo -w odoo  -d odoo -c /home/twtrubiks/work/odoo12/odoo/config/odoo.conf\n```\n\n`-r` 代表 db_user. `-w` 代表 db_password. `-d` 代表指定 database.\n\n安裝 addons\n\n```cmd\npython3 odoo-bin -i addons_1 -d odoo\n```\n\n更新 addons\n\n```cmd\npython3 odoo-bin -u addons_1 -d odoo\n```\n\n也可以一次更新或安裝多個 addons\n\n```cmd\npython3 odoo-bin -u addons_1,addons_2 -d odoo\n```\n\n例外還有比較進階的用法 `--dev`\n\n```cmd\npython3 odoo-bin -u addons_1 -d odoo --dev=all\n```\n\n`--dev=all` 代表全部都啟用.\n\n`--dev=xml` 代表當 xml 改變的時候, 會自動幫你更新(不用手動更新).\n\n`--dev=reload` 代表當 python code 改變時, 自動更新(不用手動更新).\n\n但有時候如果你覺得怪怪的, 我還是建議手動重新直接更新 addons 的指令比較好 :smile:\n\n注意 :exclamation: 沒有刪除 addons 的指令, 只能從 web 上移除.\n\n### shell\n\n* [Youtube Tutorial - odoo shell 基本教學 - CRUD](https://youtu.be/kmbiT54hUkw)\n\n```cmd\npython odoo-bin shell -w odoo -r odoo -d odoo --db_port=5432 --db_host=localhost --addons-path='/home/twtrubiks/odoo/addons'\n```\n\n如果有很多路徑請使用 `,` 隔開\n\n```cmd\n--addons-path='/home/twtrubiks/odoo/addons,/home/twtrubiks/odoo/addons2'\n```\n\n`search`\n\n```python\n>>> self.env['res.partner'].search([])\nres.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)\n>>> self.env['res.partner'].search([('name', 'like', 'kim')])\nres.partner(24,)\n>>> self.env['res.partner'].browse([11, 20])\nres.partner(11, 20)\n```\n\n* [Youtube Tutorial - odoo shell orm 基本教學 - search_read](https://youtu.be/AzGnFX4pHWI)\n\n`search_read`\n\n通常比較常使用在 js 呼叫 odoo 或是第三方呼叫 odoo api,\n\n```python\n>>> self.env['hr.expense'].search_read([], ['id', 'employee_id'])\n[{'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')}]\n\n>>> self.env['hr.expense'].search_read([('employee_id', '=', 1)], ['id', 'name', 'employee_id'])\n[{'id': 2, 'name': 'Hotel Expenses', 'employee_id': (1, 'Mitchell Admin')}, {'id': 1, 'name': 'Travel by Air', 'employee_id': (1, 'Mitchell Admin')}]\n```\n\n* [Youtube Tutorial - odoo orm group 基本教學 - read_group](https://youtu.be/ALq6CcADygs)\n\n`read_group`\n\n通常使用在 SQL 中的 GROUP BY (很適合拿來處理比較大的資料, 效能應該也會比較好 :smile: ).\n\nread_group 的定義可參考原始碼中的 `odoo/models.py`\n\n```python\n......\n@api.model\ndef read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):\n    \"\"\"\n    Get the list of records in list view grouped by the given ``groupby`` fields\n\n    :param domain: list specifying search criteria [['field_name', 'operator', 'value'], ...]\n    :param list fields: list of fields present in the list view specified on the object.\n            Each element is either 'field' (field name, using the default aggregation),\n            or 'field:agg' (aggregate field with aggregation function 'agg'),\n            or 'name:agg(field)' (aggregate field with 'agg' and return it as 'name').\n            The possible aggregation functions are the ones provided by PostgreSQL\n            (https://www.postgresql.org/docs/current/static/functions-aggregate.html)\n            and 'count_distinct', with the expected meaning.\n    :param list groupby: list of groupby descriptions by which the records will be grouped.\n            A groupby description is either a field (then it will be grouped by that field)\n            or a string 'field:groupby_function'.  Right now, the only functions supported\n            are 'day', 'week', 'month', 'quarter' or 'year', and they only make sense for\n            date/datetime fields.\n......\n```\n\n比較特別要注意的地方是 fields, groupby, lazy 這幾個欄位 (請參考註解說明 :smile: ).\n\n如果你想參考寫法, 建議參考 odoo14 的, odoo12 也可以使用, 但是有些寫法比較舊了.\n\n這邊使用 `sale.order` 當作範例,\n\n假設想要得到每個 partner_id 的平均 amount_total,\n\n```python\nself.env['sale.order'].read_group([], ['partner_id', 'amount_total:avg'], ['partner_id'])\n```\n\n![alt tag](https://i.imgur.com/6eyegIE.png)\n\n同等如下 SQL\n\n```sql\nSELECT partner_id, avg(amount_total)\nFROM sale_order\nGROUP BY partner_id;\n```\n\n注意 :exclamation: :exclamation: 這邊 field 的格式為 `field:agg`.\n\nagg 代表 aggregate, odoo 的 orm 是有支援的, 更多詳細可參考 [postgresql functions-aggregate](https://www.postgresql.org/docs/current/functions-aggregate.html).\n\n假設想要得到每個 partner_id 的平均 amount_total 以及 總和 amount_total,\n\n```python\nself.env['sale.order'].read_group([], ['partner_id', 'total:sum(amount_total)', 'avg_total:avg(amount_total)'], ['partner_id'])\n```\n\n![alt tag](https://i.imgur.com/BhNR227.png)\n\n同等如下 SQL\n\n```sql\nSELECT partner_id, avg(amount_total), sum(amount_total)\nFROM sale_order\nGROUP BY partner_id;\n```\n\n注意 :exclamation: :exclamation:這邊的 fields 的格式為 `name:agg(field)`\n\n(因為是相同的 fields 名稱, 如果使用前一種寫法會錯誤)\n\n如果想要分的更細, 甚至可以再加上 fields, 這邊增加一個狀態\n\n```python\nself.env['sale.order'].read_group([], ['partner_id', 'total:sum(amount_total)', 'avg_total:avg(amount_total)'], ['partner_id', 'state'], lazy=False)\n```\n\n![alt tag](https://i.imgur.com/IaaFXae.png)\n\n同等如下 SQL\n\n```sql\nSELECT partner_id, state, avg(amount_total), sum(amount_total)\nFROM sale_order\nGROUP BY partner_id, state;\n```\n\n`lazy` 這個參數預設為 True, 也就代表只會拿第一個 field 下去分組,\n\n如果設定為 False, 就會把全部你所指定的 fields 都拿進去分組.\n\n根據 date_order 下去分組\n\n```python\nself.env['sale.order'].read_group([], ['total:sum(amount_total)'], ['date_order:month'])\n```\n\n同等如下 SQL\n\n```sql\nSELECT  DATE_TRUNC('month', date_order),\n\t\tsum(amount_total)\nFROM sale_order\nGROUP BY DATE_TRUNC('month', date_order);\n```\n\n`day` `week` `month` `quarter` `year` 這些都是可用的參數.\n\n![alt tag](https://i.imgur.com/cp1zX6P.png)\n\n`search_count`\n\n```python\n>>> self.env['res.partner'].search_count([])\n73\n```\n\n`recordset.ids` 回傳 recordset 全部的 id\n\n```python\n>>> recordset = self.env['res.partner'].search([])\n>>> recordset.ids\n[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]\n```\n\n繼續使用上面的範例\n\n`recordset.filtered(func)` 和 python 中的 [filter](https://github.com/twtrubiks/python-notes/blob/master/filter.py) 類似\n\n```python\n>>> recordset.filtered(lambda r: r.name.startswith('C'))\nres.partner(33, 39)\n```\n\n`recordset.mapped(func)` 和 python 中的 [map](https://github.com/twtrubiks/python-notes/blob/master/map_tutorial.py) 類似\n\n```python\n>>> recordset.mapped('name')\n['Azure Interior', 'Brandon Freeman', 'Colleen Diaz', 'Nicole Ford', 'Deco Addict', 'Addison Olson', 'Douglas Fletcher', 'Floyd Steward',...\n```\n\n`recordset.sorted(func)` 和 python 中的 [sorted](https://github.com/twtrubiks/python-notes/blob/master/sorted.py) 類似\n\n```python\n>>> recordset.sorted(key=lambda r: r.id, reverse=True)\nres.partner(40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, ...\n```\n\n`create`\n\n```python\n>>> partner = self.env['res.partner']\n>>> partner.create({'name': 'twtrubiks', 'is_company': True})\nres.partner(66)\n>>> self.env.cr.commit() # 需要特別執行這行才會寫進資料庫中\n```\n\n`write`\n\nupdate data\n\n```python\n>>> partner = self.env['res.partner'].browse([2])\n>>> partner\nres.partner(2,)\n>>> partner.name\n'OdooBot'\n>>> partner.write({'name': 'hello'})\nTrue\n>>> partner.name\n'hello'\n>>> self.env.cr.commit() # 需要特別執行這行才會寫進資料庫中\n```\n\n當你更新 `One2many` 和 `Many2many` 時, 要使用比較特別的語言,\n\n我之後會補充上來.\n\n`copy`\n\n如果 fields 有定義 `copy=False`, 就沒有辦法複製.\n\n```python\n# odoo/addons/base/data/res_users_demo.xml\n>>> demo = self.env.ref('base.user_demo')\n>>> demo.copy({'name': 'twtrubiks', 'login': 'twtrubiks', 'email':''})\n>>> self.env.cr.commit() # 需要特別執行這行才會寫進資料庫中\n```\n\n`delete`\n\n```python\n>>> user = self.env['res.users'].browse([3])\n>>> user.unlink()\n2020-06-21 06:45:51,958 19735 INFO odoo odoo.models.unlink: User #1 deleted ir.model.data records with IDs: [1884]\n2020-06-21 06:45:51,996 19735 INFO odoo odoo.models.unlink: User #1 deleted res.users records with IDs: [3]\nTrue\n>>> self.env.cr.commit() # 需要特別執行這行才會寫進資料庫中\n```\n\n`sudo`\n\n* [Youtube Tutorial - odoo 基本教學 - sudo](https://youtu.be/nAmNmPCSbGg)\n\n可參考 odoo 原始碼的 `odoo/models.py`\n\n```python\ndef sudo(self, user=SUPERUSER_ID):\n    \"\"\" sudo([user=SUPERUSER])\n\n    Returns a new version of this recordset attached to the provided\n    user.\n\n    By default this returns a ``SUPERUSER`` recordset, where access\n    control and record rules are bypassed.\n\n    .. note::\n\n        Using ``sudo`` could cause data access to cross the\n        boundaries of record rules, possibly mixing records that\n        are meant to be isolated (e.g. records from different\n        companies in multi-company environments).\n\n        It may lead to un-intuitive results in methods which select one\n        record among many - for example getting the default company, or\n        selecting a Bill of Materials.\n\n    .. note::\n\n        Because the record rules and access control will have to be\n        re-evaluated, the new recordset will not benefit from the current\n        environment's data cache, so later data access may incur extra\n        delays while re-fetching from the database.\n        The returned recordset has the same prefetch object as ``self``.\n\n    \"\"\"\n    return self.with_env(self.env(user=user))\n```\n\n`sudo([user=SUPERUSER])` 如果裡面沒有填入 user id, 預設就是使用 SUPERUSER, 如果\n\n有帶入 user id, 就是使用指定的 user 的權限.\n\n注意 :exclamation: 這是 odoo12 的作法,\n\n從 odoo13 開始切換 user 已經改成 `with_user(user)` :exclamation: 可參考 [odoo13-souece code](https://github.com/odoo/odoo/blob/13.0/odoo/models.py#L5160)\n\n`deprecated use of sudo(user), use with_user(user) instead`\n\n來看下面這個例子,\n\n因為沒有指定 user id, 所以是使用 SUPERUSER, 自然可以看到全部的 records,\n\n```python\n>>> self.env['hr.expense'].sudo().search([])\nhr.expense(4, 3, 2, 1)\n```\n\n再來看這個例子, user_id = 6 只能看到自己的 records, 因為他是一般的 user,\n\n```python\n>>> self.env['hr.expense'].sudo(user=6).search([])\nhr.expense(4, 3)\n```\n\n也就是說, 知道這個特性, 我們甚至可以讓沒有權限的人看到 records (請依照自己的需求去調整) :smile:\n\n另外提醒一下, 這個 sudo 除了在 ORM 底下生效外, 在 QWeb 中也會生效, 如下方這段 code 是可行的,\n\n```xml\n......\n<tr>\n    <td><strong>Email</strong></td>\n    <td><span t-field=\"o.sudo().employee_id.identification_id\"/></td>\n</tr>\n......\n```\n\n`with_context`\n\n可參考 odoo 原始碼的 `odoo/models.py`\n\n```python\ndef with_context(self, *args, **kwargs):\n    \"\"\" with_context([context][, **overrides]) -> records\n\n    Returns a new version of this recordset attached to an extended\n    context.\n\n    The extended context is either the provided ``context`` in which\n    ``overrides`` are merged or the *current* context in which\n    ``overrides`` are merged e.g.::\n\n        # current context is {'key1': True}\n        r2 = records.with_context({}, key2=True)\n        # -> r2._context is {'key2': True}\n        r2 = records.with_context(key2=True)\n        # -> r2._context is {'key1': True, 'key2': True}\n\n    .. note:\n\n        The returned recordset has the same prefetch object as ``self``.\n    \"\"\"\n    context = dict(args[0] if args else self._context, **kwargs)\n    return self.with_env(self.env(context=context))\n```\n\n`with_context` 可以用在很多地方, 這邊用一個翻譯的舉例, 如果我同時有 `en_US` 和 `zh_TW`\n\n這兩個語言, 可以使用 `with_context`帶入不同的語言, 會自動依照語言進行翻譯,\n\n```python\n>>> self.env['product.product'].with_context(lang='zh_TW').browse(41).name\n'飛機票'\n>>> self.env['product.product'].with_context(lang='en_US').browse(41).name\n'Air Flight'\n```\n\n`with_context` 也常使用在傳值中, 可參考 [odoo 觀念-TransientModel-Wizard](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial_wizard)\n\n### odoo shell 注意事項\n\n* [Youtube Tutorial - odoo shell 教學 - 注意事項](https://youtu.be/YS6mGE3-y1k)\n\nodoo-shell 下 command 無法 save 問題,\n\n當使用 Odoo Shell 測試資料時, 會發現當我們下了指令時, db 裡面的值沒有改變,\n\n這時候必須另外執行\n\n```cmd\nself.env.cr.commit()\n```\n\n在非 Odoo Shell 中會自動執行, 在 Odoo Shell 中不會自動執行 (需要手動執行).\n\n除非你不想要把修改資料寫進去資料庫.\n\n## odoo log 說明\n\n```log\n2019-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}\n```\n\nquery_count = query 次數\n\nquery_time = query 時間\n\nremaining_time = 剩餘時間\n\n如何透過 elk 搭配 odoo, 請參考 [docker-elk-tutorial 7.6.0](https://github.com/twtrubiks/docker-elk-tutorial/tree/elk-7.6.0)\n\n## odoo 使用 gmail 發信\n\n* [Youtube Tutorial - odoo 教學 - 使用 gmail 發信](https://youtu.be/CkFHCQuzEoo)\n\ngmail 需要一些前製作業, 建議先閱讀 [使用 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 設定\n\nTechnical -> Email -> Outgoing Mail Servers\n\n![alt tag](https://i.imgur.com/mZpaHWu.png)\n\nSMTP Server\t填入 `smtp.gmail.com`\n\nSMTP Port 填入 `465`\n\nConnection Security 填入 `SSL/TLS`\n\n填入自己的 Username 和 Password\n\n![alt tag](https://i.imgur.com/V77o0hY.png)\n\n建議輸入資料後, 可以先點選測試連接 (以下是成功的畫面)\n\n![alt tag](https://i.imgur.com/rIXcdnH.png)\n\n如果出現錯誤, 請確認你的帳密是否有錯誤\n\n![alt tag](https://i.imgur.com/yMVWVF5.png)\n\n接著可以使用 odoo 內的 email 測試看是否可以成功發信\n\n![alt tag](https://i.imgur.com/sy1A69K.png)\n\n成功發信\n\n![alt tag](https://i.imgur.com/CvMuelM.png)\n\n## 如何全域修改時間日期格式\n\n路徑為 Translations -> Languages, 點選語言, 就會看到以下的畫面,\n\n圖片下方有一些參數的說明(可自行依照需求調整)\n\n![alt tag](https://i.imgur.com/Z66LDIC.png)\n\n## 其他注意事項\n\n`odoo.conf` 中的 `data_dir` 參數建立好了就不要亂改,\n\n因為亂改動可能會導致你的 odoo 打開時一片空白或是破圖的狀況.\n\n```conf\n[options]\n......\ndata_dir = /home/twtrubiks/work/odoo12/odoo-data\n```\n\n另外如果你的 odoo 不知道甚麼原因導致破圖(非上述的狀況),\n\n錯誤訊息通常可能是遺失 filestore, 這時候可以嘗試以下的幾個方法,\n\n可以試試看更新 odoo 中的 `base`,\n\n或是從 db 中刪除 `ir_attachment` table,\n\n重新使用 debug mode 中的 Regenerate Assets Bundles.\n\n(assets 這個的功能是刪除舊的 css 和 js, 然後重新產生新的, 有時遇到 assets 快取的問題, 可以選這個選項)\n\n![alt tag](https://i.imgur.com/EJTK0KY.png)\n\n## 建議使用繼承 addons 的方式修改 odoo\n\n[Youtube Tutorial - odoo 教學 - 建議使用繼承 addons 的方式修改 odoo](https://youtu.be/Yncbx95YT1Q)\n\n這邊提醒大家, 建議在修改 odoo 的時候, 儘量使用 addons 繼承的方式去修改 code,\n\n原因是維護性的問題, 原生的 code 保持乾淨,\n\n雖然用 odoo developer mode 可以很快的修改 view,\n\n但是 :exclamation: :exclamation:\n\n只要你一更新你修改的那個 addons, 就會自動還原 :exclamation: :exclamation:\n\n這邊使用 `hr_expense` 舉的例子,\n\n我透過 Edit View 修改了 view,\n\n![alt tag](https://i.imgur.com/M6goe84.png)\n\n當你保存是會生效的.\n\n可是當你去更新 `hr_expense` 的時候, 你會發生他被還原了.\n\n所以, 使用 Edit View 選項去修改 view 可以使用在測試時.\n\n正式的修改, 還是推薦使用 addons 繼承的方式 :smile:\n\n## Donation\n\n文章都是我自己研究內化後原創，如果有幫助到您，也想鼓勵我的話，歡迎請我喝一杯咖啡 :laughing:\n\n綠界科技ECPAY ( 不需註冊會員 )\n\n![alt tag](https://payment.ecpay.com.tw/Upload/QRCode/201906/QRCode_672351b8-5ab3-42dd-9c7c-c24c3e6a10a0.png)\n\n[贊助者付款](http://bit.ly/2F7Jrha)\n\n歐付寶 ( 需註冊會員 )\n\n![alt tag](https://i.imgur.com/LRct9xa.png)\n\n[贊助者付款](https://payment.opay.tw/Broadcaster/Donate/9E47FDEF85ABE383A0F5FC6A218606F8)\n\n## 贊助名單\n\n[贊助名單](https://github.com/twtrubiks/Thank-you-for-donate)\n\n## License\n\nMIT license\n"
  },
  {
    "path": "demo_abstractmodel_tutorial/README.md",
    "content": "# 介紹 AbstractModel\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - AbstractModel](https://youtu.be/jsMTVe12vRY)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n主要介紹 demo_abstractmodel_tutorial\n\n## 說明\n\n`AbstractModel` AbstractModel = BaseModel,\n\n注意 :exclamation: :exclamation: AbstractModel **不會** 在資料庫中產生對應的 table.\n\nAbstractModel 除了常常使用在之前介紹的 [demo_prototype_inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_prototype_inheritance) 中,\n\n也很常使用在 report 中 (自定義一些額外的邏輯),\n\n可參考 odoo code 中的 `addons/sale/report/sale_report.py`,\n\n```python\n......\nclass SaleOrderReportProforma(models.AbstractModel):\n    _name = 'report.sale.report_saleproforma'\n    _description = 'Proforma Report'\n\n    def _get_report_values(self, docids, data=None):\n        docs = self.env['sale.order'].browse(docids)\n        return {\n            'doc_ids': docs.ids,\n            'doc_model': 'sale.order',\n            'docs': docs,\n            'proforma': True\n        }\n```\n\n假如你的 report 有額外的邏輯, 可以將邏輯寫在 `_get_report_values` 中.\n\n請一定要先了解 Transient Model, 如果不了解可參考 [demo_odoo_tutorial_wizard](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial_wizard).\n\n開始今天的介紹 :smile:\n\n先來看 [wizard/model_wizard.py](wizard/model_wizard.py)\n\n```python\nclass ReportWizard(models.TransientModel):\n    _name = 'report.wizard'\n    _description = \"Report Wizard\"\n\n    date_start = fields.Date(string=\"Start Date\", required=True, default=fields.Date.today)\n    date_end = fields.Date(string=\"End Date\", required=True, default=fields.Date.today)\n\n    @api.multi\n    def download_report(self):\n\n        _logger.warning('=== CALL get_report ===')\n\n        data = {\n            'ids': self.ids,\n            'model': self._name,\n            'form': {\n                'date_start': self.date_start,\n                'date_end': self.date_end,\n            },\n        }\n        return self.env.ref('demo_abstractmodel_tutorial.action_report_abstractmodel').report_action(self, data=data)\n......\n```\n\n頁面上會有一個按鈕觸發 `download_report`,\n\n`demo_abstractmodel_tutorial.action_report_abstractmodel` 為\n\naddons name + report id (report id 後面會說明) `addons_name.report_id`\n\n`report_action()` 會去 call `_get_report_values()`.\n\n[wizard/model_wizard.xml](wizard/model_wizard.xml)\n\n```xml\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n    <record model=\"ir.ui.view\" id=\"custom_report_wizard\">\n    <field name=\"name\">Custom Report</field>\n    <field name=\"model\">report.wizard</field>\n    <field name=\"type\">form</field>\n    <field name=\"arch\" type=\"xml\">\n        <form string=\"Custom Report\">\n            <group>\n                <group>\n                    <field name=\"date_start\"/>\n                </group>\n                <group>\n                    <field name=\"date_end\"/>\n                </group>\n            </group>\n            <footer>\n                <button name=\"download_report\" string=\"Download Report\" type=\"object\" class=\"oe_highlight\"/>\n                <button string=\"Cancel\" special=\"cancel\"/>\n            </footer>\n        </form>\n    </field>\n    </record>\n\n    <act_window id=\"action_custom_report_wizard\"\n                name=\"Action Custom Report\"\n                res_model=\"report.wizard\"\n                view_mode=\"form\"\n                target=\"new\"/>\n\n    <menuitem action=\"action_custom_report_wizard\"\n              id=\"menu_custom_report_wizard\"\n              parent=\"hr_expense.menu_hr_expense_reports\"/>\n\n</odoo>\n```\n\n這邊定義了基本的 form, 並且將 menu 設定在 `hr_expense.menu_hr_expense_reports` 之下.\n\n![alt tag](https://i.imgur.com/BL4en9D.png)\n\n![alt tag](https://i.imgur.com/VnuJXrI.png)\n\n剛剛前面提到 report id `action_report_abstractmodel` 在 [reports/report.xml](reports/report.xml)\n\n```xml\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n    <template id=\"report_wizard_template\">\n        <t t-call=\"web.html_container\">\n            <div class=\"header\">\n                <h3 class=\"text-center\">Expense Wizard Report</h3>\n                <h4 class=\"text-center\">\n                    <strong>From</strong>:\n                    <t t-esc=\"date_start\"/>\n                    <strong>To</strong>:\n                    <t t-esc=\"date_end\"/>\n                </h4>\n            </div>\n            <div>\n                <table>\n                    <thead>\n                        <th class=\"text-center\">Name</th>\n                        <th class=\"text-center\">Date</th>\n                        <th class=\"text-center\">Unit_amount</th>\n                    </thead>\n                    <tbody>\n                        <t t-foreach=\"docs\" t-as=\"doc\">\n                            <tr>\n                                <td>\n                                    <span t-esc=\"doc['name']\"/>\n                                </td>\n                                <td class=\"text-center\">\n                                    <span t-esc=\"doc['date']\"/>\n                                </td>\n                                <td class=\"text-center\">\n                                    <span t-esc=\"doc['unit_amount']\"/>\n                                </td>\n                            </tr>\n                        </t>\n                    </tbody>\n                </table>\n            </div>\n        </t>\n    </template>\n\n    <report\n        id=\"action_report_abstractmodel\"\n        string=\"Demo Report\"\n        model=\"report.wizard\"\n        report_type=\"qweb-pdf\"\n        name=\"demo_abstractmodel_tutorial.report_wizard_template\"\n        print_report_name=\"Demo Report\"\n    />\n\n</odoo>\n```\n\n分別設定了 template id `report_wizard_template` 以及 report id `action_report_abstractmodel`.\n\n`name` 的部份為 adddons name + template id.\n\n也就是 `adddons_name.template_id`.\n\n接著看 [wizard/model_wizard.py](wizard/model_wizard.py) 的後半段,\n\n```python\n......\nclass ReportExpenseAbstractModel(models.AbstractModel):\n    _name = 'report.demo_abstractmodel_tutorial.report_wizard_template'\n    _description = 'Report Expense Wizard'\n\n    @api.model\n    def _get_report_values(self, docids, data=None):\n        _logger.warning('=== CALL get_report_values ===')\n\n        date_start = data['form']['date_start']\n        date_end = data['form']['date_end']\n        docs = self.env['hr.expense'].search([\n            ('date', '>=', date_start),\n            ('date', '<=', date_end)], order='date asc')\n        return {\n            'doc_ids': data['ids'],\n            'doc_model': data['model'],\n            'date_start': date_start,\n            'date_end': date_end,\n            'docs': docs,\n        }\n\n```\n\n前面說過了 `report_action()` 會去 call `_get_report_values()`,\n\n所以這邊定義了 `AbstractModel` 並且實作 `_get_report_values`.\n\n`_name` 這邊的比較特別, 要注意一下, 它的結構是由以下幾部份組成,\n\nreport + addons name + template id\n\n`report.addons_name.template_id`\n\n也就是 `report.demo_abstractmodel_tutorial.report_wizard_template`,\n\nreport 這個 prefix 很重要, 請不要任意的拿掉 :exclamation: :exclamation:\n\n`_get_report_values` 則是我們額外的邏輯, 最後將資料回傳給\n\n`report_wizard_template` 並且 render.\n\n![alt tag](https://i.imgur.com/XqmRovl.png)"
  },
  {
    "path": "demo_abstractmodel_tutorial/__init__.py",
    "content": "from . import wizard"
  },
  {
    "path": "demo_abstractmodel_tutorial/__manifest__.py",
    "content": "{\n    'name': \"demo_abstractmodel_tutorial\",\n    'summary': \"\"\"\n        AbstractModel report\n    \"\"\",\n    'description': \"\"\"\n        AbstractModel report\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['hr_expense'],\n\n    # always loaded\n    'data': [\n        'wizard/model_wizard.xml',\n        'reports/report.xml'\n    ],\n    'application': True,\n}\n"
  },
  {
    "path": "demo_abstractmodel_tutorial/reports/report.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n\n    <template id=\"report_wizard_template\">\n        <t t-call=\"web.html_container\">\n            <div class=\"header\">\n                <h3 class=\"text-center\">Expense Wizard Report</h3>\n                <h4 class=\"text-center\">\n                    <strong>From</strong>:\n                    <t t-esc=\"date_start\"/>\n                    <strong>To</strong>:\n                    <t t-esc=\"date_end\"/>\n                </h4>\n            </div>\n            <div>\n                <table>\n                    <thead>\n                        <th class=\"text-center\">Name</th>\n                        <th class=\"text-center\">Date</th>\n                        <th class=\"text-center\">Unit_amount</th>\n                    </thead>\n                    <tbody>\n                        <t t-foreach=\"docs\" t-as=\"doc\">\n                            <tr>\n                                <td>\n                                    <span t-esc=\"doc['name']\"/>\n                                </td>\n                                <td class=\"text-center\">\n                                    <span t-esc=\"doc['date']\"/>\n                                </td>\n                                <td class=\"text-center\">\n                                    <span t-esc=\"doc['unit_amount']\"/>\n                                </td>\n                            </tr>\n                        </t>\n                    </tbody>\n                </table>\n            </div>\n        </t>\n    </template>\n\n    <report\n        id=\"action_report_abstractmodel\"\n        string=\"Demo Report\"\n        model=\"report.wizard\"\n        report_type=\"qweb-pdf\"\n        name=\"demo_abstractmodel_tutorial.report_wizard_template\"\n        print_report_name=\"Demo Report\"\n    />\n\n</odoo>\n"
  },
  {
    "path": "demo_abstractmodel_tutorial/wizard/__init__.py",
    "content": "from . import model_wizard\n"
  },
  {
    "path": "demo_abstractmodel_tutorial/wizard/model_wizard.py",
    "content": "from odoo import api, fields, models\nimport logging\n\n_logger = logging.getLogger(__name__)\n\nclass ReportWizard(models.TransientModel):\n    _name = 'report.wizard'\n    _description = \"Report Wizard\"\n\n    date_start = fields.Date(string=\"Start Date\", required=True, default=fields.Date.today)\n    date_end = fields.Date(string=\"End Date\", required=True, default=fields.Date.today)\n\n    @api.multi\n    def download_report(self):\n\n        _logger.warning('=== CALL get_report ===')\n\n        data = {\n            'ids': self.ids,\n            'model': self._name,\n            'form': {\n                'date_start': self.date_start,\n                'date_end': self.date_end,\n            },\n        }\n        return self.env.ref('demo_abstractmodel_tutorial.action_report_abstractmodel').report_action(self, data=data)\n\nclass ReportExpenseAbstractModel(models.AbstractModel):\n    _name = 'report.demo_abstractmodel_tutorial.report_wizard_template'\n    _description = 'Report Expense Wizard'\n\n    @api.model\n    def _get_report_values(self, docids, data=None):\n        _logger.warning('=== CALL get_report_values ===')\n\n        date_start = data['form']['date_start']\n        date_end = data['form']['date_end']\n        docs = self.env['hr.expense'].search([\n            ('date', '>=', date_start),\n            ('date', '<=', date_end)], order='date asc')\n        return {\n            'doc_ids': data['ids'],\n            'doc_model': data['model'],\n            'date_start': date_start,\n            'date_end': date_end,\n            'docs': docs,\n        }\n"
  },
  {
    "path": "demo_abstractmodel_tutorial/wizard/model_wizard.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n    <record model=\"ir.ui.view\" id=\"custom_report_wizard\">\n    <field name=\"name\">Custom Report</field>\n    <field name=\"model\">report.wizard</field>\n    <field name=\"type\">form</field>\n    <field name=\"arch\" type=\"xml\">\n        <form string=\"Custom Report\">\n            <group>\n                <group>\n                    <field name=\"date_start\"/>\n                </group>\n                <group>\n                    <field name=\"date_end\"/>\n                </group>\n            </group>\n            <footer>\n                <button name=\"download_report\" string=\"Download Report\" type=\"object\" class=\"oe_highlight\"/>\n                <button string=\"Cancel\" special=\"cancel\"/>\n            </footer>\n        </form>\n    </field>\n    </record>\n\n    <act_window id=\"action_custom_report_wizard\"\n                name=\"Action Custom Report\"\n                res_model=\"report.wizard\"\n                view_mode=\"form\"\n                target=\"new\"/>\n\n    <menuitem action=\"action_custom_report_wizard\"\n              id=\"menu_custom_report_wizard\"\n              parent=\"hr_expense.menu_hr_expense_reports\"/>\n\n</odoo>\n"
  },
  {
    "path": "demo_abstractmodel_v2_tutorial/README.md",
    "content": "# 透過 AbstractModel 擴充 Model\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - 透過 AbstractModel 擴充 Model](https://youtu.be/uW1PsDPcJF4)\n\n之前有介紹過 AbstractModel 的文章\n\n* [介紹 AbstractModel](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_abstractmodel_tutorial) 搭配 report 使用\n\n* [odoo 繼承 - prototype inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_prototype_inheritance) 有提到 MailThread 這個 AbstractModel\n\n今天要來進一步介紹, 如何透過 AbstractModel 擴充 Model :smile:\n\n## 說明\n\n首先, 再提一次\n\n`AbstractModel` AbstractModel = BaseModel,\n\n注意 :exclamation: :exclamation: AbstractModel **不會** 在資料庫中產生對應的 table.\n\n先來看這個範例 [models/models.py](models/models.py)\n\n```python\nfrom odoo import models, fields, api\n\nclass DemoMixin(models.AbstractModel):\n    _name = 'demo.mixin'\n    _description = 'Demo Mixin'\n\n    test_1 = fields.Float(\n        string='test_1',\n        default='2.2'\n    )\n\n    test_2 = fields.Float(\n        string=\"test_2\",\n        compute=\"_compute_field\",\n    )\n\n    def _compute_field(self):\n        for record in self:\n            record.test_2 = 3.0\n\nclass DemoModelTutorial(models.Model):\n    _name = 'demo.model.tutorial'\n    _inherit = 'demo.mixin'\n    _description = 'Demo Model Tutorial'\n\n    name = fields.Char(required=True, string=\"名稱\")\n\n```\n\n這邊 `demo.model.tutorial` 繼承了 `demo.mixin`, 所以在 db table 中,\n\n會看到 `demo.mixin` 中的 fields.\n\n![alt tag](https://i.imgur.com/0fYEUiS.png)\n\n這邊稍微注意注意一下,\n\n在 db 中只會有 `demo.model.tutorial` 的 table, 不會有 `demo.mixin` 的 table,\n\n但是會有 `demo.mixin` 中的 fields, 也看不到 `test_2` fields, 原因是他是 _compute_field,\n\n如果你想要看到包含 `test_2` fields, 可以到 odoo 的 model 後台觀看\n\n![alt tag](https://i.imgur.com/oiASNIP.png)\n\n`demo.mixin` 的 model 在 odoo 的後台也可以觀看 (但 db 中不會出現)\n\n![alt tag](https://i.imgur.com/HkftQT3.png)\n\n![alt tag](https://i.imgur.com/3ttRkzP.png)\n\n在 tree, form ...... 都可以使用 `demo.mixin` 的 fields\n\n![alt tag](https://i.imgur.com/hFCf2mR.png)\n\n因為這個範例剛好只有一個 model 被繼承, 如果有兩個以上的 model 就更適合這樣寫了, 如下\n\n```python\n......\n\nclass DemoModelTutorial(models.Model):\n    _name = 'demo.model.tutorial'\n    _inherit = 'demo.mixin'\n    _description = 'Demo Model Tutorial'\n\n    ......\n\nclass DemoModelTutorial_v2(models.Model):\n    _name = 'demo.model.tutorial.v2'\n    _inherit = 'demo.mixin'\n    _description = 'Demo Model Tutorial v2'\n\n    ......\n\nclass DemoModelTutorial_v3(models.Model):\n    _name = 'demo.model.tutorial.v3'\n    _inherit = 'demo.mixin'\n    _description = 'Demo Model Tutorial v3'\n\n    ......\n```\n\n這樣每個 model, 都會擁有 `demo.mixin` 的 fields, 不需要把重複的 code\n\n在每個 model 中都寫一遍.\n\n剛剛介紹的 model 是我們新建立的, 假如今天有一個 model 已經存在了,\n\n想要用同樣的方式擴充 model, 可參考 [models/models_v2.py](models/models_v2.py)\n\n```python\nfrom odoo import models, fields, api\n\nclass DemoMixin2(models.AbstractModel):\n    _name = 'demo.mixin2'\n    _description = 'Demo Mixin2'\n\n    test_v2 = fields.Float(\n        string='test_v2',\n        default='2.2'\n    )\n\nclass DemoModelTutorial(models.Model):\n    _name = 'demo.model.tutorial'\n    _inherit = ['demo.model.tutorial', 'demo.mixin2']\n\n    pass\n\n```\n\n這篇文章其實就是將 [odoo 實作 scan barcode](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_sale_scan_barcode) 的概念再說一次.\n\n也可以去了解一下 [什麼是 Mixin in python](https://github.com/twtrubiks/python-notes/tree/master/what_is_the_mixin),\n\n相信這樣大家會更了解他們的概念 :smile:"
  },
  {
    "path": "demo_abstractmodel_v2_tutorial/__init__.py",
    "content": "from . import models"
  },
  {
    "path": "demo_abstractmodel_v2_tutorial/__manifest__.py",
    "content": "{\n    'name': \"demo_abstractmodel_v2_tutorial\",\n    'summary': \"\"\"\n        AbstractModel Extend Model\n    \"\"\",\n    'description': \"\"\"\n        AbstractModel Extend Model\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['base'],\n\n    # always loaded\n    'data': [\n        'security/ir.model.access.csv',\n        'views/menu.xml',\n        'views/view.xml',\n    ],\n    'application': True,\n}"
  },
  {
    "path": "demo_abstractmodel_v2_tutorial/models/__init__.py",
    "content": "from . import models\n# from . import models_v2"
  },
  {
    "path": "demo_abstractmodel_v2_tutorial/models/models.py",
    "content": "from odoo import models, fields, api\n\nclass DemoMixin(models.AbstractModel):\n    _name = 'demo.mixin'\n    _description = 'Demo Mixin'\n\n    test_1 = fields.Float(\n        string='test_1',\n        default='2.2'\n    )\n\n    test_2 = fields.Float(\n        string=\"test_2\",\n        compute=\"_compute_field\",\n    )\n\n    def _compute_field(self):\n        for record in self:\n            record.test_2 = 3.0\n\nclass DemoModelTutorial(models.Model):\n    _name = 'demo.model.tutorial'\n    _inherit = 'demo.mixin'\n    _description = 'Demo Model Tutorial'\n\n    name = fields.Char(required=True, string=\"名稱\")\n"
  },
  {
    "path": "demo_abstractmodel_v2_tutorial/models/models_v2.py",
    "content": "from odoo import models, fields, api\n\nclass DemoMixin2(models.AbstractModel):\n    _name = 'demo.mixin2'\n    _description = 'Demo Mixin2'\n\n    test_v2 = fields.Float(\n        string='test_v2',\n        default='2.2'\n    )\n\nclass DemoModelTutorial(models.Model):\n    _name = 'demo.model.tutorial'\n    _inherit = ['demo.model.tutorial', 'demo.mixin2']\n\n    pass\n"
  },
  {
    "path": "demo_abstractmodel_v2_tutorial/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_model_tutorial_user,Demo Model Tutorial Access,model_demo_model_tutorial,,1,1,1,1\n"
  },
  {
    "path": "demo_abstractmodel_v2_tutorial/views/menu.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <!-- demo_model_tutorial App Menu -->\n    <menuitem id=\"demo_model_tutorial_menu\"\n              name=\"Demo Model Tutorial\" />\n\n    <!-- Action to open the demo_model_tutorial -->\n    <act_window id=\"action_demo_model_tutorial\"\n                name=\"Demo Model Tutorial Action\"\n                res_model=\"demo.model.tutorial\"\n                view_mode=\"tree,form\"/>\n\n    <!-- Menu item to open the demo_model_tutorial -->\n    <menuitem id=\"menu_demo_model_tutorial\"\n              name=\"Demo Model Tutorial\"\n\t          action=\"action_demo_model_tutorial\"\n              parent=\"demo_model_tutorial_menu\" />\n\n</odoo>\n\n\n"
  },
  {
    "path": "demo_abstractmodel_v2_tutorial/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n  <record id=\"view_form_demo_model_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Model Tutorial Form</field>\n    <field name=\"model\">demo.model.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Expense Tutorial\">\n        <sheet>\n          <group>\n            <field name=\"name\"/>\n            <field name=\"test_1\"/>\n            <field name=\"test_2\"/>\n          </group>\n        </sheet>\n      </form>\n    </field>\n  </record>\n\n  <record id=\"view_tree_demo_model_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Model Tutorial List</field>\n    <field name=\"model\">demo.model.tutorial</field>\n    <field name=\"priority\" eval=\"1\"/>\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n          <field name=\"name\"/>\n          <field name=\"test_1\"/>\n          <field name=\"test_2\"/>\n      </tree>\n    </field>\n  </record>\n</odoo>"
  },
  {
    "path": "demo_actions_singleton/README.md",
    "content": "# odoo 觀念 - actions 和 singleton\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - actions and singleton](https://youtu.be/rRD9j4IAHWY)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n本篇主要介紹 actions and singleton\n\n以下將介紹這個 addons 的結構\n\n## 說明\n\n[data/action_data.xml](data/action_data.xml)\n\n首先, 先來建立一個 action\n\n```xml\n......\n<odoo>\n    <data>\n        <record id=\"action_server_demo\" model=\"ir.actions.server\">\n            <field name=\"name\">Action Demo</field>\n            <field name=\"model_id\" ref=\"model_demo_actions_singleton\"/>\n            <field name=\"binding_model_id\" ref=\"demo_actions_singleton.model_demo_actions_singleton\"/>\n            <field name=\"state\">code</field>\n            <field name=\"code\">\n                records.action_demo()\n            </field>\n        </record>\n\n        <record id=\"action_other_model_demo\" model=\"ir.actions.server\">\n            <field name=\"name\">Action Other Demo</field>\n            <field name=\"model_id\" ref=\"model_demo_actions_singleton\"/>\n            <field name=\"binding_model_id\" ref=\"hr_expense.model_hr_expense\"/>\n            <field name=\"state\">code</field>\n            <field name=\"code\">\n                raise Warning('Hello')\n            </field>\n        </record>\n    </data>\n</odoo>\n```\n\n`binding_model_id` 綁定 model (可以綁定和 `model_id` 不同的 model).\n\n`state` 選擇使用的方式, 這邊使用 python code.\n\n`code` 執行的程式碼, `records` 代表所選的 record, `action_demo()` 代表呼叫的 function.\n\n這邊的設定也可以在 Technical -> Actions -> Server Actions 看到 (如下)\n\n![alt tag](https://i.imgur.com/RV5ryMj.png)\n\n這邊就是剛剛 code 的設定\n\n![alt tag](https://i.imgur.com/vfeSIxp.png)\n\n這邊的 Action To Do 是使用 Execute Python Code, 還有其他的選擇\n\n![alt tag](https://i.imgur.com/NqxJLzu.png)\n\n記得也要將它加入 `__manifest__.py`.\n\n先來看 [models/models.py](models/models.py)\n\n```python\n......\n\nclass DemoActionsSingleton(models.Model):\n    _name = 'demo.actions.singleton'\n    _description = 'Demo Actions Singleton'\n\n    name = fields.Char('Description', required=True)\n\n    @api.multi\n    def action_demo(self):\n        self.ensure_one()\n        _logger.warning('=== CALL action_demo ===')\n```\n\n`action_demo` 裡面就只是單純的 print.\n\n至於要在 odoo 中的那邊呼叫 Action Demo, 請看下圖,\n\n在 record 中的 action\n\n![alt tag](https://i.imgur.com/cf6NeMr.png)\n\n當你點下去, 會觸發你的 logger\n\n![alt tag](https://i.imgur.com/wbkWbDV.png)\n\n接下來說說 `self.ensure_one()`, 這就是確認是否為 `singleton`,\n\n假如跳出 `raise ValueError exception`, 代表它非為 singleton\n\n舉個例子, 像這邊選 兩條 record, 點下 Action Demo,\n\n![alt tag](https://i.imgur.com/HMV3CHS.png)\n\n你會發現跳出 error\n\n![alt tag](https://i.imgur.com/tpti9Lb.png)\n\n原因就是在這邊我們使用了 `self.ensure_one()` 確認 (確保只使用一條 record),\n\n所以選兩條 reocrd 就會錯誤 :exclamation: :exclamation:\n\n所以結論就是 `self.ensure_one()` 是要讓你檢查是否為 `singleton`.\n\n另外, 空的 recordset 行為也像是 singleton, 當你 accessing fields 時,\n\n它不會回傳 error ( 而是會回傳 `False`),\n\n也因為這個特性, 所以我們才可以使用 `.` 歷遍 (traverse) records 而不用擔心錯誤 :smile:\n\n(舉例, 下面的例子)\n\n```python\n>>> self.company_id.parent_id\nres.company()\n>>> self.company_id.parent_id.name\nFalse\n```\n\n並不會發生錯誤, 只會回傳 `False`.\n\n關於 `id=\"action_other_model_demo\"` 可以看到他綁定的 model 是 `hr_expense`\n\n`<field name=\"binding_model_id\" ref=\"hr_expense.model_hr_expense\"/>`\n\n所以, 要到 `hr_expense` 才可以看到這個 action\n\n![alt tag](https://i.imgur.com/zQMwqca.png)\n\n![alt tag](https://i.imgur.com/r5u0Voy.png)"
  },
  {
    "path": "demo_actions_singleton/__init__.py",
    "content": "from . import models"
  },
  {
    "path": "demo_actions_singleton/__manifest__.py",
    "content": "{\n    'name': \"demo actions singleton\",\n    'summary': \"\"\"\n        tutorial - actions and singleton\n    \"\"\",\n    'description': \"\"\"\n        tutorial - actions and singleton\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['base', 'hr_expense'],\n\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'data/action_data.xml',\n        'views/menu.xml',\n        'views/view.xml',\n    ],\n    'application': True,\n}\n"
  },
  {
    "path": "demo_actions_singleton/data/action_data.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n    <data>\n        <record id=\"action_server_demo\" model=\"ir.actions.server\">\n            <field name=\"name\">Action Demo</field>\n            <field name=\"model_id\" ref=\"model_demo_actions_singleton\"/>\n            <field name=\"binding_model_id\" ref=\"demo_actions_singleton.model_demo_actions_singleton\"/>\n            <field name=\"state\">code</field>\n            <field name=\"code\">\n                records.action_demo()\n            </field>\n        </record>\n\n        <record id=\"action_other_model_demo\" model=\"ir.actions.server\">\n            <field name=\"name\">Action Other Demo</field>\n            <field name=\"model_id\" ref=\"model_demo_actions_singleton\"/>\n            <field name=\"binding_model_id\" ref=\"hr_expense.model_hr_expense\"/>\n            <field name=\"state\">code</field>\n            <field name=\"code\">\n                raise Warning('Hello')\n            </field>\n        </record>\n    </data>\n</odoo>"
  },
  {
    "path": "demo_actions_singleton/models/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_actions_singleton/models/models.py",
    "content": "from odoo import models, fields, api\nimport logging\n\n_logger = logging.getLogger(__name__)\n\nclass DemoActionsSingleton(models.Model):\n    _name = 'demo.actions.singleton'\n    _description = 'Demo Actions Singleton'\n\n    name = fields.Char('Description', required=True)\n\n    @api.multi\n    def action_demo(self):\n        self.ensure_one()\n        _logger.warning('=== CALL action_demo ===')\n"
  },
  {
    "path": "demo_actions_singleton/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_singleton_user,Demo Actions Singleton User Access,model_demo_actions_singleton,demo_actions_singleton_group_user,1,1,1,0\naccess_demo_singleton_manager,Demo Actions Singleton Manager Access,model_demo_actions_singleton,demo_actions_singleton_group_manager,1,1,1,1\n\n"
  },
  {
    "path": "demo_actions_singleton/security/security.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n\n  <record id=\"module_demo_actions_singleton\" model=\"ir.module.category\">\n    <field name=\"name\">Demo actions singleton category</field>\n  </record>\n\n  <record id=\"demo_actions_singleton_group_user\" model=\"res.groups\">\n    <field name=\"name\">User</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_actions_singleton\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('base.group_user'))]\"/>\n  </record>\n\n  <record id=\"demo_actions_singleton_group_manager\" model=\"res.groups\">\n    <field name=\"name\">Manager</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_actions_singleton\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('demo_actions_singleton_group_user'))]\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_actions_singleton/views/menu.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <!-- demo_actions_singleton App Menu -->\n    <menuitem id=\"demo_actions_singleton_menu\"\n              name=\"Demo Actions Singleton\" />\n\n    <!-- Action to open the demo_actions_singleton -->\n    <act_window id=\"action_singleton\"\n                name=\"Demo Actions Singleton Action\"\n                res_model=\"demo.actions.singleton\"\n                view_mode=\"tree,form\"/>\n\n    <!-- Menu item to open the demo_actions_singleton -->\n    <menuitem id=\"menu_action_singleton\"\n              name=\"Demo Actions Singleton\"\n\t          action=\"action_singleton\"\n              parent=\"demo_actions_singleton_menu\" />\n\n</odoo>\n\n\n"
  },
  {
    "path": "demo_actions_singleton/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n  <record id=\"view_form_demo_actions_singleton\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Actions Singleton Form</field>\n    <field name=\"model\">demo.actions.singleton</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Actions Singleton\">\n        <sheet>\n          <group>\n            <field name=\"name\"/>\n          </group>\n        </sheet>\n      </form>\n    </field>\n  </record>\n\n  <record id=\"view_tree_demo_actions_singleton\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Actions Singleton List</field>\n    <field name=\"model\">demo.actions.singleton</field>\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n        <field name=\"name\"/>\n      </tree>\n    </field>\n  </record>\n</odoo>"
  },
  {
    "path": "demo_activity/README.md",
    "content": "# odoo 觀念 - activity\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - activity](https://youtu.be/_i4yLHrXRdg)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n本篇文章主要介紹 odoo 中的 activity 這部份\n\n## 說明\n\n在 odoo 中, 肯定會常常看到 activity, 也就是如下圖的地方\n\n![alt tag](https://i.imgur.com/AIlIG2b.png)\n\n因為要先定義一個 activity 的 data,  所以先來看 [data/mail_data.xml](data/mail_data.xml)\n\n```xml\n......\n<data noupdate=\"0\">\n  <record id=\"mail_act_approval\" model=\"mail.activity.type\">\n      <field name=\"name\">Activity Approval</field>\n      <field name=\"icon\">fa-dollar</field>\n      <field name=\"res_model_id\" ref=\"demo_activity.model_demo_activity\"/>\n  </record>\n</data>\n......\n```\n\n`name` 定義 activity 的名稱.\n\n`icon` 定義 icon.\n\n`res_model_id` 選擇對應的 model.\n\n這個 activity 的 record 也可以在 odoo 中找到,\n\n路徑為 Technical -> Email -> Activity Types\n\n![alt tag](https://i.imgur.com/K6mubdq.png)\n\n![alt tag](https://i.imgur.com/X98vjmh.png)\n\n也可以進去修改相關的設定\n\n![alt tag](https://i.imgur.com/xxToZSP.png)\n\n再來看 [models/models.py](models/models.py)\n\n```python\n......\nclass DemoActivity(models.Model):\n    _name = \"demo.activity\"\n    _description = \"Demo Activity\"\n    _inherit = ['mail.thread', 'mail.activity.mixin']\n\n    name = fields.Char(string='name', required=True)\n    employee_id = fields.Many2one(\n        'hr.employee', string=\"Employee\", required=True)\n\n    def button_activity_schedule(self):\n        self.activity_schedule(\n            'demo_activity.mail_act_approval',\n            user_id = self.sudo().employee_id.user_id.id,\n            note = 'my note',\n            summary = 'my summary')\n\n    def button_activity_feedback(self):\n        self.activity_feedback(\n            ['demo_activity.mail_act_approval'])\n\n    def button_activity_unlink(self):\n        self.activity_unlink(\n            ['demo_activity.mail_act_approval'])\n\n```\n\n注意 `_inherit = ['mail.thread', 'mail.activity.mixin']`\n\n這繼承是必須的哦, 不然你的 activity 是會失效的 :smile:\n\n這是所謂的 prototype inheritance,\n\n可參考之前的文章以及影片 [demo_prototype_inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_prototype_inheritance).\n\n最重要的就是這3個 function,\n\n分別展示 `activity_schedule` `activity_feedback` `activity_unlink`\n\n`activity_schedule`\n\n指定 activity_schedule 給特定的人\n\n```python\nself.activity_schedule(\n    'demo_activity.mail_act_approval',\n    user_id = self.sudo().employee_id.user_id.id,\n    note = 'my note',\n    summary = 'my summary')\n```\n\n`demo_activity.mail_act_approval` 代表 activity id.\n\n`user_id` 代表 user.\n\n`note` 代表 note.\n\n`summary` 代表 summary.\n\n當點選範例的 activity_schedule\n\n![alt tag](https://i.imgur.com/AD48O0S.png)\n\n底下會顯示 activity\n\n![alt tag](https://i.imgur.com/1af8U1V.png)\n\n狀態列也會顯示有一個 activity\n\n![alt tag](https://i.imgur.com/LYkQdkP.png)\n\n`activity_feedback`\n\n同意(done)這個 activity\n\n當點選範例的 activity_feedback\n\n![alt tag](https://i.imgur.com/NXdAALh.png)\n\n底下會顯示 activity 狀態\n\n![alt tag](https://i.imgur.com/OtNzxqC.png)\n\n`activity_unlink`\n\n取消 activity\n\n![alt tag](https://i.imgur.com/IEoHNhc.png)\n\n這功能和直接點選 Cancel 是一樣的 ( activity 會消失 )\n\n![alt tag](https://i.imgur.com/ZzCNX4p.png)\n\n也請記得設定 security\n\n[security/ir.model.access.csv](security/ir.model.access.csv)\n\n[security/security.xml](security/security.xml)\n\n來看 [views/view.xml](views/view.xml)\n\n```xml\n......\n    <record id=\"view_activity_form\" model=\"ir.ui.view\">\n          <field name=\"name\">demo.activity.form</field>\n          <field name=\"model\">demo.activity</field>\n          <field eval=\"25\" name=\"priority\"/>\n          <field name=\"arch\" type=\"xml\">\n              <form string=\"Demo Activity\">\n                <header>\n                    <button name=\"button_activity_schedule\" string=\"activity schedule\" type=\"object\" class=\"oe_highlight\"/>\n                    <button name=\"button_activity_unlink\" string=\"activity unlink\" type=\"object\" class=\"oe_highlight\"/>\n                    <button name=\"button_activity_feedback\" string=\"activity feedback\" type=\"object\" class=\"oe_highlight\"/>\n                </header>\n                <sheet>\n                    <group>\n                        <field name=\"name\"/>\n                        <field name=\"employee_id\"/>\n                    </group>\n                </sheet>\n\n                <div class=\"oe_chatter\">\n                    <field name=\"message_follower_ids\" widget=\"mail_followers\"/>\n                    <field name=\"activity_ids\" widget=\"mail_activity\"/>\n                    <field name=\"message_ids\" widget=\"mail_thread\"/>\n                </div>\n\n              </form>\n          </field>\n    </record>\n......\n```\n\n`<button name=\"button_activity_schedule\" string=\"activity schedule\" type=\"object\" class=\"oe_highlight\"/>`\n\n`name` 就是對應 model 中的 function 的名稱, 像這邊就是對應 `demo.activity` model 中的\n\n`button_activity_schedule` function.\n\n`string` 定義 button 的名稱.\n\n最後的這段之前也說過了,\n\n```xml\n<div class=\"oe_chatter\">\n    <field name=\"message_follower_ids\" widget=\"mail_followers\"/>\n    <field name=\"activity_ids\" widget=\"mail_activity\"/>\n    <field name=\"message_ids\" widget=\"mail_thread\"/>\n</div>\n```\n\n就是顯示下面的那段\n\n![alt tag](https://i.imgur.com/7L9wkDx.png)\n\n最後記得也要設定 `__manifest__.py` 哦 :smile:\n\n注意需要 depend `mail` :exclamation: :exclamation:\n\n```python\n......\n  {\n    ......\n    # any module necessary for this one to work correctly\n    'depends': ['base', 'mail', 'hr'],\n\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'data/mail_data.xml',\n        'views/menu.xml',\n        'views/view.xml',\n    ],\n    'application': True,\n}\n```"
  },
  {
    "path": "demo_activity/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_activity/__manifest__.py",
    "content": "{\n    'name': \"demo activity\",\n    'summary': \"\"\"\n        tutorial - demo activity\n    \"\"\",\n    'description': \"\"\"\n        tutorial - demo activity\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['base', 'mail', 'hr'],\n\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'data/mail_data.xml',\n        'views/menu.xml',\n        'views/view.xml',\n    ],\n    'application': True,\n}\n"
  },
  {
    "path": "demo_activity/data/mail_data.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n    <data noupdate=\"0\">\n        <record id=\"mail_act_approval\" model=\"mail.activity.type\">\n            <field name=\"name\">Activity Approval</field>\n            <field name=\"icon\">fa-dollar</field>\n            <field name=\"res_model_id\" ref=\"demo_activity.model_demo_activity\"/>\n        </record>\n    </data>\n</odoo>\n"
  },
  {
    "path": "demo_activity/models/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_activity/models/models.py",
    "content": "from odoo import models, fields, api\n\nclass DemoActivity(models.Model):\n    _name = \"demo.activity\"\n    _description = \"Demo Activity\"\n    _inherit = ['mail.thread', 'mail.activity.mixin']\n\n    name = fields.Char(string='name', required=True)\n    employee_id = fields.Many2one(\n        'hr.employee', string=\"Employee\", required=True)\n\n    def button_activity_schedule(self):\n        self.activity_schedule(\n            'demo_activity.mail_act_approval',\n            user_id = self.sudo().employee_id.user_id.id,\n            note = 'my note',\n            summary = 'my summary')\n\n    def button_activity_feedback(self):\n        self.activity_feedback(\n            ['demo_activity.mail_act_approval'])\n\n    def button_activity_unlink(self):\n        self.activity_unlink(\n            ['demo_activity.mail_act_approval'])\n\n"
  },
  {
    "path": "demo_activity/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_activity_user,Demo Activity User Access,model_demo_activity,demo_activity_group_user,1,0,0,0\naccess_demo_activity_manager,Demo Activity Manager Access,model_demo_activity,demo_activity_group_manager,1,1,1,1"
  },
  {
    "path": "demo_activity/security/security.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n\n  <record id=\"module_demo_activity_category\" model=\"ir.module.category\">\n    <field name=\"name\">Demo Activity Category</field>\n  </record>\n\n  <record id=\"demo_activity_group_user\" model=\"res.groups\">\n    <field name=\"name\">User</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_activity_category\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('base.group_user'))]\"/>\n  </record>\n\n  <record id=\"demo_activity_group_manager\" model=\"res.groups\">\n    <field name=\"name\">Manager</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_activity_category\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('demo_activity_group_user'))]\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_activity/views/menu.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <menuitem id=\"demo_activity_menu\"\n      name=\"Demo Activity\"/>\n\n    <act_window id=\"action_demo_activity\"\n      name=\"Activity Demo\"\n      res_model=\"demo.activity\"\n      view_mode=\"tree,form\"/>\n\n    <menuitem id=\"menu_demo_activity\"\n      name=\"Activity Demo\"\n      action=\"action_demo_activity\"\n      parent=\"demo_activity_menu\" />\n</odoo>\n\n\n"
  },
  {
    "path": "demo_activity/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <record id=\"view_activity_form\" model=\"ir.ui.view\">\n          <field name=\"name\">demo.activity.form</field>\n          <field name=\"model\">demo.activity</field>\n          <field eval=\"25\" name=\"priority\"/>\n          <field name=\"arch\" type=\"xml\">\n              <form string=\"Demo Activity\">\n                <header>\n                    <button name=\"button_activity_schedule\" string=\"_activity schedule\" type=\"object\" class=\"oe_highlight\"/>\n                    <button name=\"button_activity_unlink\" string=\"activity unlink\" type=\"object\" class=\"oe_highlight\"/>\n                    <button name=\"button_activity_feedback\" string=\"activity feedback\" type=\"object\" class=\"oe_highlight\"/>\n                </header>\n                <sheet>\n                    <group>\n                        <field name=\"name\"/>\n                        <field name=\"employee_id\"/>\n                    </group>\n                </sheet>\n\n                <div class=\"oe_chatter\">\n                    <field name=\"message_follower_ids\" widget=\"mail_followers\"/>\n                    <field name=\"activity_ids\" widget=\"mail_activity\"/>\n                    <field name=\"message_ids\" widget=\"mail_thread\"/>\n                </div>\n\n              </form>\n          </field>\n    </record>\n</odoo>"
  },
  {
    "path": "demo_class_inheritance/README.md",
    "content": "# odoo 繼承 - class inheritance\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 繼承 - class inheritance](https://youtu.be/zgb_0MJ3q9w)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n本篇文章主要介紹 class inheritance 這部份\n\n## 說明\n\n`_inherit` class inheritance\n\n注意, 還有一個是 `_inherits`, 不要搞錯了哦.\n\n通常 `_inherit` 是去修改或是去擴充既有的 model,\n\n使用情境可能如下,\n\n像是在一個既有的 model 上增加一個 fields.\n\n覆蓋掉一個已經存在的 model 中的 fields 定義.\n\n增加 constraints 到一個既有的 model 上.\n\n增加額外的 method 到一個既有的 model 上.\n\n覆蓋掉一個已經存在的 model 中的 method.\n\n這張圖是 odoo 中繼承的種類, 今天介紹 class inheritance,\n\n![alt tag](https://i.imgur.com/2aQ8BNh.png)\n\n先來看 [models/models.py](models/models.py)\n\n```python\n......\nclass ClassInheritance(models.Model):\n    _name = 'hr.expense' # 可寫可不寫\n    _inherit = ['hr.expense']\n\n    test_field = fields.Char('test_field')\n......\n```\n\n目標是去繼承 `hr.expense`, 並且增加一個 `fields`.\n\n`_name = 'hr.expense' # 可寫可不寫`\n\n`hr.expense` 是一個既有的 model, 所以在 `__manifest__.py` 中有 depends 關係\n\n(記得一定要寫 depends, 不然會出現錯誤 :exclamation:)\n\n```python\n......\n 'depends': ['hr_expense'],\n......\n```\n\n`_name` 和 `_inherit` 在這邊的名稱都是一樣的,\n\n注意, 請不要自己定義一個 `_name` (和 `_inherit` 不一致), 因為這是另一個東西(如下圖, 以後說明).\n\n![alt tag](https://i.imgur.com/kjtCar6.png)\n\n`_inherit = ['hr.expense']`\n\n主要去繼承 `hr.expense`, 所以一定要有 depends :exclamation: :exclamation:\n\n當你安裝好 addons, 我們到資料庫中可以找到剛剛新增的 test_field\n\n![alt tag](https://i.imgur.com/MOFEDXy.png)\n\n\n簡單說這種繼承的方式就是在繼承的 model 上增加新功能.\n\n[views/views.xml](views/views.xml)\n\n```xml\n......\n<record id=\"view_expenses_tree_custom\" model=\"ir.ui.view\">\n  <field name=\"name\">hr.expense.tree.custom</field>\n  <field name=\"model\">hr.expense</field>\n  <field name=\"inherit_id\" ref=\"hr_expense.view_expenses_tree\"/>\n  <field name=\"arch\" type=\"xml\">\n      <field name=\"date\" position=\"after\">\n          <!-- <field name=\"test_field\" groups=\"product.group_sale_pricelist\" readonly=\"1\"/> -->\n          <field name=\"test_field\"/>\n      </field>\n\n      <!-- xpath the same result -->\n      <!--views/views.xml\n      <xpath expr=\"//field[@name='date']\" position=\"after\">\n          <field name=\"test_field\" />\n      </xpath>\n      -->\n\n  </field>\n</record>\n......\n```\n\n![alt tag](https://i.imgur.com/PobVtjJ.png)\n\n找 fields 的時候有兩種方式可以找,\n\n第一種, 比較簡單的方法, 直接找到 fields, 然後定義 position 即可\n\n```xml\n......\n<field name=\"date\" position=\"after\">\n  <field name=\"test_field\"/>\n</field>\n......\n```\n\n第二種, 使用 xpath 的語法, 稍微比較複雜一點, 但是當你一個 view\n\n裡面有重複的 fields 時, 就比較適合使用 xpath, 因為如果你使用第\n\n一種方法, 會導致找不到 (有重複它會不知道要找哪一個 :grimacing:)\n\n```xml\n......\n<xpath expr=\"//field[@name='date']\" position=\"after\">\n  <field name=\"test_field\" />\n</xpath>\n......\n```\n\n這邊補充一下 xpath slash 的不同 `/` `//` :exclamation: :exclamation:\n\n`/` 代表直接去找目前節點的 child, 如果找不到就會發生錯誤. (類似絕對路徑的概念).\n\n`//` 代表去找目前節點的後面全部的節點 (類似相對路徑的概念).\n\n通常用 `/` 找不到的節點, 改用 `//` 都會找的到 :smile:\n\n如果你想要看更詳細的說明, google 關鍵字是 `xpath slash vs double slash` :smirk:\n\n所以可以依照自己的需求下去選擇.\n\n[views/views.xml](views/views.xml)\n\nform 的部份\n\n```xml\n......\n<record id=\"hr_expense_view_form_custom\" model=\"ir.ui.view\">\n    <field name=\"name\">hr.expense.view.form.custom</field>\n    <field name=\"model\">hr.expense</field>\n    <field name=\"inherit_id\" ref=\"hr_expense.hr_expense_view_form\"/>\n    <field name=\"arch\" type=\"xml\">\n        <field name=\"employee_id\" position=\"after\">\n            <field name=\"test_field\"/>\n        </field>\n    </field>\n</record>\n......\n```\n\n![alt tag](https://i.imgur.com/DXJ4xK2.png)\n\n## 延伸閱讀\n\n* [demo_prototype_inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_prototype_inheritance)\n\n* [demo_delegation_inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_delegation_inheritance)"
  },
  {
    "path": "demo_class_inheritance/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_class_inheritance/__manifest__.py",
    "content": "{\n    'name': \"demo_class_inheritance\",\n    'summary': \"\"\"\n        demo_class_inheritance,\n        model, view, form\n    \"\"\",\n    'description': \"\"\"\n        demo_class_inheritance\n        model, view, form\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['hr_expense'],\n\n    # always loaded\n    'data': [\n        'views/views.xml',\n    ],\n    # only loaded in demonstration mode\n    # 'demo': [\n    #     'demo/demo.xml',\n    # ],\n    'installable': True,\n    'auto_install': False,\n    'application': False,\n}\n\n"
  },
  {
    "path": "demo_class_inheritance/models/__init__.py",
    "content": "from . import model\n"
  },
  {
    "path": "demo_class_inheritance/models/model.py",
    "content": "from odoo import models, fields\n\n# https://www.odoo.com/forum/help-1/question/whats-the-difference-between-inherit-and-inherits-52205\n\n\n# class inheritance\nclass ClassInheritance(models.Model):\n    _name = 'hr.expense' # 可寫可不寫\n    _inherit = ['hr.expense']\n\n    test_field = fields.Char('test_field')\n\n"
  },
  {
    "path": "demo_class_inheritance/views/views.xml",
    "content": "<odoo>\n    <data>\n        <record id=\"view_expenses_tree_custom\" model=\"ir.ui.view\">\n            <field name=\"name\">hr.expense.tree.custom</field>\n            <field name=\"model\">hr.expense</field>\n            <field name=\"inherit_id\" ref=\"hr_expense.view_expenses_tree\"/>\n            <field name=\"arch\" type=\"xml\">\n                <field name=\"date\" position=\"after\">\n                    <!-- <field name=\"test_field\" groups=\"product.group_sale_pricelist\" readonly=\"1\"/> -->\n                    <field name=\"test_field\"/>\n                </field>\n\n                <!-- xpath the same result -->\n                <!--views/views.xml\n                <xpath expr=\"//field[@name='date']\" position=\"after\">\n                    <field name=\"test_field\" />\n                </xpath>\n                -->\n\n            </field>\n        </record>\n\n        <record id=\"hr_expense_view_form_custom\" model=\"ir.ui.view\">\n            <field name=\"name\">hr.expense.view.form.custom</field>\n            <field name=\"model\">hr.expense</field>\n            <field name=\"inherit_id\" ref=\"hr_expense.hr_expense_view_form\"/>\n            <field name=\"arch\" type=\"xml\">\n                <field name=\"employee_id\" position=\"after\">\n                    <field name=\"test_field\"/>\n                </field>\n            </field>\n        </record>\n    </data>\n</odoo>"
  },
  {
    "path": "demo_config_settings/README.md",
    "content": "# 實作 config settings\n\n建議觀看影片, 會更清楚 :smile:\n\n* [實作 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)\n\n* [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)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n有時候會需要對 addons 做一些參數的 settings,\n\n所以這篇主要介紹 odoo 中如何實現 config settings 的部份.\n\n## 說明\n\n[models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_config_settings/models/models.py)\n\n```python\nclass ResConfigSettings(models.TransientModel):\n    _inherit = 'res.config.settings'\n\n    demo_prefix = fields.Char(\n                string=\"Demo Prefix\",\n                # config_parameter='demo_config_settings.config.demo_prefix',\n                )\n\n    def get_values(self):\n        res = super(ResConfigSettings, self).get_values()\n        demo_prefix = self.env[\"ir.config_parameter\"].get_param(\"demo_config_settings.config.demo_prefix\", False)\n        res.update({\n            'demo_prefix': demo_prefix,\n        })\n        return res\n\n    def set_values(self):\n        super(ResConfigSettings, self).set_values()\n        self.env['ir.config_parameter'].sudo().set_param('demo_config_settings.config.demo_prefix', self.demo_prefix)\n```\n\n這邊是使用 TransientModel, 如果不知道這個是甚麼, 建議先了解之前的文章\n\n[demo_odoo_tutorial_wizard](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial_wizard) - TransientModel 範例.\n\n這邊的重點在需要實作 `set_values` 以及 `get_values`,\n\n`set_values`\n\n將 `demo_prefix` 設定到 `ir.config_parameter` model 的 `demo_config_settings.config.demo_prefix` (這個名稱可以自己自訂) 中.\n\n`get_values`\n\n從 `ir.config_parameter` model 的 param 找是否有 `demo_config_settings.config.demo_prefix`.\n\n如果你沒有特殊的邏輯要處理, 可以直接使用 `config_parameter='demo_config_settings.config.demo_prefix'` 代替,\n\n也就是說 `set_values` `get_values` 可以使用 `config_parameter=...` 替代.\n\nviews 的部份可參考 [views/view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_config_settings/views/view.xml).\n\n然後不需要 security 資料夾, 因為它是 TransientModel.\n\n裝好 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),\n\nOdoo Setup Demo 就是我們加上去的 (在這裡填入 hello123, 記得 Save)\n\n![alt tag](https://i.imgur.com/b6HFz7O.png)\n\n然後到 Technical -> Parameters -> System Parameters\n\n![alt tag](https://i.imgur.com/jNHjHhX.png)\n\n在這裡你會看到剛剛定義的 `demo_config_settings.config.demo_prefix` 為 hello123\n\n![alt tag](https://i.imgur.com/QbJYLGo.png)\n\n這樣子就可以在程式需要取設定值時, 直接到 `ir.config_parameter` model 裡找 :smile:\n\n## implied_group 用法說明\n\n[Youtube Tutorial - odoo - implied_group 用法說明](https://youtu.be/FCmRNUSkh10)\n\n這部份稍微比較進階一點,\n\n原始碼的路徑可參考 `odoo/addons/base/models/res_config.py`\n\n```python\n......\n\nclass ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin):\n    \"\"\" Base configuration wizard for application settings.  It provides support for setting\n        default values, assigning groups to employee users, and installing modules.\n        To make such a 'settings' wizard, define a model like::\n\n            class MyConfigWizard(models.TransientModel):\n                _name = 'my.settings'\n                _inherit = 'res.config.settings'\n\n                default_foo = fields.type(..., default_model='my.model'),\n                group_bar = fields.Boolean(..., group='base.group_user', implied_group='my.group'),\n                module_baz = fields.Boolean(...),\n                config_qux = fields.Char(..., config_parameter='my.parameter')\n                other_field = fields.type(...),\n\n......\n\n```\n\n`implied_group` 這個的功能主要是用來管理 user 擁有哪些 groups 的權限.\n\n( 其實他的概念和 `implied_ids` 是一樣的, 但這個用法更進階一點 :smirk: )\n\n詳細說明請看下方的 demo,\n\n[models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_config_settings/models/models.py)\n\n```python\nclass ResConfigSettings(models.TransientModel):\n    _inherit = 'res.config.settings'\n\n    ......\n\n    group_demo_config_setting = fields.Boolean(\"Demo Config\",\n        group='base.group_user', # default\n        # group='demo_config_settings.demo_config_settings_tutorial_group',\n        implied_group='sale.group_delivery_invoice_address',\n        )\n    ......\n```\n\n`group='base.group_user'` 為預設, 如果你不設定, 就是會預設這個值.\n\n`implied_group='sale.group_delivery_invoice_address'` 這邊使用內建的\n\n`sale.group_delivery_invoice_address` 來當作範例.\n\n`sale.group_delivery_invoice_address` 路徑在 `addons/sale/security/sale_security.xml`\n\n```xml\n<record id=\"group_delivery_invoice_address\" model=\"res.groups\">\n    <field name=\"name\">Addresses in Sales Orders</field>\n    <field name=\"category_id\" ref=\"base.module_category_hidden\"/>\n</record>\n```\n\n當這個 field 為 True 的時候, 所有的 `group='base.group_user'` 都會擁有\n\n`sale.group_delivery_invoice_address` groups 的權限.\n\n(注意 :exclamation: :exclamation: field 命名一定要是 `group_xxx` )\n\n當設定為 True 時\n\n![alt tag](https://i.imgur.com/gmfqIju.png)\n\n你會發現全部的 `group='base.group_user'` 都擁有 `sale.group_delivery_invoice_address` groups 的權限.\n\n![alt tag](https://i.imgur.com/yGTegQ6.png)\n\n上面註解的 `group='demo_config_settings.demo_config_settings_tutorial_group'`\n\n只針對 admin user, 也就是設定為 True 時, 只會對擁有 admin 的 user 生效.\n\n(請自行修改測試, 這邊就不打字了, 影片內說明, 因為大同小異 :smile:)\n\n記得加入 [security/security.xml](security/security.xml)\n\n```xml\n......\n    <record id=\"demo_config_settings_tutorial_group\" model=\"res.groups\">\n        <field name=\"name\">Config Settings User</field>\n        <field name=\"category_id\" ref=\"base.module_category_hidden\"/>\n        <field name=\"users\"\n            eval=\"[(4, ref('base.user_root')),\n                    (4, ref('base.user_admin'))]\"/>\n    </record>\n......\n```\n\n`<field name=\"category_id\" ref=\"base.module_category_hidden\"/>`\n\n這代表這個 groups 是被隱藏的.\n\n也就是不會出現在 user 設定 groups 的地方.\n"
  },
  {
    "path": "demo_config_settings/__init__.py",
    "content": "from . import models"
  },
  {
    "path": "demo_config_settings/__manifest__.py",
    "content": "{\n    'name': \"demo odoo config settings\",\n    'summary': \"\"\"\n        basic tutorial -\n        demo odoo config settings\n    \"\"\",\n    'description': \"\"\"\n        basic tutorial -\n        demo odoo config settings\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['base_setup', 'sale'],\n\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'views/view.xml',\n    ],\n    'installable': True,\n    'auto_install': False,\n    'application': True,\n}\n"
  },
  {
    "path": "demo_config_settings/models/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_config_settings/models/models.py",
    "content": "from odoo import models, fields, api\n\nclass ResConfigSettings(models.TransientModel):\n    _inherit = 'res.config.settings'\n\n    demo_prefix = fields.Char(\n                string=\"Demo Prefix\",\n                # config_parameter='demo_config_settings.config.demo_prefix',\n                )\n\n    group_demo_config_setting = fields.Boolean(\"Demo Config\",\n        group='base.group_user', # default\n        # group='demo_config_settings.demo_config_settings_tutorial_group',\n        implied_group='sale.group_delivery_invoice_address',\n        )\n\n    def get_values(self):\n        res = super(ResConfigSettings, self).get_values()\n        demo_prefix = self.env[\"ir.config_parameter\"].get_param(\"demo_config_settings.config.demo_prefix\", False)\n        res.update({\n            'demo_prefix': demo_prefix,\n        })\n        return res\n\n    def set_values(self):\n        super(ResConfigSettings, self).set_values()\n        self.env['ir.config_parameter'].sudo().set_param('demo_config_settings.config.demo_prefix', self.demo_prefix)\n"
  },
  {
    "path": "demo_config_settings/security/security.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n\n  <record id=\"demo_config_settings_tutorial_group\" model=\"res.groups\">\n    <field name=\"name\">Config Settings User</field>\n    <field name=\"category_id\" ref=\"base.module_category_hidden\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_config_settings/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n  <record id=\"settings_form_inherit_custom\" model=\"ir.ui.view\">\n      <field name=\"name\">res.config.inherit.custom</field>\n      <field name=\"model\">res.config.settings</field>\n      <field name=\"inherit_id\" ref=\"base.res_config_settings_view_form\"/>\n      <field name=\"arch\" type=\"xml\">\n          <xpath expr=\"//div[hasclass('settings')]\" position=\"inside\">\n              <div class=\"app_settings_block\" data-string=\"Custom\" string=\"Custom\" data-key=\"Custom\">\n                  <div id=\"setup_demo\">\n                      <h2>Odoo Setup Demo</h2>\n                      <div class=\"row mt16 o_settings_container\">\n                          <div class=\"col-xs-12 col-md-6 o_setting_box\">\n                              <div class=\"o_setting_right_pane\">\n                                  <label string=\"Prefixes\" for=\"Prefixes\"/>\n                                  <span class=\"fa fa-lg\"/>\n                                  <div class=\"text-muted\">\n                                      Set your demo prefix.\n                                  </div>\n                                  <div class=\"content-group\">\n                                      <div class=\"mt16 row\">\n                                          <label for=\"demo_prefix\" class=\"col-xs-3 col-md-6 o_light_label\"/>\n                                          <field name=\"demo_prefix\" class=\"oe_inline\"/>\n                                      </div>\n                                  </div>\n                                  <div class=\"content-group\">\n                                      <div class=\"mt16 row\">\n                                          <label for=\"group_demo_config_setting\" class=\"col-xs-3 col-md-6 o_light_label\"/>\n                                          <field name=\"group_demo_config_setting\" class=\"oe_inline\"/>\n                                      </div>\n                                  </div>\n                              </div>\n                          </div>\n                      </div>\n                  </div>\n              </div>\n          </xpath>\n      </field>\n  </record>\n</odoo>"
  },
  {
    "path": "demo_datetime_tutorial/README.md",
    "content": "# odoo datetime 教學\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo datetime 教學](https://youtu.be/Ha0YNFm6KzI)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n本篇文章將說明 odoo 中的 datetime 機制.\n\n## 說明\n\n注意 :exclamation:\n\n在 odoo 中所有的 date 和 datetime 都是使用 UTC 的時間 (包含保存進資料庫 db 的資料).\n\n這樣你可能會問我, odoo 不是可以選時區, 這樣頁面上是怎麼呈現時區正確的時間的 :question:\n\nodoo 會透過 session (或是你的設定) 去做 time zone 的轉換.\n\n每個 user 都可以設定自己的時區, 系統會建議你設定和瀏覽器一樣的時區\n\n![alt tag](https://i.imgur.com/cl34SUC.png)\n\n先來看一下 python 的 datetime\n\n```python\n>>> from datetime import datetime\n>>> datetime(2020, 1, 10, 0, 0)\ndatetime.datetime(2020, 1, 10, 0, 0)\n\n# strptime, python 的 str -> datetime\n>>> my_datetime = datetime.strptime('2020/11/23', '%Y/%m/%d')\n>>> my_datetime\ndatetime.datetime(2020, 11, 23, 0, 0)\n\n# strftime, python 的 datetime -> str\n>>> datetime.strftime(my_datetime, '%Y-%m-%d')\n'2020-11-23'\n```\n\npython 的 timedelta\n\n```python\n>>> from datetime import date\n>>> from datetime import timedelta\n>>> date.today()\ndatetime.date(2020, 10, 14)\n\n>>> date.today() + timedelta(days=7)\ndatetime.date(2020, 10, 21)\n```\n\npython 的時區轉換\n\n```python\n>>> ## Asia/Taipei -> UTC\n>>> from pytz import timezone\n>>> from datetime import datetime\n>>> today = datetime(2022, 7, 7, 10, 0)\n>>> user_tz = timezone('Asia/Taipei')\n\n>>> today = user_tz.localize(today)\n>>> today\ndatetime.datetime(2022, 7, 7, 10, 0, tzinfo=<DstTzInfo 'Asia/Taipei' CST+8 :00:00 STD>)\n\n>>> today = today.astimezone(timezone('UTC'))\n>>> today\ndatetime.datetime(2022, 7, 7, 2, 0, tzinfo=<UTC>)\n\n\n>>> ## UTC -> Asia/Taipei\n>>> from pytz import timezone\n>>> from datetime import datetime\n>>> today = datetime(2022, 7, 7, 10, 0)\n>>> user_tz = timezone('UTC')\n\n>>> today = user_tz.localize(today)\n>>> today\ndatetime.datetime(2022, 7, 7, 10, 0, tzinfo=<UTC>)\n\n>>> today = today.astimezone(timezone('Asia/Taipei'))\n>>> today\ndatetime.datetime(2022, 7, 7, 18, 0, tzinfo=<DstTzInfo 'Asia/Taipei' CST+8 :00:00 STD>)\n```\n\nodoo 中的 odoo.tools.date_utils\n\n`start_of(value, granularity)`\n\n`end_of(value, granularity)`\n\n`add(value, **kwargs)`\n\n`subtract(value, **kwargs)`\n\n```python\nfrom odoo.tools import date_utils\nfrom datetime import date\n\n>>> date.today()\ndatetime.date(2020, 10, 14)\n\n>>> date_utils.add(date.today(), days=2)\ndatetime.date(2020, 10, 16)\n\n>>> date_utils.subtract(date.today(), months=2)\ndatetime.date(2020, 8, 14)\n```\n\nodoo 中的 `fields.Date.today()` `fields.Datetime.now()`\n\n```python\n>>> from odoo import fields\n>>> fields.Date.today()\ndatetime.date(2020, 10, 14)\n>>> fields.Datetime.now()\ndatetime.datetime(2020, 10, 14, 10, 48, 25)\n```\n\nfields.Date `to_date` converts a string into a date object.\n\nfields.Datetime `to_datetime(value)` converts a string into a datetime object.\n\nfields.Date, fields.Datetime `to_string(value)` converts a date or datetime object into a string in the format expected by the Odoo server.\n\n( 其實這個 `to_string(value)` 也只是使用 python 的 `strftime` 去轉換而已, 格式是預設的 DATETIME_FORMAT)\n\n`fields.Date.context_today(record, timestamp=None)`\n\n`fields.Datetime.context_timestamp(record, timestamp)`\n\n```python\n>>> from odoo import fields\n>>> my_datetime = fields.Datetime.to_datetime('2020-02-10 10 :00:00')\n>>> my_datetime\ndatetime.datetime(2020, 2, 10, 10, 0)\n\n>>> my_datetime = fields.Datetime.from_string('2020-02-10 10 :00:00')\n>>> my_datetime\ndatetime.datetime(2020, 2, 10, 10, 0)\n\n>>> fields.Datetime.to_string(my_datetime)\n'2020-02-10 10 :00:00'\n```\n\n寫入資料時, 可以直接輸入 string, 會自動轉換成 date / datetime\n\n```python\n>>> demo = self.env['demo.datetime'].browse(1)\n>>> demo.my_datetime\ndatetime.datetime(2020, 10, 14, 9, 30, 8)\n>>> demo.my_datetime = '2020-01-01 09 :00:00'\n>>> demo.my_datetime\ndatetime.datetime(2020, 1, 1, 9, 0)\n```\n\n轉換時區\n\n```python\nfrom pytz import timezone\n\n# Convert to Asia/Taipei time zone\n\n>>> demo = self.env['demo.datetime'].browse(1)\n>>> demo.my_datetime\ndatetime.datetime(2020, 1, 1, 9, 0)\n\n>>> demo.my_datetime.astimezone(timezone('Asia/Taipei'))\ndatetime.datetime(2020, 1, 1, 17, 0, tzinfo=<DstTzInfo 'Asia/Taipei' CST+8 :00:00 STD>)\n```\n\n將 addons 裝起來之後, 來看 [models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_datetime_tutorial/models/models.py)\n\n```python\n......\n       @api.multi\n       def demo1(self):\n           _logger.warning('db datetime')\n           _logger.warning(self.my_datetime )\n\n           _logger.warning('Asia/Taipei datetime')\n           _logger.warning(self.my_datetime.astimezone(timezone('Asia/Taipei')))\n......\n```\n\n![alt tag](https://i.imgur.com/C4yMCYb.png)\n\n可以從輸出中看到一個是 db 保存的 utc 時間, 一個是轉換後的 Taipei 時間.\n\n![alt tag](https://i.imgur.com/rprCbmE.png)\n\ndb 中保存的時間\n\n![alt tag](https://i.imgur.com/kp4NkiS.png)\n"
  },
  {
    "path": "demo_datetime_tutorial/__init__.py",
    "content": "from . import models"
  },
  {
    "path": "demo_datetime_tutorial/__manifest__.py",
    "content": "{\n    'name': \"demo_datetime_tutorial\",\n    'summary': \"\"\"\n        demo_datetime_tutorial\n    \"\"\",\n    'description': \"\"\"\n        demo_datetime_tutorial\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['base'],\n\n    # always loaded\n    'data': [\n        'security/ir.model.access.csv',\n        'views/menu.xml',\n        'views/view.xml',\n    ],\n    'installable': True,\n    'auto_install': False,\n    'application': True,\n}\n"
  },
  {
    "path": "demo_datetime_tutorial/models/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_datetime_tutorial/models/models.py",
    "content": "from odoo import models, fields, api\nfrom pytz import timezone\nimport logging\n\n_logger = logging.getLogger(__name__)\n\nclass DemoDatetime(models.Model):\n    _name = \"demo.datetime\"\n    _description = 'Demo Datetime Tutorial'\n\n    name = fields.Char('Name', required=True)\n    my_datetime = fields.Datetime(\n        'my_datetime', default=fields.Datetime.now())\n\n    @api.multi\n    def demo1(self):\n        _logger.warning('db datetime')\n        _logger.warning(self.my_datetime )\n\n        _logger.warning('Asia/Taipei datetime')\n        _logger.warning(self.my_datetime.astimezone(timezone('Asia/Taipei')))\n\n"
  },
  {
    "path": "demo_datetime_tutorial/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_datetime,Demo Datetime User Access,model_demo_datetime,,1,1,1,1"
  },
  {
    "path": "demo_datetime_tutorial/views/menu.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <!-- Demo Datetime Menu -->\n    <menuitem id=\"demo_datetime\"\n              name=\"Demo Datetime\" />\n\n    <!-- Action to open the Demo Datetime list -->\n    <act_window id=\"action_demo_datetime\"\n      name=\"Demo Datetime\"\n      res_model=\"demo.datetime\"\n      view_mode=\"tree,form\"/>\n\n    <!-- Menu item to open the Demo Datetime list -->\n    <menuitem id=\"menu_demo_datetime\"\n              name=\"Demo Datetime\"\n\t            action=\"action_demo_datetime\"\n              parent=\"demo_datetime\" />\n\n</odoo>\n\n\n"
  },
  {
    "path": "demo_datetime_tutorial/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <record id=\"view_demo_datetime_tutorial_form\" model=\"ir.ui.view\">\n        <field name=\"name\">demo_datetime_tutorial_form</field>\n        <field name=\"model\">demo.datetime</field>\n        <field name=\"arch\" type=\"xml\">\n        <form string=\"Demo Odoo Tutorial\">\n            <header>\n                <button name=\"demo1\" string=\"demo1\" type=\"object\"/>\n            </header>\n            <sheet>\n                <group>\n                    <field name=\"name\"/>\n                    <field name=\"my_datetime\"/>\n                </group>\n            </sheet>\n        </form>\n        </field>\n    </record>\n\n    <record id=\"view_demo_datetime_tutorial_tree\" model=\"ir.ui.view\">\n        <field name=\"name\">demo_datetime_tutorial_tree</field>\n        <field name=\"model\">demo.datetime</field>\n        <field name=\"arch\" type=\"xml\">\n        <tree>\n            <field name=\"name\"/>\n            <field name=\"my_datetime\"/>\n        </tree>\n        </field>\n    </record>\n</odoo>"
  },
  {
    "path": "demo_delegation_inheritance/README.md",
    "content": "# odoo 繼承 - delegation inheritance\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 繼承 - delegation inheritance](https://youtu.be/J1-Hg9vrXBs)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n* [odoo 繼承 - class inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_class_inheritance)\n\n* [odoo 繼承 - prototype inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_prototype_inheritance)\n\n本篇文章主要介紹 delegation inheritance 這部份\n\n## 說明\n\n在開始介紹範例之前, 請先看下圖\n\nModel inheritance ( `_inherit` vs `_inherits` ),\n\n[inheritance](https://www.odoo.com/documentation/12.0/howtos/backend.html#inheritance)\n\n![alt tag](https://i.imgur.com/IRPk1By.png)\n\n`_inherits` 以下為官方說明\n\n```txt\nThe 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.\n```\n\n```python\nclass DelegationInheritance(models.Model):\n    _name = 'new'\n    _inherits = 'obj1'\n```\n\n幾個重點,\n\nStored in different tables (會儲存在不同的 table 中)\n\n`new` instances contain am embedded.\n\n`obj1` instance with synchronized values.\n\n(同步的意思就是會幫你自動建立, 等等來看實例說明)\n\n先來看 [models/models.py](models/models.py)\n\n```python\nclass DelegationInheritance(models.Model):\n    _name = 'demo.delegation'\n    _description = 'Demo DelegationInheritance'\n    _inherits = {\"res.partner\": \"partner_id\"}\n\n    partner_id = fields.Many2one('res.partner', string='Partner', required=True, ondelete=\"cascade\")\n\n    first_name = fields.Char('First Name', size=16)\n    ......\n```\n\n再來看 [views/view.xml](views/view.xml)\n\n```xml\n......\n<record id=\"view_form_demo_delegation_tutorial\" model=\"ir.ui.view\">\n<field name=\"name\">Demo Delegation Tutorial Form</field>\n<field name=\"model\">demo.delegation</field>\n<field name=\"arch\" type=\"xml\">\n    <form string=\"Demo Delegation Tutorial\">\n    <sheet>\n        <group>\n            <!-- res.partner -->\n            <field name=\"name\"/>\n            <field name=\"company_id\"/>\n            <!-- res.partner -->\n\n            <!-- demo.delegation -->\n            <field name=\"partner_id\" invisible=\"1\" attrs=\"{'required': [('id', '!=', False)]}\"/>\n            <field name=\"first_name\"/>\n            <!-- demo.delegation -->\n        </group>\n    </sheet>\n    </form>\n</field>\n</record>\n......\n```\n\n其實它有點特殊, 在裡面甚至可以使用 (委派 Delegation) `res.partner` 的欄位.\n\n當建立 `demo.delegation` 時, 也會自動幫你建立 `partner_id`.\n\n你可能會問我為甚麼沒有 `name` field, 但是卻可以使用 `name` :question:\n\n因為這個 `name` 其實是屬於 `res.partner` 的 :smile:\n\n以下操作一遍流程,\n\n在 `demo.delegation` 中建立一筆資料\n\n![alt tag](https://i.imgur.com/vfPNsva.png)\n\n在 `res.partner` 中也會自動建立一筆資料\n\n![alt tag](https://i.imgur.com/JZ2EUv1.png)\n\n接著從 db 中看資料怎麼跑\n\n在 `demo.delegation` 中只紀錄了 `partner_id` 而已, 當然還有 `first_name`.\n\n![alt tag](https://i.imgur.com/i7SyECl.png)\n\n其他的 `name` `company_id` 都是儲存在 `res.partner` 中的.\n\n(雖然是在 `demo.delegation` 中輸入的, 但這就是委派的概念)\n\n![alt tag](https://i.imgur.com/UHOqrwx.png)\n\n小結論, 父類別`res.partner`的 field 會儲存在父類別`res.partner`的 table 中,\n\n而新的模型`demo.delegation`的 field 則會儲存在新的模型`demo.delegation`的 table 中.\n\n當使用新的模型`demo.delegation`時, 可以看到父類別`res.partner`的資料.\n\n當使用父類別`res.partner`時, **只能看到**父類別`res.partner`的資料.\n\n委派最重要的目的就是避免在很多的 table 中建立重複的資料. (達到共用的效果 :smile:)\n\n在原始碼中, 也有幾個範例可以參考 :smile:\n\n第一個範例為 `res.users` 以及 `res.partner`\n\n```python\n# odoo/addons/base/models/res_users.py\nclass Users(models.Model):\n    \"\"\" User class. A res.users record models an OpenERP user and is different\n        from an employee.\n\n        res.users class now inherits from res.partner. The partner model is\n        used to store the data related to the partner: lang, name, address,\n        avatar, ... The user model is now dedicated to technical data.\n    \"\"\"\n    _name = \"res.users\"\n    _description = 'Users'\n    _inherits = {'res.partner': 'partner_id'}\n\n    ......\n\n    partner_id = fields.Many2one('res.partner', required=True, ondelete='restrict', auto_join=True,\n            string='Related Partner', help='Partner-related data of the user')\n\n    ......\n\n# odoo/addons/base/models/res_partner.py\nclass Partner(models.Model):\n    _description = 'Contact'\n    _inherit = ['format.address.mixin']\n    _name = \"res.partner\"\n    _order = \"display_name\"\n\n    ......\n```\n\n當你建立 `res.users` 時, 也會自動建立一個 `res.partner`. (可搭配 db 觀看結果)\n\n在 `res.users` 底下, 可以任意得使用 `res.partner` field, 但相反過來,\n\n在 `res.partner` 底下, 只可以使用 `res.partner` 自己的 field.\n\n第二個範例為 `product.product` 以及 `product.template`\n\n```python\n# addons/product/models/product.py\nclass ProductProduct(models.Model):\n    _name = \"product.product\"\n    _description = \"Product\"\n    _inherits = {'product.template': 'product_tmpl_id'}\n    _inherit = ['mail.thread', 'mail.activity.mixin']\n    _order = 'default_code, name, id'\n\n    ......\n\tproduct_tmpl_id = fields.Many2one(\n\t\t'product.template', 'Product Template',\n\t\tauto_join=True, index=True, ondelete=\"cascade\", required=True)\n    ......\n\n# addons/product/models/product_template.py\nclass ProductTemplate(models.Model):\n    _name = \"product.template\"\n    _inherit = ['mail.thread', 'mail.activity.mixin', 'image.mixin']\n    _description = \"Product Template\"\n    _order = \"name\"\n    ......\n```\n\n"
  },
  {
    "path": "demo_delegation_inheritance/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_delegation_inheritance/__manifest__.py",
    "content": "{\n    'name': 'demo_delegation_inheritance',\n    'version': '12.0.1.0.0',\n    'summary': 'demo_delegation_inheritance',\n    'description': '''\n        demo_delegation_inheritance\n    ''',\n    'depends': ['base', 'contacts'],\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'views/menu.xml',\n        'views/view.xml',\n    ],\n    'license': 'AGPL-3',\n    'images': [\n    ],\n    'qweb': [\n    ],\n    'installable': True,\n    'auto_install': False,\n    'application': False,\n}\n"
  },
  {
    "path": "demo_delegation_inheritance/models/__init__.py",
    "content": "from . import model\n"
  },
  {
    "path": "demo_delegation_inheritance/models/model.py",
    "content": "from odoo import models, fields, api\n\n# ref.\n#\n# addons/product/models/product.py\n# class ProductProduct(models.Model):\n#   _name = \"product.product\"\n#\n# addons/product/models/product_template.py\n# class ProductTemplate(models.Model):\n#   _name = \"product.template\"\n#\n\nclass DelegationInheritance(models.Model):\n    _name = 'demo.delegation'\n    _description = 'Demo DelegationInheritance'\n    _inherits = {\"res.partner\": \"partner_id\"}\n\n    partner_id = fields.Many2one('res.partner', string='Partner', required=True, ondelete=\"cascade\")\n\n    first_name = fields.Char('First Name', size=16)\n"
  },
  {
    "path": "demo_delegation_inheritance/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_delegation_user,Demo Delegation Tutorial User Access,model_demo_delegation,demo_delegation_tutorial_group_user,1,0,0,0\naccess_demo_delegation_manager,Demo Delegation Tutorial Manager Access,model_demo_delegation,demo_delegation_tutorial_group_manager,1,1,1,1\n"
  },
  {
    "path": "demo_delegation_inheritance/security/security.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n\n  <record id=\"module_demo_delegation_tutorial\" model=\"ir.module.category\">\n    <field name=\"name\">Demo Delegation tutorial category</field>\n  </record>\n\n  <record id=\"demo_delegation_tutorial_group_user\" model=\"res.groups\">\n    <field name=\"name\">User</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_delegation_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('base.group_user'))]\"/>\n  </record>\n\n\n  <record id=\"demo_delegation_tutorial_group_manager\" model=\"res.groups\">\n    <field name=\"name\">Manager</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_delegation_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('demo_delegation_tutorial_group_user'))]\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_delegation_inheritance/views/menu.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <!-- demo_delegation_tutorial App Menu -->\n    <menuitem id=\"demo_delegation_tutorial_menu\"\n              name=\"Demo Delegation Tutorial\" />\n\n    <!-- Action to open the demo_delegation_tutorial -->\n    <act_window id=\"action_delegation_tutorial\"\n                name=\"Demo Delegation Tutorial Action\"\n                res_model=\"demo.delegation\"\n                view_mode=\"tree,form\"/>\n\n    <!-- Menu item to open the demo_delegation_tutorial -->\n    <menuitem id=\"menu_delegation_tutorial\"\n              name=\"Demo Delegation Tutorial\"\n\t          action=\"action_delegation_tutorial\"\n              parent=\"demo_delegation_tutorial_menu\" />\n</odoo>\n\n\n"
  },
  {
    "path": "demo_delegation_inheritance/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n\n  <record id=\"view_form_demo_delegation_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Delegation Tutorial Form</field>\n    <field name=\"model\">demo.delegation</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Delegation Tutorial\">\n        <sheet>\n          <group>\n            <!-- res.partner -->\n            <field name=\"name\"/>\n            <field name=\"company_id\"/>\n            <!-- res.partner -->\n\n            <!-- demo.delegation -->\n            <field name=\"partner_id\" invisible=\"1\" attrs=\"{'required': [('id', '!=', False)]}\"/>\n            <field name=\"first_name\"/>\n            <!-- demo.delegation -->\n          </group>\n        </sheet>\n      </form>\n    </field>\n  </record>\n\n  <record id=\"view_tree_demo_delegation_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Delegation Tutorial List</field>\n    <field name=\"model\">demo.delegation</field>\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n        <!-- res.partner -->\n        <field name=\"partner_id\" invisible=\"1\" attrs=\"{'required': [('id', '!=', False)]}\"/>\n        <field name=\"name\"/>\n        <field name=\"company_id\"/>\n        <!-- res.partner -->\n\n        <!-- demo.delegation -->\n        <field name=\"first_name\"/>\n        <!-- demo.delegation -->\n      </tree>\n    </field>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_expense_tutorial_v1/README.md",
    "content": "# odoo 入門篇\n\n建議觀看影片, 會更清楚 :smile:\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* 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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n* 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)\n\n* [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)\n\n* [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)\n\n* [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)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n這篇主要介紹 Many2one, Many2many, One2many 這三個東西,\n\n以下將介紹這個 addons 的結構\n\n## 說明\n\n### odoo 手把手教學 - Many2one - part1\n\n* [Youtube Tutorial - odoo 手把手教學 - Many2one - part1](https://youtu.be/vb_Z8KCI-wk)\n\n先來看 [models/models.py](models/models.py)\n\n`Many2one`\n\n```python\n......\nclass DemoExpenseTutorial(models.Model):\n    _name = 'demo.expense.tutorial'\n    _description = 'Demo Expense Tutorial'\n\n    name = fields.Char('Description', required=True)\n    employee_id = fields.Many2one('hr.employee', string=\"Employee\", required=True)\n    user_id = fields.Many2one('res.users', default=lambda self: self.env.user)\n......\n```\n\n![alt tag](https://i.imgur.com/lBj9pgz.png)\n\n一個 `hr.employee` 可以對到很多個 `demo.expense.tutorial`,\n\n所以是 多(`demo.expense.tutorial`) 對 一(`hr.employee`) 的關係,\n\n來看 db 中的狀況\n\n`demo_expense_tutorial` 會多出一個欄位 ( 對應 `hr_employee` 的 id )\n\n![alt tag](https://i.imgur.com/8BVrrJa.png)\n\n`user_id` field 中的 `default=lambda self: self.env.user` 代表預設的值會設定當前登入的 user\n\n![alt tag](https://i.imgur.com/QiDj6iM.png)\n\n因為 One2many 比較特別, 所以我們先介紹 Many2many :laughing:\n\n### odoo 手把手教學 - Many2many - part2\n\n`Many2many`\n\n* [Youtube Tutorial - odoo 手把手教學 - Many2many - part2](https://youtu.be/QeZfJqTGP-w)\n\n要建立 Many2many 之前, 一定要先定義一個 model,\n\n先定義 DemoTag (也請記得設定 [security/ir.model.access.csv](security/ir.model.access.csv) )\n\n[models/models.py](models/models.py)\n\n```python\n......\nclass DemoTag(models.Model):\n    _name = 'demo.tag'\n    _description = 'Demo Tags'\n\n    name = fields.Char(string='Tag Name', index=True, required=True)\n    active = fields.Boolean(default=True, help=\"Set active.\")\n......\n```\n\n然後接著到底下 [models/models.py](models/models.py)\n\n```python\n......\nclass DemoExpenseTutorial(models.Model):\n    _name = 'demo.expense.tutorial'\n......\n    # https://www.odoo.com/documentation/12.0/reference/orm.html#odoo.fields.Many2many\n    # Many2many(comodel_name=<object object>, relation=<object object>, column1=<object object>, column2=<object object>, string=<object object>, **kwargs)\n    #\n    # relation: database table name\n    #\n\n    # By default, the relationship table name is the two table names\n    # joined with an underscore and _rel appended at the end.\n    # In the case of our books or authors relationship, it should be named demo_expense_tutorial_demo_tag_rel.\n### odoo 手把手教學 - Many2many - part2\n    tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id', string='Tges')\n......\n```\n\nMany2many 比較多欄位, 我來說明一下,\n\n`comodel_name` 為 `demo.tag` (需要對應的 model)\n\n`relation` 為 `demo_expense_tag` (table 名稱),\n\nMany2many 會多出一個 table, 這邊是針對 table 命名,\n\n也就是 db 中的 table 名稱,\n\n![alt tag](https://i.imgur.com/B7ren2r.png)\n\n如果你沒填 `relation` 這個值, 預設的 table 名稱會是 model名稱 + comodel_name + `_rel`,\n\n所以也就會是 `demo_expense_tutorial_demo_tag_rel`.\n\n`column1` 為 `demo_expense_id`, `demo.expense.tutorial` table 中對應的 id.\n\n`column2` 為 `tag_id`, `demo.tag` table 中對應的 id.\n\n繼續看 [models/models.py](models/models.py)\n\n```python\n......\n    # Related (Reference) fields (不會存在 db)\n    # readonly default 為 True\n    # store default 為 False\n    gender = fields.Selection('Gender', related='employee_id.gender')\n......\n```\n\n`fields.Selection` 就只是下拉選單而已, 比較特別的是 `related` 這個,\n\n`related='employee_id.gender'` 這邊的意思是, 會自己去找 employee_id 中的 gender,\n\n到 employee 中找到 gender 為 Male\n\n![alt tag](https://i.imgur.com/GDOC0hS.png)\n\nDemoExpenseTutorial 中的 `gender` 自然會是 Male,\n\n![alt tag](https://i.imgur.com/mJrzLjk.png)\n\n但要注意幾件事情,\n\n`related` 預設的 field 是不會儲存在 db 中的, store default 為 False,\n\n你在 table 中是找不到 `gender` field 的 (如下圖),\n\n如果你想要儲存在 db 中的, 請另外設定 `store=Ture`,\n\n![alt tag](https://i.imgur.com/0Xcvzas.png)\n\n然後 readonly default 為 True, 也就是說你是不可以去修改的,\n\n( 如果要修改請去 employee 中找到 gender 修改 )\n\n![alt tag](https://i.imgur.com/mJrzLjk.png)\n\n接著來看最後一個\n\n### odoo 手把手教學 - One2many - part3\n\n`One2many`\n\n* [Youtube Tutorial - odoo 手把手教學 - One2many - part3](https://youtu.be/WiLdXP781N0)\n\n![alt tag](https://i.imgur.com/lV2J3Tu.png)\n\n[models/models.py](models/models.py)\n\n一個 `demo.expense.sheet.tutorial` 可以對應很多個 `demo.expense.tutorial`\n\n所以是 一(`demo.expense.sheet.tutorial`) 對 多(`demo.expense.tutorial`) 的關係,\n\n```python\n......\nclass DemoExpenseSheetTutorial(models.Model):\n    _name = 'demo.expense.sheet.tutorial'\n    _description = 'Demo Expense Sheet Tutorial'\n\n    name = fields.Char('Expense Demo Report Summary', required=True)\n\n    # One2many is a virtual relationship, there must be a Many2one field in the other_model,\n    # and its name must be related_field\n    expense_line_ids = fields.One2many(\n        'demo.expense.tutorial', # related model\n        'sheet_id', # field for \"this\" on related model\n        string='Expense Lines')\n......\n```\n\n說明 expense_line_ids 裡面的參數意義,\n\n`demo.expense.tutorial` 代表關連的 model (必填)\n\n`sheet_id` 代表所關連 model 的 field (必填)\n\n也就是說如果你要建立 One2many, 一定也要有一個 Many2one,\n\n但如果建立 Many2one 則不一定要建立 One2many.\n\nOne2many 是一個虛擬的欄位, 你在資料庫中是看不到它的存在(如下圖)\n\n![alt tag](https://i.imgur.com/F6YTOdq.png)\n\n你只會看到 Many2one 中的 sheet_id\n\n![alt tag](https://i.imgur.com/lsiLpZK.png)\n\n[models/models.py](models/models.py), `demo.expense.tutorial` 中的 sheet_id\n\n```python\nclass DemoExpenseTutorial(models.Model):\n    _name = 'demo.expense.tutorial'\n    _description = 'Demo Expense Tutorial'\n    ......\n    sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string=\"Expense Report\")\n    ......\n```\n\n記得也要設定對應的 [security/ir.model.access.csv](security/ir.model.access.csv) 和 [security/security.xml](security/security.xml).\n\n[views/view.xml](views/view.xml)\n\n```xml\n......\n  <record id=\"view_form_demo_expense_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial Form</field>\n    ......\n            <!-- <field name=\"tag_ids\"/> -->\n            <field name=\"tag_ids\" widget=\"many2many_tags\"/> <!-- widget -->\n            <field name=\"sheet_id\"/>\n......\n\n```\n\n在 odoo 中很有多 widget, 大家可以改成其他的 widget 試試看, 像是 many2many_tags 的 widget\n\n![alt tag](https://i.imgur.com/UBYyUcf.png)\n\n[views/view.xml](views/view.xml)\n\n```xml\n......\n    <record id=\"view_form_demo_expense_sheet_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Sheet Tutorial Form</field>\n    <field name=\"model\">demo.expense.sheet.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Expense Sheet Tutorial\">\n        <sheet>\n          <group>\n            <field name=\"name\"/>\n          </group>\n          <notebook>\n              <page string=\"Expense\">\n                <field name=\"expense_line_ids\">\n                  <tree>\n                    <field name=\"name\"/>\n                    <field name=\"employee_id\"/>\n                    <field name=\"tag_ids\" widget=\"many2many_tags\"/>\n                  </tree>\n                </field>\n              </page>\n          </notebook>\n        </sheet>\n      </form>\n    </field>\n  </record>\n......\n```\n\n在 `view_form_demo_expense_sheet_tutorial` 裡的 One2many 中的 expense_line_ids fields,\n\n就把需要的欄位填進去即可,\n\n![alt tag](https://i.imgur.com/jiFHHST.png)\n\n### odoo 手把手教學 - One2many Editable Bottom and Top - part3-1\n\n這邊補充一下 One2many 中的 Editable Bottom 和 Top\n\n* [Youtube Tutorial - odoo 手把手教學 - One2many Editable Bottom and Top - part3-1](https://youtu.be/HJcBAFXQYVc)\n\n[views/view.xml](views/view.xml)\n\n```xml\n  <record id=\"view_form_demo_expense_sheet_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Sheet Tutorial Form</field>\n    <field name=\"model\">demo.expense.sheet.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Expense Sheet Tutorial\">\n        <sheet>\n          ......\n          <notebook>\n              <page string=\"Expense\">\n                <field name=\"expense_line_ids\" >\n                  <tree>\n                  <!-- <tree editable=\"top\"> -->   <!-- <<<<<<<<<<<< -->\n                  <!-- <tree editable=\"bottom\"> --> <!-- <<<<<<<<<<<< -->\n                    <field name=\"name\"/>\n                    <field name=\"employee_id\"/>\n                    <field name=\"tag_ids\" widget=\"many2many_tags\"/>\n                  </tree>\n                </field>\n              </page>\n          </notebook>\n        </sheet>\n      </form>\n    </field>\n  </record>\n```\n\n如果你加上 `editable` 這個參數, 當你新增 record 的時候, 就不會整個跳出視窗, 可以直接在裡面輸入\n\n(或許比較好看 :smile:)\n\n![alt tag](https://i.imgur.com/tdues3g.png)\n\n至於 `editable=\"bottom\"` 和 `editable=\"top\"` 的差別如下\n\n`editable=\"top\"` 一個新增的 record 會顯示在最上面\n\n![alt tag](https://i.imgur.com/qWaIH59.png)\n\n`editable=\"bottom\"`一個新增的 record 會顯示在最下面\n\n![alt tag](https://i.imgur.com/d3pfgRX.png)\n\n### odoo 手把手教學 - Search Filters - part4\n\n接著來看 filter 的功能\n\n* [Youtube Tutorial - odoo 手把手教學 - Search Filters - part4](https://youtu.be/zcWMs16p9Xw)\n\n```xml\n......\n<record id=\"view_filter_demo_expense_tutorial\" model=\"ir.ui.view\">\n  <field name=\"name\">Demo Expense Tutorial Filter</field>\n  <field name=\"model\">demo.expense.tutorial</field>\n  <field name=\"arch\" type=\"xml\">\n      <search string=\"Demo Expense Tutorial Filter\">\n          <field name=\"name\" string=\"Name\"/>\n          <field name=\"employee_id\" filter_domain=\"['|', ('employee_id', 'ilike', self), ('user_id', 'ilike', self)]\" string=\"User\"/>\n          <filter name=\"filter_inactive\" domain=\"[('active','=',False)]\" string=\"Inactive\"/>\n          <filter name=\"gender\" domain=\"[('gender','=','male')]\" string=\"Male\"/>\n          <separator/>\n          <filter name=\"name\" domain=\"[('name', 'ilike', 'a')]\" string=\"Name_2\"/>\n          <group expand=\"0\" string=\"Group By\">\n            <filter string=\"Sheet\" name=\"sheet\" domain=\"[]\" context=\"{'group_by': 'sheet_id'}\"/>\n            <filter string=\"Employee\" name=\"employee\" domain=\"[]\" context=\"{'group_by': 'employee_id'}\"/>\n          </group>\n      </search>\n  </field>\n</record>\n......\n```\n\n主要都是在 tree 中搜尋, 可參考上面的 code 去看對應的邏輯\n\n![alt tag](https://i.imgur.com/NIrhMBG.png)\n\n![alt tag](https://i.imgur.com/C4TyIaF.png)\n\n`<field name=\"employee_id\" filter_domain=\"['|', ('employee_id', 'ilike', self), ('user_id', 'ilike', self)]\" string=\"User\"/>`\n\n特別說明一下這個, `self` 代表使用者輸入的內容.\n\n`<separator>` 代表 `and`, 如果沒寫則代表 `or`.\n\n`and`\n\n![alt tag](https://i.imgur.com/vZ74GyY.png)\n\n`or`\n\n![alt tag](https://i.imgur.com/mhHdRrW.png)\n\n`<filter string=\"Sheet\" name=\"sheet\" domain=\"[]\" context=\"{'group_by': 'sheet_id'}\"/>`\n\n`<filter string=\"Employee\" name=\"employee\" domain=\"[]\" context=\"{'group_by': 'employee_id'}\"/>`\n\n依照特定的 fields 分組\n\n![alt tag](https://i.imgur.com/jojaYtz.png)\n\n點選後的狀態\n\n![alt tag](https://i.imgur.com/acyqVIA.png)\n\n### odoo 手把手教學 - 說明 noupdate 以及 domain_force - part5\n\n再來看看\n\n[security/ir_rule.xml](security/ir_rule.xml)\n\n* [Youtube Tutorial - odoo 手把手教學 - 說明 noupdate 以及 domain_force - part5](https://youtu.be/twn6zz3OeRs)\n\n```xml\n......\n    <data noupdate=\"1\">\n\n        <record id=\"ir_rule_demo_expense_user\" model=\"ir.rule\">\n            <field name=\"name\">Demo Expense User</field>\n            <field name=\"model_id\" ref=\"model_demo_expense_tutorial\"/>\n            <field name=\"domain_force\">[('employee_id.user_id.id', '=', user.id)]</field>\n            <field name=\"groups\" eval=\"[(4, ref('demo_expense_tutorial_group_user'))]\"/>\n        </record>\n\n        <record id=\"ir_rule_demo_expense_manager\" model=\"ir.rule\">\n            <field name=\"name\">Demo Expense Manager</field>\n            <field name=\"model_id\" ref=\"model_demo_expense_tutorial\"/>\n            <field name=\"domain_force\">[(1, '=', 1)]</field>\n            <field name=\"groups\" eval=\"[(4, ref('demo_expense_tutorial_group_manager'))]\"/>\n        </record>\n    </data>\n......\n```\n\n`noupdate=\"1\"` (代表更新 addons 時, 不再更新, 除非你刪掉 record)\n\n假如我們在安裝完 addons 之後, 已經存在 record 了, 這時候我們手動更新這些 record 的資料,\n\n再去更新 addons, 這些 record 是不會有任何改變的.\n\n(但是, 假如你刪掉 record, 再去更新 addons, record 會重新被安裝回來.)\n\n`noupdate=\"0\"` (代表更新 addons 時, 會保持更新, 也就是會被還原)\n\n假如我們在安裝完 addons 之後, 已經存在 record 了, 這時候我們手動更新這些 record 的資料,\n\n再去更新 addons, 這些 record 是會被改回原本的.\n\n(但是, 假如你刪掉 record, 再去更新 addons, record 會重新被安裝回來.)\n\n`id=\"ir_rule_demo_expense_user\"` 第一段為針對 `demo_expense_tutorial_group_user`\n\n限制 `domain_force`, 規則很簡單, 這類的 user 只能看到自己的單子, 也就是\n\n`[('employee_id.user_id.id', '=', user.id)]`.\n\n`id=\"ir_rule_demo_expense_manager\"` 針對 `demo_expense_tutorial_group_manager`\n\n限制 `domain_force`, 這邊比較特別 `[(1, '=', 1)]`, 代表沒有限制, 也就是全部的單子都\n\n可以看到.\n\n`demo` 用戶為 User, 所以只能看到自己的單子\n\n![alt tag](https://i.imgur.com/dX9QuMj.png)\n\n`Admin` 用戶為 Manager, 所以能看到全部的單子\n\n![alt tag](https://i.imgur.com/CFMsgie.png)\n\n接著補充說明一下, 在 [security/ir_rule.xml](security/ir_rule.xml) 中可以設定更細的權限管理\n\n```xml\n<record id=\"ir_rule_demo_expense_user\" model=\"ir.rule\">\n    <field name=\"name\">Demo Expense User</field>\n    <field name=\"model_id\" ref=\"model_demo_expense_tutorial\"/>\n    <field name=\"domain_force\">[('employee_id.user_id.id', '=', user.id)]</field>\n    <field name=\"groups\" eval=\"[(4, ref('demo_expense_tutorial_group_user'))]\"/>\n    <!-- Groups (no group = global) -->\n    <!-- <field name=\"global\" eval=\"True\"/> -->\n    <field eval=\"0\" name=\"perm_unlink\"/>\n    <field eval=\"1\" name=\"perm_write\"/>\n    <field eval=\"1\" name=\"perm_read\"/>\n    <field eval=\"1\" name=\"perm_create\"/>\n</record>\n```\n\n預設的 rule 如果沒有特別設定權限, CRUD 都會是 true,\n\n但也可以去分別設定 (如上教學),\n\n像這邊給了 read, write, create 的權限 (沒給 delete 權限)\n\n![alt tag](https://i.imgur.com/M3SFyFS.png)\n\n`<field name=\"global\" eval=\"True\"/>` 則代表 global,\n\n基本上, no group = global.\n\n### odoo 手把手教學 - 如何透過 button 呼叫 view, form - part6\n\n接下來介紹前面跳過的部份, 也就是透過 button 的方式呼叫 view, form,\n\n* [Youtube Tutorial - odoo 手把手教學 - 如何透過 button 呼叫 view, form - part6](https://youtu.be/URxuH2HG44Q)\n\n[models/models.py](models/models.py)\n\n```python\nclass DemoExpenseTutorial(models.Model):\n    _name = 'demo.expense.tutorial'\n    _description = 'Demo Expense Tutorial'\n    ......\n\n    @api.multi\n    def button_sheet_id(self):\n        return {\n            'view_mode': 'form',\n            'res_model': 'demo.expense.sheet.tutorial',\n            'res_id': self.sheet_id.id,\n            'type': 'ir.actions.act_window'\n        }\n```\n\n透過前端呼叫 `button_sheet_id`, 會回傳屬於它的 sheet_id\n\n![alt tag](https://i.imgur.com/gUUgk9g.png)\n\n點進去會直接進入 sheet 中的 form\n\n![alt tag](https://i.imgur.com/lU6P9Oj.png)\n\n[views/view.xml](views/view.xml)\n\n前端的部份就是呼叫 `button_sheet_id`\n\n```xml\n<record id=\"view_form_demo_expense_tutorial\" model=\"ir.ui.view\">\n  <field name=\"name\">Demo Expense Tutorial Form</field>\n  <field name=\"model\">demo.expense.tutorial</field>\n  <field name=\"arch\" type=\"xml\">\n    <form string=\"Demo Expense Tutorial\">\n      <sheet>\n        <div class=\"oe_button_box\" name=\"button_box\">\n          <button class=\"oe_stat_button\" name=\"button_sheet_id\"\n                  string=\"SHEET ID\" type=\"object\"\n                  attrs=\"{'invisible':[('sheet_id','=', False)]}\" icon=\"fa-bars\"/>\n        </div>\n        ......\n      </sheet>\n    </form>\n  </field>\n</record>\n```\n\n既然找了 sheet_id, 也來做一個反查回來的, 也就是透過 sheet_id 找到 `demo.expense.tutorial`,\n\n[models/models.py](models/models.py)\n\n```python\nclass DemoExpenseSheetTutorial(models.Model):\n    _name = 'demo.expense.sheet.tutorial'\n    _description = 'Demo Expense Sheet Tutorial'\n\n    ......\n\n    @api.multi\n    def button_line_ids(self):\n        return {\n            'name': 'Demo Expense Line IDs',\n            'view_type': 'form',\n            'view_mode': 'tree,form',\n            'res_model': 'demo.expense.tutorial',\n            'view_id': False,\n            'type': 'ir.actions.act_window',\n            'domain': [('sheet_id', '=', self.id)],\n        }\n\n    ......\n```\n\n`res_model` 為目標的 model `demo.expense.tutorial`.\n\n`domain` 稍微說明一下 `[('sheet_id', '=', self.id)],`,\n\n`sheet_id` 是指目標 model `demo.expense.tutorial` 的 sheet_id,\n\n`self.id` 是指當下 model `demo.expense.sheet.tutorial` 的 id.\n\n![alt tag](https://i.imgur.com/oqDOi3p.png)\n\n點下去會帶出它的 `demo.expense.tutorial`\n\n![alt tag](https://i.imgur.com/2ZSl9Qj.png)\n\n[views/view.xml](views/view.xml) 的部份\n\n```xml\n......\n<record id=\"view_form_demo_expense_sheet_tutorial\" model=\"ir.ui.view\">\n  <field name=\"name\">Demo Expense Sheet Tutorial Form</field>\n  <field name=\"model\">demo.expense.sheet.tutorial</field>\n  <field name=\"arch\" type=\"xml\">\n    <form string=\"Demo Expense Sheet Tutorial\">\n      <sheet>\n        <div class=\"oe_button_box\" name=\"button_box\">\n          <button class=\"oe_stat_button\" name=\"button_line_ids\"\n                  string=\"SHEET IDs\" type=\"object\"\n                  attrs=\"{'invisible':[('expense_line_ids','=', False)]}\" icon=\"fa-bars\"/>\n        </div>\n        ......\n      </sheet>\n    </form>\n  </field>\n</record>\n```\n\n### odoo 手把手教學 - 說明 name_get 和 _name_search - part7\n\n最後來看 [models/models.py](models/models.py) 中比較特殊的部份,\n\n* [Youtube Tutorial - odoo 手把手教學 - 說明 name_get 和 _name_search - part7](https://youtu.be/g-dclCkwY5c)\n\n分別是 `name_get` 和 `_name_search`,\n\n```python\nclass DemoExpenseSheetTutorial(models.Model):\n    _name = 'demo.expense.sheet.tutorial'\n    _description = 'Demo Expense Sheet Tutorial'\n\n    ......\n\n    @api.multi\n    def name_get(self):\n        names = []\n        for record in self:\n            name = '%s-%s' % (record.create_date.date(), record.name)\n            names.append((record.id, name))\n        return names\n\n    # odoo12/odoo/odoo/addons/base/models/ir_model.py\n    @api.model\n    def _name_search(self, name='', args=None, operator='ilike', limit=100):\n        if args is None:\n            args = []\n        domain = args + ['|', ('id', operator, name), ('name', operator, name)]\n        # domain = args + [ ('name', operator, name)]\n        # domain = args + [ ('id', operator, name)]\n        return super(DemoExpenseSheetTutorial, self).search(domain, limit=limit).name_get()\n\n```\n\n首先是 `name_get`\n\n這個的功能主要是去修改 name 的名稱, 在這邊我們加上當下的時間\n\n(可以依照自己的需求下去修改)\n\n![alt tag](https://i.imgur.com/JudV8pW.png)\n\nMany2one 時也會看到自己定義的 `name_get`\n\n注意 :exclamation: 這些增加的值是不會儲存進 db 中的, db 中還是儲存的是 name 的內容而已\n(概念和 compute field 一樣 :smile:)\n\n![alt tag](https://i.imgur.com/sC9hNA8.png)\n\n再來要來說明 `_name_search`,\n\n如果沒有它, 假設我知道某個資料的 id 是 4, 在搜尋的地方打上 id,\n\n你會發現找不到資料 :joy:\n\n![alt tag](https://i.imgur.com/YokDfBf.png)\n\n但今天如果有了 `_name_search` 並實作它,\n\n你會發現這次你打 id 會才成功找到需要的資料 :satisfied:\n\n我在 code 中有放幾個範例註解, 大家可以自行玩玩看 :smile:\n\n![alt tag](https://i.imgur.com/ztUL9Xd.png)\n\n### odoo 手把手教學 - 使用 python 增加取代 One2many M2X record - part8\n\n* [Youtube Tutorial - odoo 手把手教學 - 使用 python 增加取代 One2many M2X record - part8](https://youtu.be/GBCGS2znnT8)\n\n參考 [models/models.py](models/models.py)\n\n這邊只需要注意3個 function,\n\n`add_demo_expense_record` `link_demo_expense_record` `replace_demo_expense_record`\n\n分別對應的 button 為下圖\n\n參考 [views/view.xml](views/view.xml)\n\n![alt tag](https://i.imgur.com/8gmMe3j.png)\n\n```python\nclass DemoExpenseSheetTutorial(models.Model):\n    _name = 'demo.expense.sheet.tutorial'\n    _description = 'Demo Expense Sheet Tutorial'\n\n    name = fields.Char('Expense Demo Report Summary', required=True)\n\n    # One2many is a virtual relationship, there must be a Many2one field in the other_model,\n    # and its name must be related_field\n    expense_line_ids = fields.One2many(\n        'demo.expense.tutorial', # related model\n        'sheet_id', # field for \"this\" on related model\n        string='Expense Lines')\n\n    @api.multi\n    def add_demo_expense_record(self):\n        # (0, _ , {'field': value}) creates a new record and links it to this one.\n\n        data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')\n\n        tag_data_1 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_1')\n        tag_data_2 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_2')\n\n        for record in self:\n            # creates a new record\n            val = {\n                'name': 'test_data',\n                'employee_id': data_1.employee_id,\n                'tag_ids': [(6, 0, [tag_data_1.id, tag_data_2.id])]\n            }\n\n            self.expense_line_ids = [(0, 0, val)]\n\n    @api.multi\n    def link_demo_expense_record(self):\n        # (4, id, _) links an already existing record.\n\n        data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')\n\n        for record in self:\n            # link already existing record\n            self.expense_line_ids = [(4, data_1.id, 0)]\n\n    @api.multi\n    def replace_demo_expense_record(self):\n        # (6, _, [ids]) replaces the list of linked records with the provided list.\n\n        data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')\n        data_2 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_2')\n\n        for record in self:\n            # replace multi record\n            self.expense_line_ids = [(6, 0, [data_1.id, data_2.id])]\n\n```\n\n說明 `add_demo_expense_record`\n\n```python\n......\n@api.multi\ndef add_demo_expense_record(self):\n    # (0, _ , {'field': value}) creates a new record and links it to this one.\n\n    data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')\n\n    tag_data_1 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_1')\n    tag_data_2 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_2')\n\n    for record in self:\n        # creates a new record\n        val = {\n            'name': 'test_data',\n            'employee_id': data_1.employee_id,\n            'tag_ids': [(6, 0, [tag_data_1.id, tag_data_2.id])]\n        }\n\n        self.expense_line_ids = [(0, 0, val)]\n......\n```\n\n`(0, _ , {'field': value})` 新建一筆 record 並且連接它.\n\n`self.env.ref(......)` 這個的用法是去取得既有的資料, 路徑在 [data/demo_expense_tutorial_data.xml](data/demo_expense_tutorial_data.xml).\n\n當你點選按鈕, 下面就會一直新增資料\n\n![alt tag](https://i.imgur.com/bUI3vZE.png)\n\n說明 `link_demo_expense_record`\n\n```python\n......\n@api.multi\ndef link_demo_expense_record(self):\n    # (4, id, _) links an already existing record.\n\n    data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')\n\n    for record in self:\n        # link already existing record\n        self.expense_line_ids = [(4, data_1.id, 0)]\n......\n\n```\n\n`(4, id, _)` 連接已經存在的 record.\n\n當你點選按鈕, 下面會直接連接一比資料, 如果已經連接就不會有動作,\n\n![alt tag](https://i.imgur.com/Qw1VDyU.png)\n\n說明 `replace_demo_expense_record`\n\n```python\n......\n@api.multi\ndef replace_demo_expense_record(self):\n    # (6, _, [ids]) replaces the list of linked records with the provided list.\n\n    data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')\n    data_2 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_2')\n\n    for record in self:\n        # replace multi record\n        self.expense_line_ids = [(6, 0, [data_1.id, data_2.id])]\n......\n```\n\n`(6, _, [ids])` 使用 list 取代既有的 records.\n\n當你點選按鈕, 會使用你定義的 list 取代全部的 records.\n\n![alt tag](https://i.imgur.com/b30QdSQ.png)\n\n### odoo 手把手教學 - tree create delete edit False - part9\n\n* [Youtube Tutorial - odoo 手把手教學 - tree create delete edit False - part9](https://youtu.be/0fpA89QcYZM)\n\n通常管理一個使用者可不可以建立 records, 是根據 security 資料夾裡面的檔案,\n\n也就是 `security.xml` `ir_rule.xml` `ir.model.access.csv`.\n\n記住 :exclamation: odoo 可以從 model 層(db層) 或權限下手, 也可以從 view 那層下手,\n\n當然, 如果是從安全性的角度來看 從 model 層(db層) 或權限下手 是比較高全的 :smile:\n\n今天就是要來介紹 從 view 那層下手,\n\n增加一個 tree [views/view.xml](views/view.xml)\n\n```xml\n......\n<record id=\"view_tree_demo_expense_tutorial_no_create\" model=\"ir.ui.view\">\n  <field name=\"name\">Demo Expense Tutorial List No Create</field>\n  <field name=\"model\">demo.expense.tutorial</field>\n  <field name=\"arch\" type=\"xml\">\n    <tree string=\"no_create_tree\" create=\"0\" delete=\"false\" edit=\"1\" editable=\"top\">\n      <field name=\"name\"/>\n      <field name=\"employee_id\"/>\n    </tree>\n  </field>\n</record>\n......\n```\n\n重點在 `<tree string=\"no_create_tree\" create=\"0\" delete=\"false\" edit=\"1\" editable=\"top\">`\n\n這段, 裡面增加了一下 tag, 允許就是 `1` 或 `True`, 不允許就是 `0` 或 `False`.\n\n儘管你有權限建立 records, 如果你設定了 `create=\"0\"`, 你還是沒辦法建立 records.\n\n也記得在 [views/menu.xml](views/menu.xml) 增加 action,\n\n並且要指定 `view_id` (也就是剛剛建立出來的那個)\n\n```xml\n......\n<!-- Action to open the demo_expense_tutorial_no_craete -->\n<record id=\"action_expense_tutorial_no_craete\" model=\"ir.actions.act_window\">\n    <field name=\"name\">Demo Expense Tutorial Action No Craete</field>\n    <field name=\"res_model\">demo.expense.tutorial</field>\n    <field name=\"view_type\">form</field>\n    <field name=\"view_mode\">tree</field>\n    <field name=\"view_id\" ref=\"view_tree_demo_expense_tutorial_no_create\"/>\n</record>\n......\n```\n\n你會發現 create delete 的按鈕都消失了\n\n![alt tag](https://i.imgur.com/siLhdQ4.png)\n\n### odoo 手把手教學 - 同一個 model 使用不同的 view_ids - part10\n\n* [Youtube Tutorial - odoo 手把手教學 - 同一個 model 使用不同的 view_ids - part10](https://youtu.be/YltcAu9OZhc)\n\n一般來說, 在定義一個 model 時, 通常會搭配一個 form 的 view 以及 tree 的 view, 或是特別指定一個 view,\n\n像是前面介紹到的 view_id, 但有時候會有這種情況, 也就是一個 model, 在兩個不同的地方, 分別顯示不同的\n\nform 的 view 以及 tree 的 view, 這時候就要使用 view_ids 分別下去定義.\n\n現在 meun 上會多出 Demo Expense Tutorial View ids, 點下去分別有\n\nDemo Expense Tutorial View id 1 以及 Demo Expense Tutorial View id 2\n\n他們都是屬於 `demo.expense.tutorial` model, 只不過使用了不同的 view 和 form,\n\n![alt tag](https://i.imgur.com/fuMniZM.png)\n\n為了方便區分不同的 form 和 view, 簡單用 fields 的排序不同\n\nview 1\n\n![alt tag](https://i.imgur.com/l6En89u.png)\n\nview 2\n\n![alt tag](https://i.imgur.com/lSKNnde.png)\n\n可參考 [views/menu.xml](views/menu.xml)\n\n```xml\n    ......\n    <!-- Action to open the menu_expense_tutorial_view_id_1 -->\n    <record id=\"action_expense_tutorial_view_id_1\" model=\"ir.actions.act_window\">\n        <field name=\"name\">Demo Expense Tutorial Action View id 1</field>\n        <field name=\"res_model\">demo.expense.tutorial</field>\n        <field name=\"view_type\">form</field>\n        <field name=\"view_mode\">tree,form</field>\n        <field name=\"view_ids\" eval=\"[(5, 0, 0),\n            (0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.tree_expense_view_id_1')}),\n            (0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.form_expense_view_id_1')})]\"/>\n    </record>\n\n    ......\n\n    <!-- Action to open the menu_expense_tutorial_view_id_2 -->\n    <record id=\"action_expense_tutorial_view_id_2\" model=\"ir.actions.act_window\">\n        <field name=\"name\">Demo Expense Tutorial Action View id 2</field>\n        <field name=\"res_model\">demo.expense.tutorial</field>\n        <field name=\"view_type\">form</field>\n        <field name=\"view_mode\">tree,form</field>\n        <field name=\"view_ids\" eval=\"[(5, 0, 0),\n            (0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.tree_expense_view_id_2')}),\n            (0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.form_expense_view_id_2')})]\"/>\n    </record>\n\n    ......\n```\n\n看起來雖然很複雜, 但其實不難, 設定都和之前的一樣, 只是將 view_id 換成了 view_ids, 然後分別設定不同的 view_id,\n\n這邊只有分別設定 tree 和 form, 如果你想要定義新的 kanban 或其他的 view_mode 也都是可以的.\n\n`eval=\"[(5, 0, 0)` 的意思是清除所有和它有關的 record (因為我們重新定義了需要的 view),\n\n相關說明可參考\n\n```xml\n(0, 0,  { values })    link to a new record that needs to be created with the given values dictionary\n(1, ID, { values })    update the linked record with id = ID (write *values* on it)\n(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)\n(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)\n(4, ID)                link to existing record with id = ID (adds a relationship)\n(5)                    unlink all (like using (3,ID) for all linked records)\n(6, 0, [IDs])          replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)\n```\n\n你也可以在 Technical -> Actions -> Window Actions 看到你所設定的 view_ids\n\n![alt tag](https://i.imgur.com/JKSpKwQ.png)\n\n這些就是剛剛的設定\n\n![alt tag](https://i.imgur.com/GmYShsn.png)\n\n這邊補充一個小技巧, 在定義 view 時, 有一個參數是  `<field name=\"priority\" eval=\"1\"/>`, 如果你只有一個 view 不需要特別設定,\n\n但如果你有很多個, 你可以透過這個 priority 去決定顯示 view 的優先權.\n\n### odoo 手把手教學 - widget 介紹 handle 和 many2onebutton - part11\n\n* [Youtube Tutorial - odoo 手把手教學 - widget 介紹 handle 和 many2onebutton - part11](https://youtu.be/zb5fSEtEo_g)\n\n在 odoo 中很非常多的 widget 可以使用, 除了像前面介紹的 `widget=\"many2many_tags\"` 之外, 這邊再介紹另外兩個,\n\n首先是 handle widget,\n\n這個比較常和 sequence 搭配一起使用,\n\n可參考 [models/models.py](models/models.py)\n\n```python\nclass DemoExpenseTutorial(models.Model):\n    _name = 'demo.expense.tutorial'\n    _description = 'Demo Expense Tutorial'\n    _order = \"sequence, id desc\"\n\n    ......\n\n    sequence = fields.Integer(index=True, help=\"Gives the sequence order\", default=1)\n```\n\n定義了一個 sequence fields, 然後排序使用 sequence.\n\n在 tree view 中加入 `widget=\"handle\"`,\n\n可參考 [views/view.xml](views/view.xml)\n\n```xml\n\n  <record id=\"view_tree_demo_expense_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial List</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"priority\" eval=\"1\"/>\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n      <!-- <tree default_order=\"sequence, id desc\"> -->\n        <field name=\"sequence\" widget=\"handle\"/>\n        ......\n      </tree>\n    </field>\n  </record>\n\n```\n\n這邊補充一下, 除了在 model 中定義 order 之外, 也可以在 tree, kanban 上定義,\n\n像是 `<tree default_order=\"sequence, id desc\">`.\n\n這樣就完成了, 你會發現 tree 可以排序了 :smile:\n\n![alt tag](https://i.imgur.com/oH7Pf5S.png)\n\n再來是 many2onebutton widget,\n\n通常如果一個 tree view 上有 many2one 的 fields, 如果想看這個 fields 的資料,\n\n必須要點進去 form, 再點進去 many2one 的 fields 才看的到資料,\n\n如果在 tree 的 many2one fields 上加入 `widget=\"many2onebutton\"`,\n\n就可以直接點進去觀看該 fields 的資料.\n\n可參考 [views/view.xml](views/view.xml)\n\n```xml\n<record id=\"view_tree_demo_expense_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial List</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"priority\" eval=\"1\"/>\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n        ......\n        <field name=\"sheet_id\" widget=\"many2onebutton\"/>\n      </tree>\n    </field>\n  </record>\n```\n\n你會發現 many2one fields 變藍色的了, 直接點選即可.\n\n![alt tag](https://i.imgur.com/Ck37blB.png)\n\n### odoo 手把手教學 - view 搭配 context - part12\n\n* [Youtube Tutorial - odoo 手把手教學 - view 搭配 context - part12](https://youtu.be/c-nzbAuaH9I)\n\n這部份將介紹 view 搭配 context 的使用,\n\n可參考 [views/menu.xml](views/menu.xml)\n\n```xml\n<!-- Action to open the demo_expense_tutorial_context -->\n    <record id=\"action_expense_tutorial_context\" model=\"ir.actions.act_window\">\n        <field name=\"name\">Demo Expense Tutorial Action Context</field>\n        <field name=\"res_model\">demo.expense.tutorial</field>\n        <field name=\"view_type\">form</field>\n        <field name=\"view_mode\">tree,form</field>\n        <field name=\"domain\">[]</field>\n\n        <!-- init search default -->\n        <field name=\"context\">{'search_default_name': 'test123'}</field>\n\n        <!-- init create default name 'test123'-->\n        <!-- <field name=\"context\">{'default_name': 'test123'}</field> -->\n    </record>\n```\n\n來說明一下 `<field name=\"context\">{'search_default_name': 'test123'}</field>`\n\n這段程式碼, `name` 是我定義的 fields, 格式是 `search_default + fields`,\n\n也就是進入這個 view 的時候, 預設會幫你搜尋 `name` 吻合 `test123`.\n\n![alt tag](https://i.imgur.com/9Pn9Ya3.png)\n\n接著來看另一個, `<field name=\"context\">{'default_name': 'test123'}</field>`\n\n這段程式碼, 格式是 `default + fields`, 注意哦, 這次沒有 search,\n\n那這個和剛剛的有什麼不同呢 :question:\n\n當你建立一個 records 的時候, 他預設會幫你的 `name` fields 自動帶入 `test123`.\n\n![alt tag](https://i.imgur.com/AKjdOqQ.png)\n\ncontext 也可以在 developer mode 中的 Edit Action 看到,\n\n![alt tag](https://i.imgur.com/evOb5Zz.png)\n\n### odoo 手把手教學 - view 搭配 active_test context - part13\n\n* [Youtube Tutorial - odoo 手把手教學 - view 搭配 active_test context - part13](https://youtu.be/RR9ycgky444)\n\n這部份延續上一次的介紹, 來看看 `active_test` 這個東西,\n\n這部份建議大家看影片會比較清楚 :smile:\n\n先來看 [models/models.py](models/models.py)\n\n```python\nclass DemoExpenseTutorial(models.Model):\n    _name = 'demo.expense.tutorial'\n    _description = 'Demo Expense Tutorial'\n    _order = \"sequence, id desc\"\n\n    ......\n    active = fields.Boolean(default=True, help=\"Set active.\")\n```\n\n通常如果有定義 active fields,\n\n預設的情況下, 你的 view 就是會顯示只有 `active=True` 的 records,\n\n如果你要顯示 `active=False` 的 records,\n\n則必須另外去 filter 出來, 如下圖\n\n![alt tag](https://i.imgur.com/GTpo0aa.png)\n\n這樣你可能會問我, 為什麼會這樣呢 :question:\n\n原因是 odoo 原始碼內的 `odoo/models.py` 這段\n\n```python\n@api.model\ndef _where_calc(self, domain, active_test=True):\n  ......\n  if 'active' in self._fields and active_test and self._context.get('active_test', True):\n      # the item[0] trick below works for domain items and '&'/'|'/'!'\n      # operators too\n      if not any(item[0] == 'active' for item in domain):\n          domain = [('active', '=', 1)] + domain\n```\n\n預設如果沒有特別指定, 邏輯就是會跑 `active = 1` 也就是 True.\n\n那如果我今天希望預設顯示 active 為 True 和 False 同時都顯示, 這樣要如何實作 :question:\n\n搭配 `<field name=\"context\">{'active_test':False}</field>` 這段程式碼,\n\n可參考 [views/menu.xml](views/menu.xml)\n\n```xml\n<!-- Action to open the demo_expense_tutorial_test_active -->\n  <record id=\"action_expense_tutorial_test_active\" model=\"ir.actions.act_window\">\n      <field name=\"name\">Demo Expense Tutorial Action Test Active</field>\n      <field name=\"res_model\">demo.expense.tutorial</field>\n      <field name=\"view_type\">form</field>\n      <field name=\"view_mode\">tree,form</field>\n      <field name=\"domain\">[]</field>\n\n      <!-- init show all (active True False) record -->\n      <field name=\"context\">{'active_test':False}</field>\n\n      <!-- init show only (active True) record -->\n      <!-- <field name=\"context\">{}</field> -->\n  </record>\n```\n\n這樣子預設就會把全部的 records (不管 active 狀態) 都顯示出來.\n\ncontext 同樣也可以在 developer mode 中的 Edit Action 看到,\n\n![alt tag](https://i.imgur.com/vtaVxD9.png)\n\n### odoo 手把手教學 - view 搭配 domain - part14\n\n* [Youtube Tutorial - odoo 手把手教學 - view 搭配 domain - part14](https://youtu.be/Rh-rmXIHTZo)\n\n這部份將介紹 view 搭配 domain 的使用,\n\n使用方法和 context 差不多 :smile:\n\n可參考 [views/menu.xml](views/menu.xml)\n\n```xml\n  <!-- Action to open the demo_expense_tutorial_domain -->\n  <record id=\"action_expense_tutorial_domain\" model=\"ir.actions.act_window\">\n      <field name=\"name\">Demo Expense Tutorial Action Domain</field>\n      <field name=\"res_model\">demo.expense.tutorial</field>\n      <field name=\"view_type\">form</field>\n      <field name=\"view_mode\">tree,form</field>\n      <field name=\"domain\">[('name', 'like', 'test')]</field>\n      <field name=\"context\">{}</field>\n  </record>\n```\n\n說明 `<field name=\"domain\">[('name', 'like', 'test')]</field>` 這段程式碼,\n\n只會顯示 name fields like test 的內容,\n\n注意 :exclamation: 和 `search_default_name` 不一樣的地方是, 他不會顯示 search 的東西,\n\n使用者也不能自行修改\n\n![alt tag](https://i.imgur.com/GMCdOBK.png)\n\ndomain 同樣也可以在 developer mode 中的 Edit Action 看到,\n\n![alt tag](https://i.imgur.com/OGdJ5jt.png)\n\n### odoo 手把手教學 - 如何看到當下 view 繼承頁面 - part15\n\n* [Youtube Tutorial - odoo 手把手教學 - 如何看到當下 view 繼承頁面 - part15](https://youtu.be/Vs6ScbYuZNs)\n\n有時候當我們寫了很多的繼承 ( tree 或 form), 在當下的頁面, 會不知道是否有被繼承過,\n\n這時候推薦大家一個小技巧 :smile:\n\n使用 [demo_class_inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_class_inheritance) 這個 addons 當作範例.\n\n首先, 先進去你想要查看的頁面, 這邊進入 hr_expnese\n\n![alt tag](https://i.imgur.com/4jotzYG.png)\n\ndebug 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)\n\n點選 Edit View: List\n\n![alt tag](https://i.imgur.com/yBwoOTa.png)\n\n點選 Inherited Views 這個 tab\n\n![alt tag](https://i.imgur.com/53oTTuu.png)\n\n你就可以很清楚的看到這個頁面被 `demo_class_inheritance` 繼承 :smile:\n\n像是 form 或其他的 view_type 也都是同樣的方法哦 :smirk:\n\n### odoo 手把手教學 - odoo rainbow - part16\n\n* [Youtube Tutorial - odoo 手把手教學 - odoo rainbow - part16](https://youtu.be/g4vywRLklE0)\n\n在 odoo 中也有特效這個東西\n\n```python\n@api.multi\ndef button_rainbow_man(self):\n    return {\n        'effect': {\n            'fadeout': 'slow',\n            'message': 'hello',\n            'type': 'rainbow_man',\n        }\n    }\n```\n\n![alt tag](https://i.imgur.com/mn2hmox.png)\n\n### odoo 手把手教學 - tree decoration - part17\n\n* [Youtube Tutorial - odoo 手把手教學 - tree decoration - part17](https://youtu.be/tJdw6IEb8UQ)\n\n在 odoo 中有很多的 decoration 可以使用, 通常是搭配 tree 顯示特殊的資料.\n\n使用方法非常的簡單, 直接加上需要顯示的邏輯即可,\n\n可參考 [views/view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/views/view.xml)\n\n```xml\n......\n  <record id=\"view_tree_demo_expense_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial List</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"priority\" eval=\"1\"/>\n    <field name=\"arch\" type=\"xml\">\n      <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\">\n        ......\n      </tree>\n    </field>\n  </record>\n......\n```\n\n![alt tag](https://i.imgur.com/G6HLRhv.png)\n\n關於 `decoration-{$name}` 的詳細說明, 可參考官方文件 [Advanced Views](https://www.odoo.com/documentation/12.0/developer/howtos/backend.html#advanced-views)\n\n### odoo 手把手教學 - model _rec_name 說明 - part18\n\n* [Youtube Tutorial - odoo 手把手教學 - model _rec_name 說明 - part18](https://youtu.be/JtcSnbHNjAU)\n\n今天要和大家介紹在 model 中有時會看到的 `_rec_name`,\n\n[models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py)\n\n```python\n......\nclass DemoTag(models.Model):\n    _name = 'demo.tag'\n    _description = 'Demo Tags'\n    _rec_name = 'complete_name'\n\n    name = fields.Char(string='Tag Name', index=True, required=True)\n    complete_name = fields.Char('Complete Name', compute='_compute_complete_name')\n    active = fields.Boolean(default=True, help=\"Set active.\")\n\n    @api.depends('name')\n    def _compute_complete_name(self):\n        for record in self:\n            record.complete_name = 'hello world - {}'.format(record.name)\n......\n```\n\n首先, 你需要知道一件事, 如果你建立一個 model, 該 model 沒有特別定義 `name`, 且沒另外指定 `_rec_name`\n\n(如果沒特別指定 `_rec_name`, default 就是使用 `name`)\n\n通常這時候 odoo 的訊息會提醒你建議你設定 `name` field 或是指定 `_rec_name`.\n\n![alt tag](https://i.imgur.com/Pm4ggmV.png)\n\n但這只是 WARNING, 你也可以不要理他.\n\n但我的建議是, 如果你不指定 `name`, 就請特別去指定 `_rec_name`.\n\n當然, 如果你有設定了 `name`, 你也可以特別去指定 `_rec_name` 為其他的 field.\n\n這個 `name` 和 `_rec_name` 只是指定顯示的名稱而已.\n\n詳細的 demo 差異可以看影片的說明.\n\n### odoo 手把手教學 - copy override 說明 - part19\n\n* [Youtube Tutorial - odoo 手把手教學 - copy override 說明 - part19](https://youtu.be/VDnIFb7e7wM)\n\n在 odoo 中有幾個比較特殊的 function, 分別是 `create` `write` `copy` `unlink`,\n\n`create` 建立一比 record 時.\n\n`write`  更新一比 record 時.\n\n`copy`   複製一比 record 時.\n\n`unlink` 刪除一比 record 時.\n\n今天來介紹 `copy` 當作範例, 其他的大家可以以此類推 :smile:\n\n```python\nclass DemoExpenseTutorial(models.Model):\n    _name = 'demo.expense.tutorial'\n    _description = 'Demo Expense Tutorial'\n    _order = \"sequence, id desc\"\n\n    name = fields.Char('Description', required=True)\n    ......\n\n    tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id', string='Tges', copy=False)\n\n......\n\n    @api.multi\n    def copy(self, default=None):\n        default = dict(default or {})\n        if not default.get('name'):\n            default['name'] = '{} copy'.format(self.name)\n        return super(DemoExpenseTutorial, self).copy(default)\n```\n\n當點選 Duplicate 時, 會觸發這個 `copy`\n\n(在這邊做的事情是 override, 和之前介紹的繼承觀念其實是差不多的)\n\n![alt tag](https://i.imgur.com/44USuhK.png)\n\n你會發現 name 被加上 copy 了.\n\n![alt tag](https://i.imgur.com/mGenCa7.png)\n\n然後預設的 field 是 `copy=True`, 如果不想要他被 copy, 可以直接在 fields\n\n上設定 `copy=False`.\n\n最後要提醒大家, 在 odoo 中少用/小心使用 duplicate, 不然就是你要非常清楚裡面寫了甚麼,\n\n因為我很常用到複製出來的 record 有問題, 可能是翻譯, 又可能是複製出來的這比很奇怪,\n\n唯一的可能就是他的 copy 沒有寫好, 特殊的邏輯沒有補上去, 導致你複製出來的 record 行為很怪.\n\n基本上在要修改 `create` `write` `copy` `unlink` 時, 可以先想想有沒有比較簡單的方式能\n\n改動你的需求, 如果真的沒有, 才選擇改他 :smirk:\n\n## odoo 手把手教學 - move position 說明 - part20\n\n* [Youtube Tutorial - odoo 手把手教學 - move position 說明 - part20](https://youtu.be/l-bFOqTYgTA)\n\n在 odoo 中常常容易使用到繼承的方式改寫 view, 最常見的 position 就是 after, before, replace,\n\n但有時候會有一種狀況, 就是單純想要交換兩個既有的 fields 位置 (不使用直接在 odoo 上面改, 不推薦),\n\n例如, 下面這個已經存在的 view, 希望交換 Employee 和 Description 的位置,\n\n![alt tag](https://i.imgur.com/EsmRtpu.png)\n\n假設這個 view 只能使用繼承的方式修改, 這時候通常很麻煩, 因為有可能你的作法是把整個 tree 去 replace 掉,\n\n再自己去排版, 但為了幾個 fields 就去 replace 掉整個 view, 真得有點麻煩 :expressionless:\n\n所以, 今天來認識 move position 這個東西, 寫法可參考 [view.xml](views/view.xml)\n\n```xml\n<!-- change name, employee_id fields-->\n<record id=\"view_tree_demo_expense_tutorial_move\" model=\"ir.ui.view\">\n  <field name=\"name\">view_tree_demo_expense_tutorial_move</field>\n  <field name=\"model\">demo.expense.tutorial</field>\n  <field name=\"inherit_id\" ref=\"demo_expense_tutorial_v1.view_tree_demo_expense_tutorial\"/>\n  <field name=\"arch\" type=\"xml\">\n      <xpath expr=\"//field[@name='name']\" position=\"before\">\n        <field name=\"employee_id\" position=\"move\"/>\n      </xpath>\n  </field>\n</record>\n```\n\n基本上就是在要移動的 fields 上的後面加上 `position=\"move\"` 即可, 效果如下\n\n![alt tag](https://i.imgur.com/Gek9iqM.png)\n\n有了這個東西, 以後單純想要交換兩個 fields, 只需要使用 move 即可, 不需要再整個 replace 了 :smile:\n\n## odoo 手把手教學 - ir.actions.act_url 說明 - part21\n\n這部份很簡單, 只是要和大家說可以透過 `ir.actions.act_url` 來開始 url\n\n可參考 [models/models.py](models/models.py)\n\n```python\n@api.multi\ndef button_act_url(self):\n    self.ensure_one()\n    return {\n        'type': 'ir.actions.act_url',\n        'target': 'new',\n        # 'target': 'self',\n        'url': 'https://github.com/twtrubiks/odoo-demo-addons-tutorial',\n    }\n```\n\n## odoo 手把手教學 - Smart Button 說明 - part22\n\n* [Youtube Tutorial - odoo 手把手教學 - Smart Button 說明 - part22](https://youtu.be/fsZK1KRgnF0)\n\n甚麼是 Smart Button :question: 如果你常用 odoo, 你一定常看到這個東西,\n\n如下, 這就是所謂的 Smart Button\n\n![alt tag](https://i.imgur.com/Wu1l4Mg.png)\n\n其實之前就有介紹過 Smart Button 了, 今天就再順便說明要如何設計底下的那個數字\n\n首先, 你需要透過 compute 這個參數建立一個 fields (用來計算出這個數字)\n\n[models/models.py](models/models.py)\n\n```python\n......\ndemo_expenses_count = fields.Integer(\n      compute='_compute_demo_expenses_count',\n      string='Demo Expenses Count')\n\n......\n\ndef _compute_demo_expenses_count(self):\n    # usually used read_group\n    for record in self:\n        record.demo_expenses_count = len(self.expense_line_ids)\n......\n```\n\n在這邊只是簡單的算出數量而已(為了demo), 註解在這邊的意思是說, 通常都會使用 read_group\n\n來做計算 (可自行參考 odoo source code)\n\n最後, 自行將這個 fields 放到 view 中即可, 可參考 [views/view.xml](views/view.xml)\n\n```xml\n......\n<div class=\"oe_button_box\" name=\"button_box\">\n  <button class=\"oe_stat_button\"\n          name=\"button_line_ids\"\n          type=\"object\"\n          attrs=\"{'invisible':[('expense_line_ids','=', False)]}\"\n          icon=\"fa-bars\">\n          <field name=\"demo_expenses_count\" widget=\"statinfo\" string=\"Counts\"/>\n  </button>\n</div>\n......\n```\n\n效果如下\n\n![alt tag](https://i.imgur.com/62zKchb.png)\n\n## odoo 手把手教學 - options create_edit 說明 - part23\n\n* [Youtube Tutorial - odoo 手把手教學 - options create_edit 說明 - part23](https://youtu.be/GdPKllI7quI)\n\n今天要來介紹 `options` 這個參數,\n\n請參考 [view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/views/view.xml)\n\n```xml\n  <record id=\"view_form_demo_expense_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial Form</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Expense Tutorial\">\n        ......\n            <field name=\"name\"/>\n            <field name=\"employee_id\"/>\n            <!-- <field name=\"employee_id\" options=\"{'no_quick_create': True}\"/> -->\n            <!-- <field name=\"employee_id\" options=\"{'no_create_edit': True}\"/> -->\n            <!-- <field name=\"employee_id\" options=\"{'no_create': True}\"/> -->\n            <!-- <field name=\"employee_id\" options=\"{'no_open': True}\"/> -->\n        ......\n      </form>\n    </field>\n  </record>\n```\n\n在一般的情況下 (不設定任何的 `option`), 顯示如下,\n\n![alt tag](https://i.imgur.com/pfQFJOp.png)\n\n![alt tag](https://i.imgur.com/OGES89X.png)\n\n以下是每一種 option 呈現的效果, 大家可以自行玩玩看 :smile:\n\n`no_quick_create`\n\n![alt tag](https://i.imgur.com/Sa2ClYv.png)\n\n`no_create_edit`\n\n![alt tag](https://i.imgur.com/B3OoDub.png)\n\n`no_create`\n\n![alt tag](https://i.imgur.com/Sa2ClYv.png)\n\n`no_open`\n\n![alt tag](https://i.imgur.com/I1tcv9o.png)\n\n當然, 如果你的需求是多個組合, 也可以多個一起使用.\n\n## odoo 手把手教學 - PostgreSQL ondelete cascade 說明 - part24\n\n* [Youtube Tutorial - odoo 手把手教學 - PostgreSQL ondelete cascade 說明 - part24](https://youtu.be/OTh5R2LrwJE)\n\n今天要介紹 Many2one fields 中的 `ondelete=\"cascade\"` 參數代表的意思,\n\n這邊要先說明一下, `ondelete='cascade'` 這個東西並不是 odoo 的, 它是 PostgreSQL 的特性 :exclamation:\n\n使用方法很簡單, 如下, 可參考 [models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py)\n\n```python\n......\nclass DemoExpenseTutorial(models.Model):\n    _name = 'demo.expense.tutorial'\n    ......\n    sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string=\"Expense Report\", ondelete='cascade')\n    ......\n\nclass DemoExpenseSheetTutorial(models.Model):\n    _name = 'demo.expense.sheet.tutorial'\n\n    ......\n    expense_line_ids = fields.One2many(\n        'demo.expense.tutorial', # related model\n        'sheet_id', # field for \"this\" on related model\n        string='Expense Lines')\n    ......\n```\n\n在開始介紹之前, 大致上除了 `'cascade'` 之外, 還有其他幾個選項, 如下分別是\n\n說明 `ondelete='set null'`\n\n這個是 default, 可參考 odoo `fields.py` 中的 Many2one 說明(如下).\n\n```python\n......\nclass Many2one(_Relational):\n    \"\"\" The value of such a field is a recordset of size 0 (no\n    ......\n\n    :param ondelete: what to do when the referred record is deleted;\n        possible values are: ``'set null'``, ``'restrict'``, ``'cascade'``\n\n    ......\n    \"\"\"\n    ......\n    _slots = {\n        'ondelete': 'set null',         # what to do when value is deleted\n        'auto_join': False,             # whether joins are generated upon search\n        'delegate': False,              # whether self implements delegation\n    }\n......\n```\n\n( 如果文章看的不是很清楚, 請參考影片中的說明, 我會一個一個說明 :sunglasses: )\n\n說明 `ondelete='set null'` (預設行為)\n\n如果直接刪除 `sheet_id` ( 底下有很多`expense_line_ids`), 可以成功刪除 `sheet_id`, 但你會發現\n\n`expense_line_ids` 並沒有被刪除 ( `sheet_id` 變為 `null`).\n\n說明 `ondelete='cascade'`\n\n如果直接刪除 `sheet_id` ( 底下有很多`expense_line_ids`), 可以成功刪除 `sheet_id`, 且你會發現\n\n`expense_line_ids` 也自動都被移除了.\n\n說明 `ondelete='restrict'`\n\n如果直接刪除 `sheet_id` ( 底下有很多`expense_line_ids`), 無法刪除 `sheet_id`,\n\n![alt tag](https://i.imgur.com/F42UMyz.png)\n\n你必需先移除 `sheet_id` 底下的 `expense_line_ids`, 才可以刪除 `sheet_id`.\n\n其實, 你可以把他們想成是 child 和 parent 的關係即可 :smile:\n\n要如何知道 fields 有 `ondelete='....'` 之類的特性呢 :question:\n\n除了可以透過 code 或 odoo 的 model fields 中查看之外,\n\n![alt tag](https://i.imgur.com/yaLqCTw.png)\n\n也可以利用查看 db table 的工具 (pgadmin4)\n\n![alt tag](https://i.imgur.com/YnRY3pX.png)\n\n## odoo 手把手教學 - view parent 說明 - part26\n\n* [Youtube Tutorial - odoo 手把手教學 - view parent 說明 - part26](https://youtu.be/i_hG4s_YJN0)\n\n在 view 中可以透過 `parent` 這個值, 拿到 `parent` 的 `fields` 內容 (可能有點繞口 :smile:)\n\n不懂沒關係, 請看以下的說明 :smile:\n\n通常會使用在 view 中的 domain 或是 attrs,\n\n首先, 先看一下 [models/models.py](models/models.py)\n\n```python\n......\nclass DemoExpenseTutorial(models.Model):\n    _name = 'demo.expense.tutorial'\n    _description = 'Demo Expense Tutorial'\n    _order = \"sequence, id desc\"\n\n    name = fields.Char('Description', required=True)\n    ......\n    sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string=\"Expense Report\", ondelete='restrict')\n\n......\n\nclass DemoExpenseSheetTutorial(models.Model):\n    _name = 'demo.expense.sheet.tutorial'\n    _description = 'Demo Expense Sheet Tutorial'\n\n    name = fields.Char('Expense Demo Report Summary', required=True)\n\n    # One2many is a virtual relationship, there must be a Many2one field in the other_model,\n    # and its name must be related_field\n    expense_line_ids = fields.One2many(\n        'demo.expense.tutorial', # related model\n        'sheet_id', # field for \"this\" on related model\n        string='Expense Lines')\n......\n```\n\n接著看 [views/view.xml](views/view.xml)\n\n```xml\n......\n  <record id=\"view_form_demo_expense_sheet_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Sheet Tutorial Form</field>\n    <field name=\"model\">demo.expense.sheet.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Expense Sheet Tutorial\">\n      ......\n          <group>\n            <field name=\"name\"/>\n          </group>\n          <notebook>\n              <page string=\"Expense\">\n                <field name=\"expense_line_ids\" >\n                  <!-- <tree> -->\n                  <tree editable=\"top\">   <!-- <<<<<<<<<<<< -->\n                  <!-- <tree editable=\"bottom\"> --> <!-- <<<<<<<<<<<< -->\n                    <field name=\"name\"/>\n                    <field name=\"employee_id\"/>\n                    <field name=\"tag_ids\" widget=\"many2many_tags\" attrs=\"{'readonly': [('parent.name', '=', 'test-readonly')]}\"/>\n                  </tree>\n                </field>\n              </page>\n          </notebook>\n        </sheet>\n      </form>\n    </field>\n  </record>\n......\n```\n\n主要請看 `<field name=\"tag_ids\" widget=\"many2many_tags\" attrs=\"{'readonly': [('parent.name', '=', 'test-readonly')]}\"/>`\n\n當 sheet 的 `name` 為 `test-readonly` 的時候, `tag_ids` 這個 fields 會變成 `readonly`.\n\n![alt tag](https://i.imgur.com/u0CMNR5.png)\n\n請注意 :exclamation: 我們並沒有 `parent` 這個欄位, 但是在 view 中可以透過這種方式使用 parent (也就是 sheet ) 的東西.\n\n當 sheet 的 `name` 不是 `test-readonly` 時, `tag_ids` 這個 fields 會變成可以 edit ( 不是`readonly` ).\n\n![alt tag](https://i.imgur.com/PfFlKZN.png)\n\n另外一點要注意的是, 請搭配 `<tree editable=\"top\">` 或 `<tree editable=\"bottom\">`, 單純使用 `<tree>` 不會生效 :exclamation:\n\n`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)\n\n## odoo 手把手教學 - domain 搭配 fields 的三種用法 - part27\n\n* [Youtube Tutorial - odoo 手把手教學 - domain 搭配 fields 的三種用法 - part27](https://youtu.be/ZUNRoWxVWAE)\n\n在 odoo 中 domain 幾乎無所不在 :smile: 今天和大家介紹三種 domain 搭配 fields 的用法,\n\n第一種 - 直接在 model 中的 fileds 定義\n\n[model/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py)\n\n```python\nclass DemoExpenseTutorial(models.Model):\n    _name = 'demo.expense.tutorial'\n    ......\n    employee_id = fields.Many2one('hr.employee', string=\"Employee\", required=True,\n                domain=[('active', '=', True)] )\n    ......\n```\n\n開啟 [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\n\n![alt tag](https://i.imgur.com/KV9dRJE.png)\n\n第二種 - 直接在 view 中的 fileds 定義\n\n[views/view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/views/view.xml)\n\n```xml\n  <record id=\"view_form_demo_expense_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial Form</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Expense Tutorial\">\n        ......\n            <field name=\"employee_id\" domain=\"[('user_id', '=', user_id)]\"/>\n        ......\n          </group>\n        </sheet>\n      </form>\n    </field>\n  </record>\n```\n\n開啟 [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\n\n![alt tag](https://i.imgur.com/uh2QV3R.png)\n\n:exclamation:這邊要注意的是, 如果第一種和第二種同時寫, 以第二種在 view 上定義的為主 :exclamation:\n\n第三種 - 透過 `onchange` 的方法增加 domain\n\n這種方法蠻酷的, 所以我留到最後來講 :smile:\n\n首先, 如果不了解 `onchange` 可參考 [介紹 model](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#%E4%BB%8B%E7%B4%B9-model).\n\n請看下面的範例 [model/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py)\n\n```python\n......\n@api.onchange('user_id')\ndef onchange_user_id(self):\n    # domain\n    result = dict()\n    result['domain'] = {\n        'employee_id': [('user_id', '=', self.user_id.id)]\n    }\n    # equal\n    # self.env['hr.employee'].search([('user_id', '=', self.user_id.id)])\n    return result\n......\n```\n\n當改變 `user_id` 時, 會增加對應的 domain, 需要回傳一個 dict,\n\n這個 dict 包含 fields, 也就是 `employee_id`, 後面則是我們所需要的 domain.\n\n## odoo 手把手教學 - form_view_ref 以及 tree_view_ref 說明 - part28\n\n* [Youtube Tutorial - odoo 手把手教學 - form_view_ref 以及 tree_view_ref 說明 - part28](https://youtu.be/_YkrOp3ytlQ)\n\n還記得 [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:\n\n那時候是使用 `ir.actions.act_window` 也就是 action 的方式定義不同的 view_ids,\n\n今天如果想單獨針對 fields 定義 view 時, 就需要使用 `form_view_ref` `tree_view_ref` :exclamation:\n\n使用方法也很簡單, 請參考 [views/view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/views/view.xml)\n\n```xml\n  <record id=\"view_form_demo_expense_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial Form</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Expense Tutorial\">\n        ......\n            <field name=\"sheet_id\" context=\"{'form_view_ref':'demo_expense_tutorial_v1.view_form_demo_expense_sheet_tutorial'}\"/>\n            <!-- <field name=\"sheet_id\" context=\"{'form_view_ref':'demo_expense_tutorial_v1.custom_view_form_demo_sheet'}\"/> -->\n\n        ......\n          </group>\n        </sheet>\n      </form>\n    </field>\n  </record>\n  ......\n   <record id=\"custom_view_form_demo_sheet\" model=\"ir.ui.view\">\n    <field name=\"name\">Custim Demo Sheet Form</field>\n    <field name=\"model\">demo.expense.sheet.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"custom_view_form_demo_sheet\">\n        <sheet>\n          ......\n        </sheet>\n      </form>\n    </field>\n  </record>\n```\n\n在需要的 fields 上, 加上 `context=\"{'form_view_ref':......}\"`, 然後再定義你的 view 即可,\n\n`tree_view_ref` 也是一樣的概念 :smile:\n\n注意 :exclamation:, 在這裡只要你有定義一個以上的 `demo.expense.sheet.tutorial` form view 時,\n\n記得一定要使用 `form_view_ref` ( 否則它會自動選最後一個 ).\n\n## odoo 手把手教學 - Message Post 教學 - part29\n\n可參考 [models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py)\n\n```python\n......\n@api.multi\ndef btn_message_post(self):\n    for rec in self:\n        if rec.user_id:\n            rec.user_id.partner_id.message_post(body=\"test body\", subject=\"test subject\")\n        else:\n            raise UserError('請選擇使用者(user_id)')\n......\n```\n透過 `partner_id.message_post(....\")`\n\n可以完成 Message Post, 資訊要到 Contacts (`res.partner`) 底下看,\n\n![alt tag](https://i.imgur.com/UEfWFHt.png)\n\n## odoo 手把手教學 - groups 搭配 fields 用法 - part30\n\n* [Youtube Tutorial - odoo 手把手教學 - groups 搭配 fields 用法 - part30](https://youtu.be/JyNyg7iHar0)\n\n這邊的用法和 [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) 是類似的,\n\n只不過對象換成了 groups,\n\n寫法如下, 請參考 [models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py) 中,\n\n```python\nclass DemoExpenseTutorial(models.Model):\n    _name = 'demo.expense.tutorial'\n    ......\n    tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id',\n        string='Tges', copy=False,\n        groups='demo_expense_tutorial_v1.demo_expense_tutorial_group_manager'\n    )\n......\n```\n\n`tag_ids` field 增加了 `groups='demo_expense_tutorial_v1.demo_expense_tutorial_group_manager'`,\n\n代表的意思是只有 Manager 可以看到這個 field(擁有權限),\n\n假如今天一個 User 權限的人, 不管在 tree 或是 form 都看不到 `tag_ids`.\n\n但這不只是隱藏起來, 也就是說如果你強制去取值, 還是無法拿到資料的(因為權限不夠),\n\n建議可以用 shell 模式下去嘗試取值.\n\n然後另一種寫法如下,\n\n請參考 [views/view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/views/view.xml),\n\n```xml\n  <record id=\"view_form_demo_expense_tutorial\" model=\"ir.ui.view\">\n......\n    <field name=\"tag_ids\" widget=\"many2many_tags\" groups=\"demo_expense_tutorial_v1.demo_expense_tutorial_group_manager\"/>\n......\n  </record>\n```\n\n但這只是**隱藏**起來, 也就是說如果你強制去取值, 還是可以拿到資料的(沒有權限限制),\n\n差別在於,\n\n如果你是寫在 model, 會自動幫你產生到全部的 view 上(並且需要對應的權限).\n\n如果你單獨寫在 view 上, 是針對個別的 view (像這邊就是只在 form 上) 生效,\n\n(但不需要對應的權限).\n\n## odoo 手把手教學 - ACID transactions 說明 - part31\n\n* [Youtube Tutorial - odoo 手把手教學 - ACID transactions 說明 - part31](https://youtu.be/M36CNiK9xrM)\n\n這邊先說結論, 下面會再說明, 如果你遵守 odoo 的 ORM,\n\n你是不需要另外去處理 ACID transactions 的問題.\n\n相關的 odoo source code 可參考 `odoo/odoo/sql_db.py`\n\n```python\nclass Cursor(object):\n    \"\"\"Represents an open transaction to the PostgreSQL DB backend,\n       acting as a lightweight wrapper around psycopg2's\n       ``cursor`` objects.\n\n        ``Cursor`` is the object behind the ``cr`` variable used all\n        over the OpenERP code.\n\n        .. rubric:: Transaction Isolation\n      ......\n\n```\n\n如果你不知道甚麼是 ACID, 可參考 [Transaction 概念簡介](https://github.com/twtrubiks/django-transactions-tutorial#transaction).\n\n這邊就用一個例子來說明 Atomicity (原子性),\n\n可參考 [models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/models/models.py)\n\n```python\n......\n@api.multi\ndef btn_test_acid_atomicity(self):\n    for index in range(3):\n        self.create({\n            'name': index,\n            'employee_id': 1\n        })\n        if index == 1:\n            raise UserError('error - auto rollback')\n......\n```\n\n嘗試上面的 code.\n\n假設今天有3筆資料,\n\n只有兩種結果, 3筆全都寫入成功, 或是3筆全都失敗未寫入,\n\n不會有1筆成功寫入, 2筆失敗的狀況.\n\n## odoo 手把手教學 - 特殊 groups 應用說明 - part32\n\n* [Youtube Tutorial - odoo 手把手教學 - 特殊 groups 應用說明 - part32](https://youtu.be/PSiDfM840NI)\n\n這邊介紹幾個比較特殊的 groups 給大家,\n\n首先是 `base.group_no_one`,\n\n它的 groups 定義在原始碼中的 `/odoo/addons/base/security/base_groups.xml`\n\n```xml\n......\n<record model=\"res.groups\" id=\"group_no_one\">\n    <field name=\"name\">Technical Features</field>\n</record>\n......\n```\n\n這個 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) 的時候才看的到.\n\n接著是 `base.group_erp_manager` 和 `base.group_system`,\n\n這些 groups 則是當你擁有 `Access Rights` 和 `Settings` 權限的時候你才看的到.\n\n它的 groups 定義在原始碼中的 `/odoo/addons/base/security/base_groups.xml`\n\n```xml\n......\n<record model=\"res.groups\" id=\"group_erp_manager\">\n    <field name=\"name\">Access Rights</field>\n</record>\n\n<record model=\"res.groups\" id=\"group_system\">\n    <field name=\"name\">Settings</field>\n    <field name=\"implied_ids\" eval=\"[(4, ref('group_erp_manager'))]\"/>\n    <field name=\"users\" eval=\"[(4, ref('base.user_root')), (4, ref('base.user_admin'))]\"/>\n</record>\n......\n```\n\n當然除了使用 groups 定義之外, 也可以直接指定 user,\n\n像是 `base.user_admin` 就是只有 admin user 才看的到.\n\n它的定義在原始碼中的 `/odoo/addons/base/data/res_users_data.xml`\n\n```xml\n......\n\n<!-- user 2 is the human admin user -->\n<record id=\"user_admin\" model=\"res.users\">\n    <field name=\"login\">admin</field>\n    <field name=\"password\">admin</field>\n    <field name=\"partner_id\" ref=\"base.partner_admin\"/>\n    <field name=\"company_id\" ref=\"main_company\"/>\n    <field name=\"company_ids\" eval=\"[(4, ref('main_company'))]\"/>\n    <field name=\"groups_id\" eval=\"[(6,0,[])]\"/>\n    <field name=\"signature\"><![CDATA[<span>-- <br/>\nAdministrator</span>]]></field>\n</record>\n......\n```\n\n使用方法其實之前都說明過了, 可參考 [views/view.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_expense_tutorial_v1/views/view.xml)\n\n```xml\n  <record id=\"view_form_demo_expense_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial Form</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    ......\n            <field name=\"debug_field\" groups=\"base.group_no_one\"/>\n            <field name=\"admin_field\" groups=\"base.user_admin\"/>\n    ......\n```\n\n基本上原生的都是定義在原始碼中的 `odoo/addons/base/security/base_groups.xml`\n\n像是基本的 user groups 種類,\n\n```xml\n......\n<record model=\"res.groups\" id=\"group_user\">\n    <field name=\"name\">Internal User</field>\n</record>\n\n......\n\n<record id=\"group_portal\" model=\"res.groups\">\n    <field name=\"name\">Portal</field>\n    ......\n</record>\n\n......\n\n<record id=\"group_public\" model=\"res.groups\">\n    <field name=\"name\">Public</field>\n    ......\n</record>\n\n......\n\n```\n\n透過定義 user 以及 groups, 可以組合出更靈活的架構.\n"
  },
  {
    "path": "demo_expense_tutorial_v1/__init__.py",
    "content": "# from . import controllers\nfrom . import models"
  },
  {
    "path": "demo_expense_tutorial_v1/__manifest__.py",
    "content": "{\n    'name': \"demo expense tutorial v1\",\n    'summary': \"\"\"\n        tutorial - Many2one, Many2many, One2many\n        demo expense tutorial v1\n    \"\"\",\n    'description': \"\"\"\n        tutorial - Many2one, Many2many, One2many\n        demo expense tutorial v1\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['hr_contract'],\n\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'security/ir_rule.xml',\n        'data/demo_expense_tutorial_data.xml',\n        'views/view.xml',\n        'views/menu.xml',\n    ],\n    # only loaded in demonstration mode\n    # 'demo': [\n    #     'demo/demo.xml',\n    # ],\n    'application': True,\n}\n"
  },
  {
    "path": "demo_expense_tutorial_v1/controllers/__init__.py",
    "content": "from . import controllers"
  },
  {
    "path": "demo_expense_tutorial_v1/controllers/controllers.py",
    "content": "# from odoo import http\n"
  },
  {
    "path": "demo_expense_tutorial_v1/data/demo_expense_tutorial_data.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n    <data noupdate=\"0\">\n        <!-- demo.expense.tutorial -->\n        <record id=\"demo_expense_tutorial_data_1\" model=\"demo.expense.tutorial\">\n            <field name=\"name\">demo_expense_tutorial_data_1</field>\n            <field name=\"employee_id\" ref=\"hr.employee_admin\"></field>\n        </record>\n\n        <record id=\"demo_expense_tutorial_data_2\" model=\"demo.expense.tutorial\">\n            <field name=\"name\">demo_expense_tutorial_data_2</field>\n            <field name=\"employee_id\" ref=\"hr.employee_admin\"></field>\n        </record>\n\n        <!-- demo.tag -->\n        <record id=\"demo_tag_data_1\" model=\"demo.tag\">\n            <field name=\"name\">demo_tag_data_1</field>\n        </record>\n\n        <record id=\"demo_tag_data_2\" model=\"demo.tag\">\n            <field name=\"name\">demo_tag_data_2</field>\n        </record>\n    </data>\n</odoo>\n"
  },
  {
    "path": "demo_expense_tutorial_v1/demo/demo.xml",
    "content": "<odoo>\n    <data>\n\n    </data>\n</odoo>"
  },
  {
    "path": "demo_expense_tutorial_v1/models/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_expense_tutorial_v1/models/models.py",
    "content": "from odoo import models, fields, api\nfrom odoo.exceptions import UserError\n\nclass DemoTag(models.Model):\n    _name = 'demo.tag'\n    _description = 'Demo Tags'\n    _rec_name = 'complete_name'\n\n    name = fields.Char(string='Tag Name', index=True, required=True)\n    complete_name = fields.Char('Complete Name', compute='_compute_complete_name')\n    active = fields.Boolean(default=True, help=\"Set active.\")\n\n    @api.depends('name')\n    def _compute_complete_name(self):\n        for record in self:\n            record.complete_name = 'hello world - {}'.format(record.name)\n\nclass DemoExpenseTutorial(models.Model):\n    _name = 'demo.expense.tutorial'\n    _description = 'Demo Expense Tutorial'\n    _order = \"sequence, id desc\"\n\n    name = fields.Char('Description', required=True)\n\n    # employee_id = fields.Many2one('hr.employee', string=\"Employee\", required=True,\n    #             domain=[('active', '=', True)] )\n    employee_id = fields.Many2one('hr.employee', string=\"Employee\", required=True)\n\n    user_id = fields.Many2one('res.users', default=lambda self: self.env.user)\n\n    # https://www.odoo.com/documentation/12.0/reference/orm.html#odoo.fields.Many2many\n    # Many2many(comodel_name=<object object>, relation=<object object>, column1=<object object>, column2=<object object>, string=<object object>, **kwargs)\n    #\n    # relation: database table name\n    #\n\n    # By default, the relationship table name is the two table names\n    # joined with an underscore and _rel appended at the end.\n    # In the case of our books or authors relationship, it should be named demo_expense_tutorial_demo_tag_rel.\n\n\n    tag_ids = fields.Many2many('demo.tag', 'demo_expense_tag', 'demo_expense_id', 'tag_id',\n        string='Tges', copy=False,\n        groups='demo_expense_tutorial_v1.demo_expense_tutorial_group_manager'\n    )\n    sheet_id = fields.Many2one('demo.expense.sheet.tutorial', string=\"Expense Report\", ondelete='restrict')\n\n    # Related (Reference) fields (不會存在 db)\n    # readonly default 為 True\n    # store default 為 False\n    gender = fields.Selection('Gender', related='employee_id.gender')\n\n    sequence = fields.Integer(index=True, help=\"Gives the sequence order\", default=1)\n    active = fields.Boolean(default=True, help=\"Set active.\")\n    debug_field = fields.Char('debug_field')\n    admin_field = fields.Char('admin_field')\n\n    @api.multi\n    def copy(self, default=None):\n        default = dict(default or {})\n        if not default.get('name'):\n            default['name'] = '{} copy'.format(self.name)\n        return super(DemoExpenseTutorial, self).copy(default)\n\n    @api.multi\n    def button_sheet_id(self):\n        return {\n            'view_mode': 'form',\n            'res_model': 'demo.expense.sheet.tutorial',\n            'res_id': self.sheet_id.id,\n            'type': 'ir.actions.act_window'\n        }\n\n    @api.multi\n    def button_rainbow_man(self):\n        return {\n            'effect': {\n                'fadeout': 'slow',\n                'message': 'hello',\n                'type': 'rainbow_man',\n            }\n        }\n\n    @api.multi\n    def btn_test_acid_atomicity(self):\n        for index in range(3):\n            self.create({\n                'name': index,\n                'employee_id': 1\n            })\n            if index == 1:\n                raise UserError('error - auto rollback')\n\n    @api.multi\n    def button_act_url(self):\n        self.ensure_one()\n        return {\n            'type': 'ir.actions.act_url',\n            'target': 'new',\n            # 'target': 'self',\n            'url': 'https://github.com/twtrubiks/odoo-demo-addons-tutorial',\n        }\n\n    @api.multi\n    def btn_message_post(self):\n        for rec in self:\n            if rec.user_id:\n                rec.user_id.partner_id.message_post(body=\"test body\", subject=\"test subject\")\n            else:\n                raise UserError('請選擇使用者(user_id)')\n\n    @api.onchange('user_id')\n    def onchange_user_id(self):\n        # domain\n        result = dict()\n        result['domain'] = {\n            'employee_id': [('user_id', '=', self.user_id.id)]\n        }\n        # equal\n        # self.env['hr.employee'].search([('user_id', '=', self.user_id.id)])\n        return result\n\nclass DemoExpenseSheetTutorial(models.Model):\n    _name = 'demo.expense.sheet.tutorial'\n    _description = 'Demo Expense Sheet Tutorial'\n\n    name = fields.Char('Expense Demo Report Summary', required=True)\n\n    # One2many is a virtual relationship, there must be a Many2one field in the other_model,\n    # and its name must be related_field\n    expense_line_ids = fields.One2many(\n        'demo.expense.tutorial', # related model\n        'sheet_id', # field for \"this\" on related model\n        string='Expense Lines')\n\n    demo_expenses_count = fields.Integer(\n        compute='_compute_demo_expenses_count',\n        string='Demo Expenses Count')\n\n    @api.multi\n    def add_demo_expense_record(self):\n        # (0, _ , {'field': value}) creates a new record and links it to this one.\n\n        data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')\n\n        tag_data_1 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_1')\n        tag_data_2 = self.env.ref('demo_expense_tutorial_v1.demo_tag_data_2')\n\n        for record in self:\n            # creates a new record\n            val = {\n                'name': 'test_data',\n                'employee_id': data_1.employee_id,\n                'tag_ids': [(6, 0, [tag_data_1.id, tag_data_2.id])]\n            }\n\n            self.expense_line_ids = [(0, 0, val)]\n\n    @api.multi\n    def link_demo_expense_record(self):\n        # (4, id, _) links an already existing record.\n\n        data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')\n\n        for record in self:\n            # link already existing record\n            self.expense_line_ids = [(4, data_1.id, 0)]\n\n    @api.multi\n    def replace_demo_expense_record(self):\n        # (6, _, [ids]) replaces the list of linked records with the provided list.\n\n        data_1 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_1')\n        data_2 = self.env.ref('demo_expense_tutorial_v1.demo_expense_tutorial_data_2')\n\n        for record in self:\n            # replace multi record\n            self.expense_line_ids = [(6, 0, [data_1.id, data_2.id])]\n\n    @api.multi\n    def button_line_ids(self):\n        return {\n            'name': 'Demo Expense Line IDs',\n            'view_type': 'form',\n            'view_mode': 'tree,form',\n            'res_model': 'demo.expense.tutorial',\n            'view_id': False,\n            'type': 'ir.actions.act_window',\n            'domain': [('sheet_id', '=', self.id)],\n        }\n\n    def _compute_demo_expenses_count(self):\n        # usually used read_group\n        for record in self:\n            record.demo_expenses_count = len(self.expense_line_ids)\n\n    @api.multi\n    def name_get(self):\n        names = []\n        for record in self:\n            name = '%s-%s' % (record.create_date.date(), record.name)\n            names.append((record.id, name))\n        return names\n\n    # odoo12/odoo/odoo/addons/base/models/ir_model.py\n    @api.model\n    def _name_search(self, name='', args=None, operator='ilike', limit=100):\n        if args is None:\n            args = []\n        domain = args + ['|', ('id', operator, name), ('name', operator, name)]\n        # domain = args + [ ('name', operator, name)]\n        # domain = args + [ ('id', operator, name)]\n        return super(DemoExpenseSheetTutorial, self).search(domain, limit=limit).name_get()\n"
  },
  {
    "path": "demo_expense_tutorial_v1/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_expense_user,Demo Expense Tutorial User Access,model_demo_expense_tutorial,demo_expense_tutorial_group_user,1,1,1,0\naccess_demo_expense_manager,Demo Expense Tutorial Manager Access,model_demo_expense_tutorial,demo_expense_tutorial_group_manager,1,1,1,1\n\naccess_demo_expense_user_tag,Demo Expense Tutorial Tag User Access,model_demo_tag,demo_expense_tutorial_group_user,1,0,0,0\naccess_demo_expense_manager_tag,Demo Expense Tutorial Tag Manager Access,model_demo_tag,demo_expense_tutorial_group_manager,1,1,1,1\n\naccess_demo_expense_sheet_user,Demo Expense Sheet User Tutorial Access,model_demo_expense_sheet_tutorial,demo_expense_tutorial_group_user,1,1,1,0\naccess_demo_expense_sheet_manager,Demo Expense Sheet Manager Tutorial Access,model_demo_expense_sheet_tutorial,demo_expense_tutorial_group_manager,1,1,1,1\n"
  },
  {
    "path": "demo_expense_tutorial_v1/security/ir_rule.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n    <data noupdate=\"1\">\n\n        <record id=\"ir_rule_demo_expense_user\" model=\"ir.rule\">\n            <field name=\"name\">Demo Expense User</field>\n            <field name=\"model_id\" ref=\"model_demo_expense_tutorial\"/>\n            <field name=\"domain_force\">[('employee_id.user_id.id', '=', user.id)]</field>\n            <field name=\"groups\" eval=\"[(4, ref('demo_expense_tutorial_group_user'))]\"/>\n            <!-- Groups (no group = global) -->\n            <!-- <field name=\"global\" eval=\"True\"/> -->\n\n            <field eval=\"0\" name=\"perm_unlink\"/>\n            <field eval=\"1\" name=\"perm_write\"/>\n            <field eval=\"1\" name=\"perm_read\"/>\n            <field eval=\"1\" name=\"perm_create\"/>\n\n        </record>\n\n        <record id=\"ir_rule_demo_expense_manager\" model=\"ir.rule\">\n            <field name=\"name\">Demo Expense Manager</field>\n            <field name=\"model_id\" ref=\"model_demo_expense_tutorial\"/>\n            <field name=\"domain_force\">[(1, '=', 1)]</field>\n            <field name=\"groups\" eval=\"[(4, ref('demo_expense_tutorial_group_manager'))]\"/>\n        </record>\n    </data>\n</odoo>\n"
  },
  {
    "path": "demo_expense_tutorial_v1/security/security.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n\n  <record id=\"module_demo_expense_tutorial\" model=\"ir.module.category\">\n    <field name=\"name\">Demo expense tutorial category</field>\n  </record>\n\n  <record id=\"demo_expense_tutorial_group_user\" model=\"res.groups\">\n    <field name=\"name\">User</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_expense_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('base.group_user'))]\"/>\n  </record>\n\n  <record id=\"demo_expense_tutorial_group_manager\" model=\"res.groups\">\n    <field name=\"name\">Manager</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_expense_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('demo_expense_tutorial_group_user'))]\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_expense_tutorial_v1/views/menu.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <!-- demo_expense_tutorial App Menu -->\n    <menuitem id=\"demo_expense_tutorial_menu\"\n              name=\"Demo Expense Tutorial\" />\n\n    <!-- Action to open the demo_expense_tutorial -->\n    <act_window id=\"action_expense_tutorial\"\n                name=\"Demo Expense Tutorial Action\"\n                res_model=\"demo.expense.tutorial\"\n                view_mode=\"tree,form\"/>\n\n    <!-- Menu item to open the demo_expense_tutorial -->\n    <menuitem id=\"menu_expense_tutorial\"\n              name=\"Demo Expense Tutorial\"\n\t          action=\"action_expense_tutorial\"\n              parent=\"demo_expense_tutorial_menu\" />\n\n    <!-- Action to open the demo_expense_sheet_tutorial -->\n    <!-- <act_window id=\"action_expense_sheet_tutorial\"\n                name=\"Demo Expense Sheet Tutorial Action\"\n                res_model=\"demo.expense.sheet.tutorial\"\n                view_mode=\"tree,form\"/> -->\n\n    <record id=\"action_expense_sheet_tutorial\" model=\"ir.actions.act_window\">\n        <field name=\"name\">Demo Expense Sheet Tutorial Action</field>\n        <field name=\"res_model\">demo.expense.sheet.tutorial</field>\n        <field name=\"view_type\">form</field>\n        <field name=\"view_mode\">tree,form</field>\n        <field name=\"view_ids\" eval=\"[(5, 0, 0),\n            (0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.view_tree_demo_expense_sheet_tutorial')}),\n            (0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.view_form_demo_expense_sheet_tutorial')})]\"/>\n    </record>\n\n    <!-- Menu item to open the demo_expense_sheet_tutorial -->\n    <menuitem id=\"menu_expense_sheet_tutorial\"\n              name=\"Demo Expense Sheet Tutorial\"\n\t          action=\"action_expense_sheet_tutorial\"\n              parent=\"demo_expense_tutorial_menu\" />\n\n    <!-- Action to open the demo_expense_tutorial_no_craete -->\n    <record id=\"action_expense_tutorial_no_craete\" model=\"ir.actions.act_window\">\n        <field name=\"name\">Demo Expense Tutorial Action No Craete</field>\n        <field name=\"res_model\">demo.expense.tutorial</field>\n        <field name=\"view_type\">form</field>\n        <field name=\"view_mode\">tree</field>\n        <field name=\"view_id\" ref=\"view_tree_demo_expense_tutorial_no_create\"/>\n    </record>\n\n    <!-- Menu item to open the demo_expense_tutorial_no_craete -->\n    <menuitem id=\"menu_expense_tutorial_no_craete\"\n              name=\"Demo Expense Tutorial No Create\"\n\t          action=\"action_expense_tutorial_no_craete\"\n              parent=\"demo_expense_tutorial_menu\" />\n\n    <!-- Menu item to open the demo_expense_view_ids -->\n    <menuitem id=\"menu_expense_tutorial_view_ids\"\n              name=\"Demo Expense Tutorial View ids\"\n              parent=\"demo_expense_tutorial_menu\" />\n\n    <!-- Action to open the menu_expense_tutorial_view_id_1 -->\n    <record id=\"action_expense_tutorial_view_id_1\" model=\"ir.actions.act_window\">\n        <field name=\"name\">Demo Expense Tutorial Action View id 1</field>\n        <field name=\"res_model\">demo.expense.tutorial</field>\n        <field name=\"view_type\">form</field>\n        <field name=\"view_mode\">tree,form</field>\n        <field name=\"view_ids\" eval=\"[(5, 0, 0),\n            (0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.tree_expense_view_id_1')}),\n            (0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.form_expense_view_id_1')})]\"/>\n    </record>\n\n    <!-- Menu item to open the demo_expense_view_id_1 -->\n    <menuitem id=\"menu_expense_tutorial_view_id_1\"\n              name=\"Demo Expense Tutorial View id 1\"\n              sequence=\"1\"\n\t          action=\"action_expense_tutorial_view_id_1\"\n              parent=\"menu_expense_tutorial_view_ids\" />\n\n    <!-- Action to open the menu_expense_tutorial_view_id_2 -->\n    <record id=\"action_expense_tutorial_view_id_2\" model=\"ir.actions.act_window\">\n        <field name=\"name\">Demo Expense Tutorial Action View id 2</field>\n        <field name=\"res_model\">demo.expense.tutorial</field>\n        <field name=\"view_type\">form</field>\n        <field name=\"view_mode\">tree,form</field>\n        <field name=\"view_ids\" eval=\"[(5, 0, 0),\n            (0, 0, {'view_mode': 'tree', 'view_id': ref('demo_expense_tutorial_v1.tree_expense_view_id_2')}),\n            (0, 0, {'view_mode': 'form', 'view_id': ref('demo_expense_tutorial_v1.form_expense_view_id_2')})]\"/>\n    </record>\n\n    <!-- Menu item to open the demo_expense_view_id_2 -->\n    <menuitem id=\"menu_expense_tutorial_view_id_2\"\n              name=\"Demo Expense Tutorial View id 2\"\n              sequence=\"2\"\n\t          action=\"action_expense_tutorial_view_id_2\"\n              parent=\"menu_expense_tutorial_view_ids\" />\n\n    <!-- Menu item to open the menu_expense_tutorial_context-->\n    <menuitem id=\"menu_expense_tutorial_root_context\"\n              name=\"Demo Expense Tutorial View context\"\n              parent=\"demo_expense_tutorial_menu\" />\n\n    <!-- Action to open the demo_expense_tutorial_context -->\n    <record id=\"action_expense_tutorial_context\" model=\"ir.actions.act_window\">\n        <field name=\"name\">Demo Expense Tutorial Action Context</field>\n        <field name=\"res_model\">demo.expense.tutorial</field>\n        <field name=\"view_type\">form</field>\n        <field name=\"view_mode\">tree,form</field>\n        <field name=\"domain\">[]</field>\n\n        <!-- init search default -->\n        <field name=\"context\">{'search_default_name': 'test123'}</field>\n\n        <!-- init create default name 'test123'-->\n        <!-- <field name=\"context\">{'default_name': 'test123'}</field> -->\n    </record>\n\n    <!-- Action to open the demo_expense_tutorial_domain -->\n    <record id=\"action_expense_tutorial_domain\" model=\"ir.actions.act_window\">\n        <field name=\"name\">Demo Expense Tutorial Action Domain</field>\n        <field name=\"res_model\">demo.expense.tutorial</field>\n        <field name=\"view_type\">form</field>\n        <field name=\"view_mode\">tree,form</field>\n        <field name=\"domain\">[('name', 'like', 'test')]</field>\n        <field name=\"context\">{}</field>\n    </record>\n\n    <!-- Action to open the demo_expense_tutorial_test_active -->\n    <record id=\"action_expense_tutorial_test_active\" model=\"ir.actions.act_window\">\n        <field name=\"name\">Demo Expense Tutorial Action Test Active</field>\n        <field name=\"res_model\">demo.expense.tutorial</field>\n        <field name=\"view_type\">form</field>\n        <field name=\"view_mode\">tree,form</field>\n        <field name=\"domain\">[]</field>\n\n        <!-- init show all (active True False) record -->\n        <field name=\"context\">{'active_test':False}</field>\n\n        <!-- init show only (active True) record -->\n        <!-- <field name=\"context\">{}</field> -->\n    </record>\n\n    <!-- Menu item to open the demo_expense_tutorial_context -->\n    <menuitem id=\"menu_expense_tutorial_context\"\n              name=\"Demo Expense Tutorial context\"\n\t          action=\"action_expense_tutorial_context\"\n              parent=\"menu_expense_tutorial_root_context\" />\n\n    <!-- Menu item to open the demo_expense_tutorial_domain -->\n    <menuitem id=\"menu_expense_tutorial_domain\"\n              name=\"Demo Expense Tutorial domain\"\n\t          action=\"action_expense_tutorial_domain\"\n              parent=\"menu_expense_tutorial_root_context\" />\n\n    <!-- Menu item to open the demo_expense_tutorial_domain -->\n    <menuitem id=\"menu_expense_tutorial_active_test\"\n              name=\"Demo Expense Tutorial active_test\"\n\t          action=\"action_expense_tutorial_test_active\"\n              parent=\"menu_expense_tutorial_root_context\" />\n</odoo>\n\n\n"
  },
  {
    "path": "demo_expense_tutorial_v1/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n\n  <record id=\"view_form_demo_expense_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial Form</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Expense Tutorial\">\n        <header>\n          <button name=\"button_rainbow_man\" string=\"Test Rainbow\" type=\"object\" class=\"oe_highlight o_expense_sheet_submit\"/>\n          <button name=\"button_act_url\" string=\"Test Act Url\" type=\"object\" class=\"oe_highlight o_expense_sheet_submit\"/>\n          <button name=\"btn_message_post\" string=\"Message Post\" type=\"object\" class=\"oe_highlight o_expense_sheet_submit\"/>\n          <button name=\"btn_test_acid_atomicity\" string=\"Test acid atomicity\" type=\"object\" class=\"oe_highlight o_expense_sheet_submit\"/>\n        </header>\n        <sheet>\n          <div class=\"oe_button_box\" name=\"button_box\">\n            <button class=\"oe_stat_button\" name=\"button_sheet_id\"\n                    string=\"SHEET ID\" type=\"object\"\n                    attrs=\"{'invisible':[('sheet_id','=', False)]}\" icon=\"fa-bars\"/>\n          </div>\n          <group>\n            <field name=\"name\"/>\n\n            <!-- <field name=\"employee_id\" domain=\"[('user_id', '=', user_id)]\"/> -->\n            <field name=\"employee_id\"/>\n\n            <!-- <field name=\"employee_id\" options=\"{'no_quick_create': True}\"/> -->\n            <!-- <field name=\"employee_id\" options=\"{'no_create_edit': True}\"/> -->\n            <!-- <field name=\"employee_id\" options=\"{'no_create': True}\"/> -->\n            <!-- <field name=\"employee_id\" options=\"{'no_open': True}\"/> -->\n            <field name=\"user_id\"/>\n            <!-- <field name=\"tag_ids\"/> -->\n            <field name=\"tag_ids\" widget=\"many2many_tags\"/> <!-- widget -->\n            <!-- <field name=\"tag_ids\" widget=\"many2many_tags\" groups=\"demo_expense_tutorial_v1.demo_expense_tutorial_group_manager\"/> -->\n\n            <field name=\"sheet_id\" context=\"{'form_view_ref':'demo_expense_tutorial_v1.view_form_demo_expense_sheet_tutorial'}\"/>\n            <!-- <field name=\"sheet_id\" context=\"{'form_view_ref':'demo_expense_tutorial_v1.custom_view_form_demo_sheet'}\"/> -->\n\n            <field name=\"gender\"/>\n            <field name=\"debug_field\" groups=\"base.group_no_one\"/>\n            <field name=\"admin_field\" groups=\"base.user_admin\"/>\n          </group>\n        </sheet>\n      </form>\n    </field>\n  </record>\n\n  <record id=\"view_tree_demo_expense_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial List</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"priority\" eval=\"1\"/>\n    <field name=\"arch\" type=\"xml\">\n      <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\">\n      <!-- <tree default_order=\"sequence, id desc\"> -->\n        <field name=\"sequence\" widget=\"handle\"/>\n        <field name=\"name\"/>\n        <field name=\"employee_id\"/>\n        <field name=\"user_id\"/>\n        <field name=\"tag_ids\"/>\n        <field name=\"sheet_id\" widget=\"many2onebutton\"/>\n      </tree>\n    </field>\n  </record>\n\n  <!-- change name, employee_id fields-->\n  <record id=\"view_tree_demo_expense_tutorial_move\" model=\"ir.ui.view\">\n    <field name=\"name\">view_tree_demo_expense_tutorial_move</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"inherit_id\" ref=\"demo_expense_tutorial_v1.view_tree_demo_expense_tutorial\"/>\n    <field name=\"arch\" type=\"xml\">\n        <xpath expr=\"//field[@name='name']\" position=\"before\">\n          <field name=\"employee_id\" position=\"move\"/>\n        </xpath>\n    </field>\n  </record>\n\n  <record id=\"view_form_demo_expense_sheet_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Sheet Tutorial Form</field>\n    <field name=\"model\">demo.expense.sheet.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Expense Sheet Tutorial\">\n        <header>\n          <button name=\"add_demo_expense_record\" string=\"add demo expense record\" type=\"object\"/>\n          <button name=\"link_demo_expense_record\" string=\"link demo expense record\" type=\"object\"/>\n          <button name=\"replace_demo_expense_record\" string=\"replace expense record\" type=\"object\"/>\n        </header>\n        <sheet>\n          <div class=\"oe_button_box\" name=\"button_box\">\n            <button class=\"oe_stat_button\"\n                    name=\"button_line_ids\"\n                    type=\"object\"\n                    attrs=\"{'invisible':[('expense_line_ids','=', False)]}\"\n                    icon=\"fa-bars\">\n                    <field name=\"demo_expenses_count\" widget=\"statinfo\" string=\"Counts\"/>\n            </button>\n          </div>\n          <group>\n            <field name=\"name\"/>\n          </group>\n          <notebook>\n              <page string=\"Expense\">\n                <field name=\"expense_line_ids\" >\n                  <!-- <tree> -->\n                  <tree editable=\"top\">   <!-- <<<<<<<<<<<< -->\n                  <!-- <tree editable=\"bottom\"> --> <!-- <<<<<<<<<<<< -->\n                    <field name=\"name\"/>\n                    <field name=\"employee_id\"/>\n                    <field name=\"tag_ids\" widget=\"many2many_tags\" attrs=\"{'readonly': [('parent.name', '=', 'test-readonly')]}\"/>\n                  </tree>\n                </field>\n              </page>\n          </notebook>\n        </sheet>\n      </form>\n    </field>\n  </record>\n\n  <record id=\"custom_view_form_demo_sheet\" model=\"ir.ui.view\">\n    <field name=\"name\">Custim Demo Sheet Form</field>\n    <field name=\"model\">demo.expense.sheet.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"custom_view_form_demo_sheet\">\n        <sheet>\n          <group>\n            <field name=\"name\"/>\n          </group>\n          <notebook>\n              <page string=\"Expense\">\n                <field name=\"expense_line_ids\" >\n                  <tree editable=\"top\">\n                    <field name=\"name\"/>\n                    <field name=\"employee_id\"/>\n                    <field name=\"tag_ids\" widget=\"many2many_tags\" attrs=\"{'readonly': [('parent.name', '=', 'test-readonly')]}\"/>\n                  </tree>\n                </field>\n              </page>\n          </notebook>\n        </sheet>\n      </form>\n    </field>\n  </record>\n\n  <record id=\"view_tree_demo_expense_sheet_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Sheet Tutorial List</field>\n    <field name=\"model\">demo.expense.sheet.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n        <field name=\"name\"/>\n        <field name=\"expense_line_ids\"/>\n      </tree>\n    </field>\n  </record>\n\n  <record id=\"view_filter_demo_expense_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial Filter</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n        <search string=\"Demo Expense Tutorial Filter\">\n            <field name=\"name\" string=\"Name\"/>\n            <field name=\"employee_id\" filter_domain=\"['|', ('employee_id', 'ilike', self), ('user_id', 'ilike', self)]\" string=\"User\"/>\n            <filter name=\"filter_inactive\" domain=\"[('active','=',False)]\" string=\"Inactive\"/>\n            <filter name=\"gender\" domain=\"[('gender','=','male')]\" string=\"Male\"/>\n            <separator/>\n            <filter name=\"name\" domain=\"[('name', 'ilike', 'a')]\" string=\"Name_2\"/>\n            <group expand=\"0\" string=\"Group By\">\n              <filter string=\"Sheet\" name=\"sheet\" domain=\"[]\" context=\"{'group_by': 'sheet_id'}\"/>\n              <filter string=\"Employee\" name=\"employee\" domain=\"[]\" context=\"{'group_by': 'employee_id'}\"/>\n            </group>\n        </search>\n    </field>\n  </record>\n\n  <record id=\"view_tree_demo_expense_tutorial_no_create\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial List No Create</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <tree string=\"no_create_tree\" create=\"0\" delete=\"false\" edit=\"1\" editable=\"top\">\n        <field name=\"name\"/>\n        <field name=\"employee_id\"/>\n      </tree>\n    </field>\n  </record>\n\n  <record id=\"tree_expense_view_id_1\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial List View id 1</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n        <field name=\"name\"/>\n        <field name=\"employee_id\"/>\n        <field name=\"user_id\"/>\n        <field name=\"sheet_id\"/>\n      </tree>\n    </field>\n  </record>\n\n  <record id=\"form_expense_view_id_1\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial Form view id 1</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Expense Tutorial\">\n        <sheet>\n          <div class=\"oe_button_box\" name=\"button_box\">\n            <button class=\"oe_stat_button\" name=\"button_sheet_id\"\n                    string=\"SHEET ID\" type=\"object\"\n                    attrs=\"{'invisible':[('sheet_id','=', False)]}\" icon=\"fa-bars\"/>\n          </div>\n          <group>\n            <field name=\"name\"/>\n            <field name=\"employee_id\"/>\n            <field name=\"user_id\"/>\n            <field name=\"sheet_id\"/>\n          </group>\n        </sheet>\n      </form>\n    </field>\n  </record>\n\n  <record id=\"tree_expense_view_id_2\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial List View id 2</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <!-- <field name=\"priority\" eval=\"1\"/> -->\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n        <field name=\"sheet_id\"/>\n        <field name=\"user_id\"/>\n        <field name=\"employee_id\"/>\n        <field name=\"name\"/>\n      </tree>\n    </field>\n  </record>\n\n  <record id=\"form_expense_view_id_2\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Expense Tutorial Form view id 2</field>\n    <field name=\"model\">demo.expense.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Expense Tutorial\">\n        <sheet>\n          <div class=\"oe_button_box\" name=\"button_box\">\n            <button class=\"oe_stat_button\" name=\"button_sheet_id\"\n                    string=\"SHEET ID\" type=\"object\"\n                    attrs=\"{'invisible':[('sheet_id','=', False)]}\" icon=\"fa-bars\"/>\n          </div>\n          <group>\n            <field name=\"sheet_id\"/>\n            <field name=\"user_id\"/>\n            <field name=\"employee_id\"/>\n            <field name=\"name\"/>\n          </group>\n        </sheet>\n      </form>\n    </field>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_fields_view_get_tutorial/README.md",
    "content": "# odoo fields_view_get 介紹教學\n\n建議觀看影片, 會更清楚 :smile:\n\n* [Youtube Tutorial - odoo fields_view_get 介紹教學](https://youtu.be/TpEw3TQiZ_M)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n主要介紹 odoo 中 fields_view_get 這個 function 的功能以及用法.\n\n## 說明\n\n先說結論, 透過 `fields_view_get` 這個方法, 我們可以動態的做很多常規方法無法做到的事情,\n\n今天就來舉個例子, 我希望除了 Billing Manager 這個 groups 之外, 其他的人對 `account.invoice` 中的\n\n`invoice_line_ids` field 都必須是 readonly (如圖下方).\n\n![alt tag](https://i.imgur.com/VuIMx64.png)\n\n讓我們使用 `fields_view_get` 他來解決這個問題吧 :smile: (常規的方法不好解決 :sob:)\n\n請參考 [models/account_invoice.py](models/account_invoice.py)\n\n```python\nclass AccountInvoice(models.Model):\n    _inherit = 'account.invoice'\n\n    @api.model\n    def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):\n        res = super(AccountInvoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)\n        print('view_type:', view_type)\n\n        if not self.env.user.has_group('account.group_account_manager'):\n            if view_type == 'form':\n                doc = etree.XML(res['arch'])\n                for node in doc.xpath('//field[@name=\"invoice_line_ids\"]'):\n                    print('node.attrib dict:', node.attrib)\n                    node_values = node.get('modifiers')\n                    modifiers = json.loads(node_values)\n                    modifiers['readonly'] = True\n                    node.set('modifiers', json.dumps(modifiers))\n                res['arch'] = etree.tostring(doc)\n\n        ......\n\n        return res\n\n```\n\n先把這個 addons 裝起來, 當你在 invoice form 的介面底下看 terminal 的輸出訊息,\n\n![alt tag](https://i.imgur.com/nMW9NQF.png)\n\n(你也可以把 `res['arch']` print 出來, 你就會發現 xml 的資料都包含在裡面)\n\n很明顯的, 當 type 要是 form 的時候才會有 xml 的資料, 主要是去修改 modifiers 裡面\n\n的資料, 要讓他變成是 `readonly=True`.\n\n(程式碼如上, 邏輯就是先抓到 `modifiers` 這個 node, 接著透過 json 的方式下去修改,\n\n最後記得要放回 arch, 也就是 `res['arch'] = etree.tostring(doc)`)\n\n所以透過這段 code, 邏輯就是, 只有擁用 Billing Manager(`account.group_account_manager`)\n\n的 groups 才**不是** readonly, 否則都是 readonly. (建議看影片的 demo :smirk:)\n\n剛剛的 type 是 form, 接著來看看 tree 的狀況,\n\n```python\nclass AccountInvoice(models.Model):\n    _inherit = 'account.invoice'\n\n    @api.model\n    def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):\n        res = super(AccountInvoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)\n        print('view_type:', view_type)\n\n        .......\n\n        if self.env.user.has_group('account.group_account_manager'):\n            if view_type == 'tree':\n                doc = etree.XML(res['arch'])\n                for node in doc.xpath('//field[@name=\"partner_id\"]'):\n                    node.addnext(\n                        etree.Element('field', {'string': 'test partner_id fields',\n                                                'name': 'partner_id'}))\n                res['arch'] = etree.tostring(doc)\n\n        return res\n```\n\n當我們發現 type 是 tree 的時候, 且權限是 Billing Manager(`account.group_account_manager`),\n\n我們在 `partner_id` 的欄位後面再動態加一個 `partner_id` (名稱改為 test partner_id fields)\n\n![alt tag](https://i.imgur.com/OMElix0.png)\n\n快速總結, 透過這個 `fields_view_get` 你可以做到很多非常規的變化, 也可以對 xml 做進一步的修改,\n\n達到動態的邏輯變化.\n"
  },
  {
    "path": "demo_fields_view_get_tutorial/__init__.py",
    "content": "from . import models"
  },
  {
    "path": "demo_fields_view_get_tutorial/__manifest__.py",
    "content": "{\n    'name': \"demo_fields_view_get_tutorial\",\n    'summary': 'tutorial - fields_view_get',\n    'description': \"\"\"\n        tutorial - fields_view_get\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['account'],\n\n    # always loaded\n    'data': [\n    ],\n    # only loaded in demonstration mode\n    # 'demo': [\n    #     'demo/demo.xml',\n    # ],\n    'application': True,\n\n}\n"
  },
  {
    "path": "demo_fields_view_get_tutorial/models/__init__.py",
    "content": "from . import account_invoice\n"
  },
  {
    "path": "demo_fields_view_get_tutorial/models/account_invoice.py",
    "content": "import json\nfrom odoo import api, exceptions, fields, models, _\nfrom lxml import etree\nfrom odoo.exceptions import UserError\n\n\nclass AccountInvoice(models.Model):\n    _inherit = 'account.invoice'\n\n    @api.model\n    def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):\n        res = super(AccountInvoice, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)\n        print('view_type:', view_type)\n\n        if not self.env.user.has_group('account.group_account_manager'):\n            if view_type == 'form':\n                doc = etree.XML(res['arch'])\n                for node in doc.xpath('//field[@name=\"invoice_line_ids\"]'):\n                    print('node.attrib dict:', node.attrib)\n                    node_values = node.get('modifiers')\n                    modifiers = json.loads(node_values)\n                    modifiers['readonly'] = True\n                    node.set('modifiers', json.dumps(modifiers))\n                res['arch'] = etree.tostring(doc)\n\n        if self.env.user.has_group('account.group_account_manager'):\n            if view_type == 'tree':\n                doc = etree.XML(res['arch'])\n                for node in doc.xpath('//field[@name=\"partner_id\"]'):\n                    node.addnext(\n                        etree.Element('field', {'string': 'test partner_id fields',\n                                                'name': 'partner_id'}))\n                res['arch'] = etree.tostring(doc)\n\n        return res\n\n\n"
  },
  {
    "path": "demo_hierarchy_tutorial/README.md",
    "content": "# odoo hierarchy 實作\n\n建議觀看影片, 會更清楚 :smile:\n\n* [Youtube Tutorial - odoo 手把手教學 hierarchy - part1](https://youtu.be/O_ch9553VQ0) - [文章快速連結](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_hierarchy_tutorial#%E8%AA%AA%E6%98%8E)\n\n* [Youtube Tutorial - odoo 手把手教學 hierarchy - part2](https://youtu.be/SER-ZVDnwGw) - [文章快速連結](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_hierarchy_tutorial#%E8%AA%AA%E6%98%8E-child_of-%E5%92%8C-parent_of)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n主要介紹 odoo 中如何實現 階層(hierarchy) 的關係.\n\n## 說明\n\n* [Youtube Tutorial - odoo 手把手教學 hierarchy - part1](https://youtu.be/O_ch9553VQ0)\n\n之前不管是介紹 Many2one 還是 One2many, 都是對別的 model 產生關聯,\n\n那有沒有和自己產生關聯的呢 :smile:\n\n* [odoo 手把手教學 - Many2one - part1](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)\n\n* [odoo 手把手教學 - One2many - part3](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)\n\n答案是有的哦, 就是 odoo 中的 階層(hierarchy) 的關係 :satisfied:\n\n階層(hierarchy) 的關係範例圖如下,\n\n![alt tag](https://i.imgur.com/jFmmet1.png)\n\n接著來看教學的範例,\n\n[models/models.py](models/models.py)\n\n```python\nclass DemoHierarchyTutorial(models.Model):\n    _name = 'demo.hierarchy'\n    _description = 'Demo Hierarchy Tutorial'\n\n    name = fields.Char(string='name', index=True)\n    parent_id = fields.Many2one('demo.hierarchy', string='Related Partner', index=True)\n    parent_name = fields.Char(related='parent_id.name', readonly=True, string='Parent name')\n    child_ids = fields.One2many('demo.hierarchy', 'parent_id', string='Contacts', domain=[('active', '=', True)])\n    active = fields.Boolean(default=True)\n```\n\n比較特別的就是 `parent_id` 和 `child_ids` 都關聯到同一個 model (也就是自己本身 `demo.hierarchy`),\n\n然後一個是 Many2one `parent_id` 和 One2many `child_ids`.\n\n[views/view.xml](views/view.xml)\n\n```xml\n<record id=\"view_form_demo_hierarchy\" model=\"ir.ui.view\">\n<field name=\"name\">Demo Hierarchy Form</field>\n<field name=\"model\">demo.hierarchy</field>\n<field name=\"arch\" type=\"xml\">\n    <form string=\"Demo Hierarchy\">\n    <sheet>\n        <group>\n        <field name=\"name\"/>\n        <field name=\"active\"/>\n        <field name=\"parent_id\"/>\n        <field name=\"parent_name\"/>\n        </group>\n        <notebook>\n        <page string=\"Hierarchy\">\n            <field name=\"child_ids\" mode=\"kanban\">\n            <form string=\"Contact / Address\">\n                <sheet>\n                <field name=\"parent_id\" invisible=\"1\"/>\n                <hr/>\n                <group>\n                    <field name=\"name\" string=\"Contact Name\"/>\n                </group>\n                </sheet>\n            </form>\n            </field>\n        </page>\n        </notebook>\n    </sheet>\n    </form>\n</field>\n</record>\n```\n\n寫法和一般的 Many2one 或 One2many 是一樣的, 然後在 One2many 裡面,\n\n將 `parent_id` 隱藏起來, 因為不需要.\n\n`<field name=\"parent_id\" invisible=\"1\"/>`\n\n先來建立一比 `demo.hierarchy` (test1), `parent_id` 先不填\n\n![alt tag](https://i.imgur.com/sZGTOvZ.png)\n\n點選 add 再建立一比 `demo.hierarchy` (test2)\n\n![alt tag](https://i.imgur.com/EfUkYeN.png)\n\n呈現效果如下, test2 的 parent 就是 test1\n\n![alt tag](https://i.imgur.com/pHWlbU5.png)\n\n點選 add 再建立一比 `demo.hierarchy` (test3),\n\n呈現效果如下, test2 和 test1 的 parent 都是 test1\n\n![alt tag](https://i.imgur.com/yVhz0Be.png)\n\ntree 的部份\n\n![alt tag](https://i.imgur.com/6Hsl2Gp.png)\n\ndb 中的狀態\n\n![alt tag](https://i.imgur.com/kuHStcy.png)\n\n## 說明 child_of 和 parent_of\n\n* [Youtube Tutorial - odoo 手把手教學 hierarchy - part2](https://youtu.be/SER-ZVDnwGw)\n\n在 odoo 中很常看到 child_of 和 parent_of,\n\n可以參考 Contact `res.partner`.\n\nodoo 原始碼中的範例, 路徑 `odoo/addons/base/models/res_partner.py`\n\n```python\nclass Partner(models.Model):\n    _description = 'Contact'\n    _inherit = ['format.address.mixin']\n    _name = \"res.partner\"\n    _order = \"display_name\"\n\n    ......\n    parent_id = fields.Many2one('res.partner', string='Related Company', index=True)\n    parent_name = fields.Char(related='parent_id.name', readonly=True, string='Parent name')\n    child_ids = fields.One2many('res.partner', 'parent_id', string='Contacts', domain=[('active', '=', True)])\n    ref = fields.Char(string='Internal Reference', index=True)\n    ......\n```\n\n其中 parent_id 是 Many2one 的關係 , 而 child_ids 則是 One2many的關係.\n\n階層關係如下\n\n![alt tag](https://i.imgur.com/jFmmet1.png)\n\ndb 關係如下\n\n![alt tag](https://i.imgur.com/4TOjc8k.png)\n\n階層關係如下\n\n![alt tag](https://i.imgur.com/cCAonbi.png)\n\ndb 關係如下\n\n![alt tag](https://i.imgur.com/FQ7s9C0.png)\n\n`child_of`\n\n```python\n>>> self.env['res.partner'].search([('id', 'child_of', 14)])  #(小技巧, 從後面看回來, 14 的孩子)\nres.partner(14, 26, 33, 27, 68)\n```\n\n```python\n>>> self.env['res.partner'].search([('id', 'child_of', [11])])\nres.partner(11, 20, 22, 31, 23)\n```\n\nchild_of 也可以一次找多個\n\n```python\n>>> self.env['res.partner'].search([('id', 'child_of', [14, 11])])\nres.partner(14, 26, 33, 27, 68......)\n```\n\n`parent_of`\n\n```python\n>>> self.env['res.partner'].search([('id', 'parent_of', 68)]) #(小技巧, 從後面看回來, 68 的父親)\nres.partner(14, 68, 26)\n```\n\n在 odoo 原始碼中, 可能會看到以下的 code\n\n`('company_id','child_of',[user.company_id.id])]`\n\n問題點在於為甚麼要別使用 `[]`\n\n我這邊猜測應該是為了要避免 WARNING\n\n```python\n>>> self.env['res.partner'].search([('id', 'parent_of', [False])])\nres.partner()\n>>> self.env['res.partner'].search([('id', 'parent_of', False)])  # 會有 WARNING\n2020-07-29 WARNING odoo919 odoo.osv.expression: Unexpected domain [('id', 'parent_of', False)], interpreted as False\nres.partner()\n```"
  },
  {
    "path": "demo_hierarchy_tutorial/__init__.py",
    "content": "from . import models"
  },
  {
    "path": "demo_hierarchy_tutorial/__manifest__.py",
    "content": "{\n    'name': \"demo hierarchy tutorial\",\n    'summary': \"\"\"\n        demo_hierarchy_tutorial\n    \"\"\",\n    'description': \"\"\"\n        demo_hierarchy_tutorial\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['base'],\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'views/menu.xml',\n        'views/view.xml',\n    ],\n    'installable': True,\n    'auto_install': False,\n    'application': True,\n}\n"
  },
  {
    "path": "demo_hierarchy_tutorial/models/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_hierarchy_tutorial/models/models.py",
    "content": "from odoo import models, fields, api\n\nclass DemoHierarchyTutorial(models.Model):\n    _name = 'demo.hierarchy'\n    _description = 'Demo Hierarchy Tutorial'\n\n    name = fields.Char(string='name', index=True)\n    parent_id = fields.Many2one('demo.hierarchy', string='Related Partner', index=True)\n    parent_name = fields.Char(related='parent_id.name', readonly=True, string='Parent name')\n    child_ids = fields.One2many('demo.hierarchy', 'parent_id', string='Contacts', domain=[('active', '=', True)])\n    active = fields.Boolean(default=True)\n"
  },
  {
    "path": "demo_hierarchy_tutorial/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_hierarchy_user,Demo Hierarchy User Access,model_demo_hierarchy,demo_hierarchy_group_user,1,0,0,0\naccess_demo_hierarchy_manager,Demo Hierarchy Manager Access,model_demo_hierarchy,demo_hierarchy_group_manager,1,1,1,1"
  },
  {
    "path": "demo_hierarchy_tutorial/security/security.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n\n  <record id=\"module_demo_hierarchy\" model=\"ir.module.category\">\n    <field name=\"name\">Demo hierarchy category</field>\n  </record>\n\n  <record id=\"demo_hierarchy_group_user\" model=\"res.groups\">\n    <field name=\"name\">User</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_hierarchy\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('base.group_user'))]\"/>\n  </record>\n\n  <record id=\"demo_hierarchy_group_manager\" model=\"res.groups\">\n    <field name=\"name\">Manager</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_hierarchy\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('demo_hierarchy_group_user'))]\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_hierarchy_tutorial/views/menu.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <!-- demo_hierarchy App Menu -->\n    <menuitem id=\"demo_hierarchy_menu\"\n      name=\"Demo Hierarchy\"/>\n\n    <!-- Action to open the demo_hierarchy_menu -->\n    <act_window id=\"action_hierarchy\"\n      name=\"Demo Hierarchy Action\"\n      res_model=\"demo.hierarchy\"\n      view_mode=\"tree,form\"/>\n\n    <!-- Menu item to open the demo_hierarchy_menu -->\n    <menuitem id=\"menu_odoo_tutorial\"\n      name=\"Demo Hierarchy Tutorial\"\n      action=\"action_hierarchy\"\n      parent=\"demo_hierarchy_menu\"/>\n</odoo>\n\n\n"
  },
  {
    "path": "demo_hierarchy_tutorial/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n  <record id=\"view_form_demo_hierarchy\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Hierarchy Form</field>\n    <field name=\"model\">demo.hierarchy</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Hierarchy\">\n        <sheet>\n          <group>\n            <field name=\"name\"/>\n            <field name=\"active\"/>\n            <field name=\"parent_id\"/>\n            <field name=\"parent_name\"/>\n          </group>\n          <notebook>\n            <page string=\"Hierarchy\">\n              <field name=\"child_ids\" mode=\"kanban\">\n                <form string=\"Contact / Address\">\n                  <sheet>\n                    <field name=\"parent_id\" invisible=\"1\"/>\n                    <hr/>\n                    <group>\n                      <field name=\"name\" string=\"Contact Name\"/>\n                    </group>\n                  </sheet>\n                </form>\n              </field>\n            </page>\n          </notebook>\n        </sheet>\n      </form>\n    </field>\n  </record>\n\n  <record id=\"view_tree_demo_hierarchy\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Hierarchy List</field>\n    <field name=\"model\">demo.hierarchy</field>\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n        <field name=\"name\"/>\n        <field name=\"active\"/>\n        <field name=\"parent_id\"/>\n        <field name=\"parent_name\"/>\n      </tree>\n    </field>\n  </record>\n</odoo>"
  },
  {
    "path": "demo_hook_tutorial/README.md",
    "content": "# odoo 觀念 - 實作 init hook\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - 實作 init hook](https://youtu.be/2ZmfH3wBHm8)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n本篇文章主要介紹 odoo 中的 init hook 這部份\n\n## 說明\n\n還記得之前我們介紹過 [介紹 security, menu, tree, form](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#%E4%BB%8B%E7%B4%B9-security-menu-tree-form)\n\n裡面有提到很多時候會透過 `data/xxx.xml` 的方式自動產生一些預設資料,\n\n但是, 有時候我們的邏輯比較複雜, 無法簡單的使用 xml 表示, 這時候, 就可以透過 hook 的方式.\n\n先來看 `__manifest__.py`\n\n```python\n{\n    ......\n\n    'pre_init_hook': 'pre_init_hook',\n    'post_init_hook': 'post_init_hook',\n    'uninstall_hook': 'uninstall_hook',\n    'post_load': 'post_load_hook',\n\n    'application': True,\n}\n\n```\n\n在 odoo 中, hook 總共有 4 種,\n\n`pre_init_hook` 在安裝 addons 之前, 會先執行他, 執行完畢後, 才開始安裝 addons. (更新不會生效)\n\n`post_init_hook` 在安裝 addons 完之後, 才會執行他. (更新不會生效)\n\n`uninstall_hook` 在移除 addons 完之後, 才會執行他.\n\n`post_load` 這個比較特殊, 他只會生效在當你使用 odoo-bin CLI 安裝或更新時, 優先於 `pre_init_hook`.\n\n`post_load` 比較進階, 常常搭配 [介紹 Monkey Patch](https://github.com/twtrubiks/fluent-python-notes/tree/master/what_is_the_Monkey_Patch), 請參考 odoo source code.\n\n請參考 `__init__.py`\n\n```python\nfrom odoo import api, SUPERUSER_ID\n\nimport logging\n\n_logger = logging.getLogger(__name__)\n\ndef pre_init_hook(cr):\n    env = api.Environment(cr, SUPERUSER_ID, {})\n    # data = env[......].search([......])\n\n    _logger.warning('=== pre_init_hook ===')\n\ndef post_init_hook(cr, registry):\n    env = api.Environment(cr, SUPERUSER_ID, {})\n    # data = env[......].search([......])\n\n    _logger.warning('=== post_init_hook ===')\n\ndef uninstall_hook(cr, registry):\n    env = api.Environment(cr, SUPERUSER_ID, {})\n    # data = env[......].search([......])\n\n    _logger.warning('=== uninstall_hook ===')\n\ndef post_load_hook():\n    _logger.warning('=== post_load_hook ===')\n```\n\n當我們透過 odoo-bin CLI 安裝 addons 時,\n\n執行順序為 `post_load_hook` -> `pre_init_hook` -> `post_init_hook`\n\n(如果從 odoo 介面安裝 addons 則不會有 `post_load_hook` )\n\n![alt tag](https://i.imgur.com/zFeoeNl.png)\n\n從 odoo 介面移除 addons\n\n![alt tag](https://i.imgur.com/viGukst.png)\n\n透過這個方法, 當我們在安裝或是移除 addons 時, 可以針對資料面再進行一次檢查或清理.\n\n像是有時候繼承既有的 rule, 當你移除 addons 時, 該 rule 並不會被還原,\n\n這時候, 就可以用 hook 的方式來處理 :satisfied:\n"
  },
  {
    "path": "demo_hook_tutorial/__init__.py",
    "content": "from odoo import api, SUPERUSER_ID\n\nimport logging\n\n_logger = logging.getLogger(__name__)\n\ndef pre_init_hook(cr):\n    env = api.Environment(cr, SUPERUSER_ID, {})\n    # data = env[......].search([......])\n\n    _logger.warning('=== pre_init_hook ===')\n\ndef post_init_hook(cr, registry):\n    env = api.Environment(cr, SUPERUSER_ID, {})\n    # data = env[......].search([......])\n\n    _logger.warning('=== post_init_hook ===')\n\ndef uninstall_hook(cr, registry):\n    env = api.Environment(cr, SUPERUSER_ID, {})\n    # data = env[......].search([......])\n\n    _logger.warning('=== uninstall_hook ===')\n\ndef post_load_hook():\n    _logger.warning('=== post_load_hook ===')"
  },
  {
    "path": "demo_hook_tutorial/__manifest__.py",
    "content": "{\n    'name': \"demo hook tutorial\",\n\n    'summary': \"\"\"demo hook tutorial\"\"\",\n\n    'description': \"\"\"demo hook tutorial\"\"\",\n\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['base'],\n\n    'pre_init_hook': 'pre_init_hook',\n    'post_init_hook': 'post_init_hook',\n    'uninstall_hook': 'uninstall_hook',\n    'post_load': 'post_load_hook',\n\n    'application': True,\n}\n"
  },
  {
    "path": "demo_i18n_expense_tutorial/README.md",
    "content": "# odoo 觀念 - Translating 翻譯教學 i18n\n\n建議觀看影片, 會更清楚 :smile:\n\n* [Youtube Tutorial - odoo 觀念 - Translating 翻譯教學 i18n - part1](https://youtu.be/_gGmwgk8250) - [文章快速連結](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_i18n_expense_tutorial#%E5%9F%BA%E6%9C%AC%E8%A7%80%E5%BF%B5)\n\n* [Youtube Tutorial - odoo 觀念 - Translating 翻譯教學 i18n - part2](https://youtu.be/Ka7ucdDnfHA) - [文章快速連結](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_i18n_expense_tutorial#%E4%BD%BF%E7%94%A8%E7%A8%8B%E5%BC%8F%E7%A2%BC%E7%BF%BB%E8%AD%AF)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n本篇文章主要介紹 odoo 中的 Translating 翻譯教學,\n\n也就是 i18n 的資料夾, 會教你如何繼承(覆蓋)它.\n\n官方文件可參考 [translations.html](https://www.odoo.com/documentation/12.0/reference/translations.html)\n\n## 基本觀念\n\n* [Youtube Tutorial - odoo 觀念 - Translating 翻譯教學 i18n - part1](https://youtu.be/_gGmwgk8250)\n\n先和大家說如果你要開啟其他語言要如何開啟,\n\nTranslations -> Languages 選擇要啟用的語言即可\n\n![alt tag](https://i.imgur.com/H1X8nP2.png)\n\n也可以使用指令的方式啟用語言\n\n```cmd\npython3 odoo-bin -d odoo --load-language=zh_TW -c odoo.conf\n```\n\n在需要翻譯的頁面底下選擇 Technical Translation\n\n這邊強烈建議大家先切換語系到**英文**, 再下去進行翻譯, 不然很容易怪怪的 :joy:\n\n![alt tag](https://i.imgur.com/tYUDr68.png)\n\n就會看到以下的畫面\n\n![alt tag](https://i.imgur.com/6wm36X2.png)\n\n## 後台設定\n\n當然也可以從 odoo 後台管理這些翻譯檔案\n\nTranslations -> Import / Export\n\n![alt tag](https://i.imgur.com/HnCWsc9.png)\n\nExport Translation\n\n匯出 po 檔案,\n\n![alt tag](https://i.imgur.com/pWaJYC1.png)\n\n也可以使用指令的方式匯出 po 檔案\n\n```cmd\npython3 odoo-bin -d odoo --i18n-export=hr_expense.po --modules=hr_expense -c odoo.conf\n```\n\n指定匯出 po 檔案(特定語言), 注意, odoo 必須已經安裝對應的語言\n\n```cmd\npython3 odoo-bin -d odoo --i18n-export=zh_TW.po --modules=hr_expense --language=zh_TW -c odoo.conf\n```\n\n執行完後, 可查看路徑底下是否有對應的檔案.\n\nImport Translation\n\n匯入 po 檔案,\n\n注意 :exclamation: :exclamation: Overwrite Existing Terms 請記住一定要打勾, 否則不會生效.\n\n![alt tag](https://i.imgur.com/TGevO7f.png)\n\n也可以使用指令的方式匯入 po 檔案,\n\n請記得將 zh_TW.po (檔名可自訂) 放入任意 addons 的路徑中.\n\n```cmd\npython3 odoo-bin -d odoo --i18n-import=zh_TW.po --language=zh_TW -c odoo.conf\n```\n\n如果沒有生效, 請再加上 `--i18n-overwrite`.\n\n## 使用程式碼翻譯\n\n[Youtube Tutorial - odoo 觀念 - Translating 翻譯教學 i18n - part2](https://youtu.be/Ka7ucdDnfHA)\n\n除了從 odoo 的後台翻譯之外, 也可以從程式碼翻譯,\n\n也就是教大家如何去覆寫(覆蓋) po 翻譯檔案(i18n 資料夾),\n\n這邊使用覆蓋內建的 `hr_expense` 當作範例,\n\n只需要建立一個 addons, 然後有 i18n 的資料夾, 直接覆蓋掉需要的翻譯即可\n\n[i18n/zh_TW.po](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_i18n_expense_tutorial/i18n/zh_TW.po)\n\n```po\n#. module: hr_expense\n#: model:ir.model.fields,field_description:hr_expense.field_hr_expense__name\nmsgid \"Description\"\nmsgstr \"測試描述\"\n```\n\n直接安裝即可\n\n```cmd\npython3 odoo-bin -i demo_i18n_expense_tutorial -d odoo -c odoo.conf\n```\n\n如果你有修改(更新)翻譯的 po 檔案, 指令需要加上 `--i18n-overwrite`\n\n```cmd\npython3 odoo-bin --i18n-overwrite -u demo_i18n_expense_tutorial -d odoo -c odoo.conf\n```\n\n如果不想用指令更新, 也可以從 odoo 的後台進行更新, 注意, 更新的方法不是更新 addons.\n\n是先重起 odoo, 然後到 odoo 後台的 Load a Translation (如下圖)\n\n![alt tag](https://i.imgur.com/E5X1h7I.png)\n\n選擇對應的語言,\n\n注意 :exclamation: :exclamation: Overwrite Existing Terms 請記住一定要打勾, 否則不會生效.\n\n![alt tag](https://i.imgur.com/2FbZie6.png)\n\n如果更新時出現以下錯誤訊息\n\n```text\npsycopg2.ProgrammingError: ON CONFLICT DO UPDATE command cannot affect row a second time\nHINT:  Ensure that no rows proposed for insertion within the same command have duplicate constrained values.\n```\n\n![alt tag](https://i.imgur.com/1OVRBIv.png)\n\n代表你的 po 檔案裡面可能有重複翻譯的字段, 可以使用 terminal 找出是哪個 addons 的翻譯檔出錯 (如下圖).\n\n![alt tag](https://i.imgur.com/LTgKdx8.png)\n\n\n\n"
  },
  {
    "path": "demo_i18n_expense_tutorial/__init__.py",
    "content": ""
  },
  {
    "path": "demo_i18n_expense_tutorial/__manifest__.py",
    "content": "{\n    'name': \"demo i18n expense tutorial\",\n    'summary': \"\"\"\n        i18n tutorial\n    \"\"\",\n    'description': \"\"\"\n        i18n tutorial\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['hr_expense'],\n\n    # always loaded\n    'data': [\n\n    ],\n    'application': True,\n}\n"
  },
  {
    "path": "demo_i18n_expense_tutorial/i18n/zh_TW.po",
    "content": "# Translation of Odoo Server.\n# This file contains the translation of the following modules:\n# * hr_expense\n#\n\nmsgid \"\"\nmsgstr \"\"\n\"Project-Id-Version: Odoo Server 12.0\\n\"\n\"Report-Msgid-Bugs-To: \\n\"\n\"POT-Creation-Date: 2018-12-19 08:20+0000\\n\"\n\"PO-Revision-Date: 2018-08-24 09:18+0000\\n\"\n\"Last-Translator: Martin Trigaux, 2018\\n\"\n\"Language-Team: Chinese (Taiwan) (https://www.transifex.com/odoo/teams/41243/zh_TW/)\\n\"\n\"MIME-Version: 1.0\\n\"\n\"Content-Type: text/plain; charset=UTF-8\\n\"\n\"Content-Transfer-Encoding: \\n\"\n\"Language: zh_TW\\n\"\n\"Plural-Forms: nplurals=1; plural=0;\\n\"\n\n#. module: hr_expense\n#: model:ir.model.fields,field_description:hr_expense.field_hr_expense__name\nmsgid \"Description\"\nmsgstr \"測試描述\""
  },
  {
    "path": "demo_inherit_controller/README.md",
    "content": "# odoo 觀念 - 如何繼承 inherit controller\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - 如何繼承 inherit controller](https://youtu.be/kZG-CKQ2M7A)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 繼承 - class inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_class_inheritance)\n\n本篇文章主要介紹 odoo 中的繼承 controller 這部份 :smile:\n\n## 說明\n\n今天來繼承 sale addons 裡面的 `portal_my_quotes`,\n\n路徑在 `addons/sale/controllers/portal.py`,\n\n安裝好這個範例 addons, 可直接瀏覽 [http://0.0.0.0:8069/my/quotes](http://0.0.0.0:8069/my/quotes)\n\n請查看 [controllers/portal.py](controllers/portal.py)\n\n```python\nfrom odoo import http\nfrom odoo.addons.sale.controllers.portal import CustomerPortal\n\nclass TutorialPortal(CustomerPortal):\n\n    @http.route(['/my/quotes', '/my/quotes/page/<int:page>'], type='http', auth=\"user\", website=True)\n    def portal_my_quotes(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):\n        res = super(TutorialPortal, self).portal_my_quotes(page=1, date_begin=None, date_end=None, sortby=None, **kw)\n        # res = super().portal_my_quotes(page=1, date_begin=None, date_end=None, sortby=None, **kw)\n        print('inherit controller')\n        return res\n......\n```\n\n從上述的 code 你可以發現, 我們先 import 了 `CustomerPortal`,\n\n`from odoo.addons.sale.controllers.portal import CustomerPortal`\n\n因為需要繼承 CustomerPortal.\n\n接著找到對應的 function, 也就是 `portal_my_quotes`, 基本上,\n\n`http.route` 以及 `portal_my_quotes` 都保持一樣即可.\n\n接著透過 super 去呼叫父類別,\n\n`res = super(TutorialPortal, self).portal_my_quotes(......)`\n\n如果是 python3, 可以使用更精簡的寫法,\n\n`res = super().portal_my_quotes(......)`\n\n把你需要增加的東西寫在這邊(可能是對 `res` 做修改), 這裡使用簡單的 print 代替,\n\n`print('inherit controller')`\n\n最後我們回傳 res.\n\n整流程就是, 先呼叫原始的 `portal_my_quotes` 之後, 再執行 `print('inherit controller')` 這段.\n\n(非常建議用中斷點看 code 如何跑的, 可直接看影片的說明)\n\n在某些情況下, 對 res 做修改可能無法完全符合我們的需求, 這時候可以使用更快的方法(但不推薦),\n\n就是直接整個去覆蓋掉 `portal_my_quotes`, 也就是直接把原始的 code, 再貼上一遍,\n\n然後再加上自己需要的功能.\n\n```python\nfrom odoo import http, _\nfrom odoo.http import request\nfrom odoo.addons.portal.controllers.portal import pager as portal_pager\nfrom odoo.addons.sale.controllers.portal import CustomerPortal\n\nclass TutorialPortal(CustomerPortal):\n\n    ......\n\n    @http.route(['/my/quotes', '/my/quotes/page/<int:page>'], type='http', auth=\"user\", website=True)\n    def portal_my_quotes(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):\n        values = self._prepare_portal_layout_values()\n        partner = request.env.user.partner_id\n        SaleOrder = request.env['sale.order']\n\n        domain = [\n            ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),\n            ('state', 'in', ['sent', 'cancel'])\n        ]\n\n        searchbar_sortings = {\n            'date': {'label': _('Order Date'), 'order': 'date_order desc'},\n            'name': {'label': _('Reference'), 'order': 'name'},\n            'stage': {'label': _('Stage'), 'order': 'state'},\n        }\n\n        # default sortby order\n        if not sortby:\n            sortby = 'date'\n        sort_order = searchbar_sortings[sortby]['order']\n\n        archive_groups = self._get_archive_groups('sale.order', domain)\n        if date_begin and date_end:\n            domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]\n\n        # count for pager\n        quotation_count = SaleOrder.search_count(domain)\n        # make pager\n        pager = portal_pager(\n            url=\"/my/quotes\",\n            url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby},\n            total=quotation_count,\n            page=page,\n            step=self._items_per_page\n        )\n        # search the count to display, according to the pager data\n        quotations = SaleOrder.search(domain, order=sort_order, limit=self._items_per_page, offset=pager['offset'])\n        request.session['my_quotations_history'] = quotations.ids[:100]\n\n        values.update({\n            'date': date_begin,\n            'quotations': quotations.sudo(),\n            'page_name': 'quote',\n            'pager': pager,\n            'archive_groups': archive_groups,\n            'default_url': '/my/quotes',\n            'searchbar_sortings': searchbar_sortings,\n            'sortby': sortby,\n        })\n        return request.render(\"sale.portal_my_quotations\", values)\n```\n\n注意, 這邊也有多 import 了一些東西, 因為 `portal_my_quotes` 有使用到,\n\n如果使用這種方法, 流程就是直接跑你定義的 `portal_my_quotes`.\n\n(而不會再去呼叫原始的, 因為沒透過 `super` 去呼叫)\n"
  },
  {
    "path": "demo_inherit_controller/__init__.py",
    "content": "from . import controllers"
  },
  {
    "path": "demo_inherit_controller/__manifest__.py",
    "content": "{\n    'name': \"demo inherit controller\",\n    'summary': \"\"\"demo inherit controller\"\"\",\n    'description': \"\"\"demo inherit controller\"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['sale_management'],\n\n    # always loaded\n    'data': [\n\n    ],\n    'application': True,\n}\n"
  },
  {
    "path": "demo_inherit_controller/controllers/__init__.py",
    "content": "from . import portal\n"
  },
  {
    "path": "demo_inherit_controller/controllers/portal.py",
    "content": "from odoo import http, _\nfrom odoo.http import request\nfrom odoo.addons.portal.controllers.portal import pager as portal_pager\nfrom odoo.addons.sale.controllers.portal import CustomerPortal\n\nclass TutorialPortal(CustomerPortal):\n\n    @http.route(['/my/quotes', '/my/quotes/page/<int:page>'], type='http', auth=\"user\", website=True)\n    def portal_my_quotes(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):\n        res = super(TutorialPortal, self).portal_my_quotes(page=1, date_begin=None, date_end=None, sortby=None, **kw)\n        # res = super().portal_my_quotes(page=1, date_begin=None, date_end=None, sortby=None, **kw)\n        print('inherit controller')\n        return res\n\n    # @http.route(['/my/quotes', '/my/quotes/page/<int:page>'], type='http', auth=\"user\", website=True)\n    # def portal_my_quotes(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):\n    #     values = self._prepare_portal_layout_values()\n    #     partner = request.env.user.partner_id\n    #     SaleOrder = request.env['sale.order']\n\n    #     domain = [\n    #         ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),\n    #         ('state', 'in', ['sent', 'cancel'])\n    #     ]\n\n    #     searchbar_sortings = {\n    #         'date': {'label': _('Order Date'), 'order': 'date_order desc'},\n    #         'name': {'label': _('Reference'), 'order': 'name'},\n    #         'stage': {'label': _('Stage'), 'order': 'state'},\n    #     }\n\n    #     # default sortby order\n    #     if not sortby:\n    #         sortby = 'date'\n    #     sort_order = searchbar_sortings[sortby]['order']\n\n    #     archive_groups = self._get_archive_groups('sale.order', domain)\n    #     if date_begin and date_end:\n    #         domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]\n\n    #     # count for pager\n    #     quotation_count = SaleOrder.search_count(domain)\n    #     # make pager\n    #     pager = portal_pager(\n    #         url=\"/my/quotes\",\n    #         url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby},\n    #         total=quotation_count,\n    #         page=page,\n    #         step=self._items_per_page\n    #     )\n    #     # search the count to display, according to the pager data\n    #     quotations = SaleOrder.search(domain, order=sort_order, limit=self._items_per_page, offset=pager['offset'])\n    #     request.session['my_quotations_history'] = quotations.ids[:100]\n\n    #     values.update({\n    #         'date': date_begin,\n    #         'quotations': quotations.sudo(),\n    #         'page_name': 'quote',\n    #         'pager': pager,\n    #         'archive_groups': archive_groups,\n    #         'default_url': '/my/quotes',\n    #         'searchbar_sortings': searchbar_sortings,\n    #         'sortby': sortby,\n    #     })\n    #     return request.render(\"sale.portal_my_quotations\", values)\n"
  },
  {
    "path": "demo_multi_company/README.md",
    "content": "# odoo 觀念 - multi company\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - multi company - part1](https://youtu.be/u8u0eRzY8kg)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n本篇文章主要介紹 odoo 中的 multi company 的一小部份 :smile:\n\n因為 multi company 的方法有很多種.\n\n## 說明\n\n首先, odoo 在 Multi-company 的設計上是有很多想法以及方法的,\n\n可參考官方文件 [Multi-company Guidelines](https://www.odoo.com/documentation/14.0/developer/howtos/company.html) 觀看.\n\n(補充一下, 從 odoo13 開始, Multi-company 的概念有改動, 但不影響本篇的教學 :relaxed:)\n\n今天主要是要介紹 fields 中的一個參數 `company_dependent=True`,\n\n這個參數主要是為了 Multi-company 設計的.\n\n寫法很簡單, 就是在 model 中加入參數即可\n\n[models/model.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_multi_company/models/model.py)\n\n```python\n......\n\nclass DemoCompany(models.Model):\n    _name = 'demo.company'\n    _description = 'Demo Company'\n\n    name = fields.Char('Description', required=True)\n\n    property_account_receivable_id = fields.Many2one('account.account',\n        company_dependent=True,\n        string=\"Account Receivable\",\n        domain=\"[('internal_type', '=', 'receivable'), ('deprecated', '=', False)]\",\n        required=True)\n\n    company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, default=lambda self: self.env.user.company_id)\n......\n```\n\n主要就是 `property_account_receivable_id` 中的 `company_dependent=True`.\n\n在 view 的介面底下是可以看到這個欄位\n\n![alt tag](https://i.imgur.com/fRZ4ioI.png)\n\n如果你有多公司, 切換到其他的公司, 你會發現這個欄位有可能會變空的.\n\n(切換公司時, 內容也會不一樣, 注意這邊是相同的 record)\n\n![alt tag](https://i.imgur.com/HCFwDd3.png)\n\n但你在 db 中的 `demo_company` 不會有 `property_account_receivable_id` 的欄位\n\n![alt tag](https://i.imgur.com/fz0aK8h.png)\n\n這邊你可能會覺得很奇怪, 但這就是他特別的地方 :smile:\n\n那他會存在哪裡呢 :question:\n\n他會存在 `ir_property` 中\n\n![alt tag](https://i.imgur.com/I9T8uVx.png)\n\n在後台的部份, 也可以看到 `ir_property` 的東西\n\n路徑在 `Technical -> Parameters -> Company Properties`\n\n![alt tag](https://i.imgur.com/Xf55Oip.png)\n\n裡面的值, 紀錄著 model 和 id\n\n![alt tag](https://i.imgur.com/k3VIApY.png)\n\n然後請把 Resource(res_id) 手動清空,\n\n![alt tag](https://i.imgur.com/AZir7v0.png)\n\n等下要示範使用以下的 code 來取值\n\n```python\nself.env['ir.property'].with_context(force_company=self.company_id.id).get('property_account_receivable_id', 'demo.company')\n```\n\n要清空的原因是因為其他的 Company Properties 很多是使用 code 的方式產生的\n\n原始碼中的 `odoo/addons/account/models/chart_template.py`\n\n```python\n@api.multi\ndef generate_properties(self, acc_template_ref, company):\n    ......\n    PropertyObj = self.env['ir.property']\n    todo_list = [\n        ('property_account_receivable_id', 'res.partner', 'account.account'),\n        ('property_account_payable_id', 'res.partner', 'account.account'),\n        ('property_account_expense_categ_id', 'product.category', 'account.account'),\n        ('property_account_income_categ_id', 'product.category', 'account.account'),\n        ('property_account_expense_id', 'product.template', 'account.account'),\n        ('property_account_income_id', 'product.template', 'account.account'),\n    ]\n```\n\n`property_account_receivable_id` `property_account_payable_id` ... 都是 code 產生的.\n\n(這些透過 code 產生的值 res_id 都是 False)\n\n而 Resource(res_id) 清空的原因則是因為原始碼中的 `odoo/addons/base/models/ir_property.py`\n\n```python\n......\n\ndef _get_property(self, name, model, res_id):\n    domain = self._get_domain(name, model)\n    if domain is not None:\n        domain = [('res_id', '=', res_id)] + domain\n        #make the search with company_id asc to make sure that properties specific to a company are given first\n        return self.search(domain, limit=1, order='company_id')\n    return self.browse(())\n\n......\n```\n\n通常 domain 這邊的 res_id 會是 False. (如果有值就會被過濾掉)\n\n```python\nclass DemoCompany(models.Model):\n    _name = 'demo.company'\n    _description = 'Demo Company'\n\n    ......\n\n    def action_get_default_account(self):\n        default_account = self.env['ir.property'].with_context(force_company=self.company_id.id).get('property_account_receivable_id', 'demo.company')\n        _logger.warning(default_account)\n        _logger.warning(self.property_account_receivable_id)\n        _logger.warning('============= HELLO ==================')\n\n```\n\n點選這個按鈕可以觸發這個 function\n\n![alt tag](https://i.imgur.com/X5xPuZ5.png)\n\n你會發現輸出一樣的資訊\n\n一個是透過 company_id 找出當下對應的 model 中的 `property_account_receivable_id`,\n\n另一個則是直接呼叫 model 中的 `property_account_receivable_id`.\n\n![alt tag](https://i.imgur.com/k5KTLXH.png)\n\n當你嘗試著新增 record, 你會發現 `property_account_receivable_id` 預設都會有值\n\n(這也是為甚麼我和大家說要清空 res_id 的原因, 因為 odoo 的 code 中是這樣設定的, 前面有說明)\n\n![alt tag](https://i.imgur.com/e4vLm0A.png)\n"
  },
  {
    "path": "demo_multi_company/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_multi_company/__manifest__.py",
    "content": "{\n    'name': \"demo multi company\",\n\n    'summary': \"\"\"\"\"\",\n\n    'description': \"\"\"\"\"\",\n\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['account'],\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'views/menu.xml',\n        'views/view.xml',\n    ],\n    'application': True,\n}\n"
  },
  {
    "path": "demo_multi_company/models/__init__.py",
    "content": "from . import model\n"
  },
  {
    "path": "demo_multi_company/models/model.py",
    "content": "from odoo import models, fields\nimport logging\n\n_logger = logging.getLogger(__name__)\n\nclass DemoCompany(models.Model):\n    _name = 'demo.company'\n    _description = 'Demo Company'\n\n    name = fields.Char('Description', required=True)\n\n    property_account_receivable_id = fields.Many2one('account.account',\n        company_dependent=True,\n        string=\"Account Receivable\",\n        domain=\"[('internal_type', '=', 'receivable'), ('deprecated', '=', False)]\",\n        required=True)\n\n    company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, default=lambda self: self.env.user.company_id)\n\n    def action_get_default_account(self):\n        default_account = self.env['ir.property'].with_context(force_company=self.company_id.id).get('property_account_receivable_id', 'demo.company')\n        _logger.warning(default_account)\n        _logger.warning(self.property_account_receivable_id)\n        _logger.warning('============= HELLO ==================')\n"
  },
  {
    "path": "demo_multi_company/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_company_user,Demo Company Tutorial User Access,model_demo_company,demo_company_group_user,1,1,1,1\n\n"
  },
  {
    "path": "demo_multi_company/security/security.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n\n  <record id=\"module_demo_company_tutorial\" model=\"ir.module.category\">\n    <field name=\"name\">Demo company tutorial category</field>\n  </record>\n\n  <record id=\"demo_company_group_user\" model=\"res.groups\">\n    <field name=\"name\">User</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_company_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('base.group_user'))]\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_multi_company/views/menu.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <!-- Demo Company Menu -->\n    <menuitem id=\"demo_company_menu\"\n      name=\"Demo Company\" />\n\n    <!-- Action to open the demo_company list -->\n    <act_window id=\"action_demo_company\"\n      name=\"Demo Company\"\n      res_model=\"demo.company\"\n      view_mode=\"tree,form\"/>\n\n    <!-- Menu item to open the demo_company list -->\n    <menuitem id=\"menu_demo_company\"\n      name=\"Demo Company\"\n\t    action=\"action_demo_company\"\n      parent=\"demo_company_menu\" />\n</odoo>\n\n\n"
  },
  {
    "path": "demo_multi_company/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n  <record id=\"view_form_demo_company\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Company Form</field>\n    <field name=\"model\">demo.company</field>\n    <field name=\"arch\" type=\"xml\">\n      <form>\n        <header>\n          <button name=\"action_get_default_account\" string=\"Get Default Account\" type=\"object\" class=\"oe_highlight o_expense_sheet_submit\"/>\n        </header>\n        <group>\n          <field name=\"name\"/>\n          <field name=\"property_account_receivable_id\"/>\n          <field name=\"company_id\"/>\n        </group>\n      </form>\n    </field>\n  </record>\n\n  <record id=\"view_tree_demo_company\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Company List</field>\n    <field name=\"model\">demo.company</field>\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n        <field name=\"name\"/>\n        <field name=\"company_id\"/>\n      </tree>\n    </field>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_odoo_tutorial/README.md",
    "content": "# odoo 手把手建立第一個 addons\n\n* [Youtube Tutorial - odoo 手把手建立第一個 addons - part1](https://youtu.be/GMrPakLNh8g) - 介紹 model - [文章快速連結](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#%E4%BB%8B%E7%B4%B9-model)\n\n* [Youtube Tutorial - odoo 手把手建立第一個 addons - part2](https://youtu.be/EnD-VxuILWM) - 介紹 security, menu, tree, form - [文章快速連結](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#%E4%BB%8B%E7%B4%B9-security-menu-tree-form)\n\n* [Youtube Tutorial - odoo 手把手建立第一個 addons - part3](https://youtu.be/25MSbidCf1U) - 介紹 report, controller - [文章快速連結](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#%E4%BB%8B%E7%B4%B9-report-controller)\n\n* [Youtube Tutorial - Odoo Controller Website 教學](https://youtu.be/nfq0Uo455Vc) - [文章快速連結](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#odoo-controller-website-%E6%95%99%E5%AD%B8)\n\n* [Youtube Tutorial - Odoo Qweb 教學](https://youtu.be/FE9lvN62aTo) - [文章快速連結](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#odoo-qweb-%E6%95%99%E5%AD%B8)\n\n* [Youtube Tutorial - 說明 odoo manifest 中的 auto_install](https://youtu.be/xTezPfJAJ_Q) - [文章快速連結](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#%E8%AA%AA%E6%98%8E-odoo-manifest-%E4%B8%AD%E7%9A%84-auto_install)\n\n* [Youtube Tutorial - odoo testing 教學](https://youtu.be/nfiBgXgYkYg) - [文章快速連結](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#odoo-testing-%E6%95%99%E5%AD%B8)\n\n* [進階 - Youtube Tutorial - 使用 SQL VIEW 定義 model](https://youtu.be/LPigYLtxeoA) - [文章快速連結](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial#%E4%BD%BF%E7%94%A8-sql-view-%E5%AE%9A%E7%BE%A9-model)\n\n* [Youtube Tutorial - odoo 使用 RAW SQL 說明](https://youtu.be/hfOLmoIfO9E) - [文章快速連結](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)\n\n建議觀看影片, 會更清楚 :smile:\n\n以下將介紹這個 addons 的結構\n\n## 說明\n\n### 介紹 model\n\n* [Youtube Tutorial - odoo 手把手建立第一個 addons - part1](https://youtu.be/GMrPakLNh8g) - 介紹 model\n\n首先是 `__manifest__.py`, 比較重要的是 `depends`,\n\n```python\n{\n    'name': \"demo odoo tutorial\",\n    ......\n\n    # any module necessary for this one to work correctly\n    'depends': ['base', 'mail'],\n\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'data/data_demo_odoo.xml',\n        'views/menu.xml',\n        'views/view.xml',\n        'reports/report.xml',\n        'views/demo_odoo_template.xml',\n    ],\n    ......\n}\n\n```\n\n在 odoo 的世界中, 一定會看到某個 addons 依賴 xxx addons, 想簡單一點,\n\n你可以把它想成是模組化(方便管理), `data` 的部份我等等再回來介紹.\n\n看 [models](models) 資料夾, 裡面有 `__init__.py` 和 `models.py`,\n\n`__init__.py` 單純就是 import `models.py` 而已.\n\n`models.py` 這邊就很重要了\n\n```python\nclass DemoOdooTutorial(models.Model):\n    _name = 'demo.odoo.tutorial'\n    _description = 'Demo Odoo Tutorial'\n    _inherit = ['mail.thread', 'mail.activity.mixin'] # track_visibility\n```\n\nodoo 中的 model 主要有幾個, 分別是 AbstractModel、Model、TransientModel,\n\n最基本的 BaseModel, 其實 BaseModel = AbstractModel,\n\n[https://www.odoo.com/documentation/13.0/reference/orm.html#abstractmodel](https://www.odoo.com/documentation/13.0/reference/orm.html#abstractmodel)\n\n```python\n@pycompat.implements_to_string\nclass BaseModel(MetaModel('DummyModel', (object,), {'_register': False})):\n    \"\"\" Base class for Odoo models.\n\n    Odoo models are created by inheriting:\n\n    *   :class:`Model` for regular database-persisted models\n\n    *   :class:`TransientModel` for temporary data, stored in the database but\n        automatically vacuumed every so often\n\n    *   :class:`AbstractModel` for abstract super classes meant to be shared by\n        multiple inheriting models\n\n    The system automatically instantiates every model once per database. Those\n    instances represent the available models on each database, and depend on\n    which modules are installed on that database. The actual class of each\n    instance is built from the Python classes that create and inherit from the\n    corresponding model.\n\n    Every model instance is a \"recordset\", i.e., an ordered collection of\n    records of the model. Recordsets are returned by methods like\n    :meth:`~.browse`, :meth:`~.search`, or field accesses. Records have no\n    explicit representation: a record is represented as a recordset of one\n    record.\n\n    To create a class that should not be instantiated, the _register class\n    attribute may be set to False.\n    \"\"\"\n    ....\n```\n\n今天只會先提到 Model, `Model` 繼承自 AbstractModel\n\n```python\nclass Model(AbstractModel):\n    \"\"\" Main super-class for regular database-persisted Odoo models.\n\n    Odoo models are created by inheriting from this class::\n\n        class user(Model):\n            ...\n\n    The system will later instantiate the class once per database (on\n    which the class' module is installed).\n    \"\"\"\n    _auto = True                # automatically create database backend\n    _register = False           # not visible in ORM registry, meant to be python-inherited only\n    _abstract = False           # not abstract\n    _transient = False          # not transient\n```\n\n`_auto = True` 會自動在 db 中建立 table.\n\n`_name` 為 model 的名稱, 請注意幾件事情, model 名稱建議都使用單數, 然後不要使用 `_` 分隔名稱,\n\n請使用 `.` 像是範例中的 `demo.odoo.tutorial` (在 db 中, table 名稱會顯示 `demo_odoo_tutorial`, 如下圖)\n\n![alt tag](https://i.imgur.com/s6ngYGo.png)\n\n`_inherit` 在 odoo 中不管是 model 還是 view, 甚至是權限, 都會使用繼承 (這邊先知道這樣即可 :smile:).\n\n再來說明 field\n\n```python\n    ......\n    name = fields.Char('Description', required=True)\n\n    # track_visibility='always' 和 track_visibility='onchange'\n    is_done_track_onchange = fields.Boolean(\n        string='Is Done?', default=False, track_visibility='onchange')\n    name_track_always = fields.Char(string=\"track_name\", track_visibility='always')\n\n    start_datetime = fields.Datetime('Start DateTime', default=fields.Datetime.now())\n    stop_datetime = fields.Datetime('End Datetime')\n\n    field_onchange_demo = fields.Char('onchange_demo')\n    field_onchange_demo_set = fields.Char('onchange_demo_set', readonly=True)\n\n    # float digits\n    # field tutorial\n    input_number = fields.Float(string='input number', digits=(10,3))\n    ......\n```\n\n`track_visibility` 為追蹤值的改變, 這也是為甚麼要繼承 `mail.thread` 以及 `mail.activity.mixin` 的原因,\n\n如果你有修改值, 會紀錄改變(如下圖),\n\n![alt tag](https://i.imgur.com/XjCwGHQ.png)\n\n`start_datetime` field 有 default, 設定為當天的時間,\n\n當建立一筆資料時, 會顯示當下的時間,\n\n![alt tag](https://i.imgur.com/VYPAz9S.png)\n\n`field_onchange_demo_set` field 中的 `readonly=True`,\n\n你可以發現是無法修改的 (可能是根據其他欄位透過 code 改變它的值)\n\n![alt tag](https://i.imgur.com/1A5RIDH.png)\n\n`input_number` Float field 中的 digits 為設定進位以及小數點, 像這邊是算到小數點第3位並使用10進位\n\n![alt tag](https://i.imgur.com/0ZXafMi.png)\n\n下一部份的 code\n\n```python\n......\nfield_compute_demo = fields.Integer(compute=\"_get_field_compute\") # readonly\n\n# field_compute_demo = fields.Integer(compute=\"_get_field_compute\",\n#                                     inverse=\"_set_input_number\",\n#                                     search=\"_search_upper\")\n\n    _sql_constraints = [\n        ('name_uniq', 'unique(name)', 'Description must be unique'),\n    ]\n\n    @api.constrains('start_datetime', 'stop_datetime')\n    def _check_date(self):\n        for data in self:\n            if data.start_datetime > data.stop_datetime:\n                raise ValidationError(\n                    \"data.stop_datetime  > data.start_datetime\"\n                )\n\n    @api.depends('input_number')\n    def _get_field_compute(self):\n        for data in self:\n            data.field_compute_demo = data.input_number * 1000\n\n    def _set_input_number(self):\n        for data in self:\n            data.input_number = data.field_compute_demo / 1000\n\n    def _search_upper(self, operator, value):\n        return [('input_number', operator, value)]\n\n    @api.onchange('field_onchange_demo')\n    def onchange_demo(self):\n        if self.field_onchange_demo:\n            self.field_onchange_demo_set = 'set {}'.format(self.field_onchange_demo)\n......\n\n```\n\n`field_compute_demo` field 為 compute field, compute field 預設為 readonly,\n\n而且這個 field 預設是不會存在 db 中的 (`store=False`, 也就是每次都是計算出來的),\n\n如果想要將值保存在 db 中, 需再加上 `store=True`.\n\n如果你設定 `store=False` (或是沒指定),\n\n當你去搜尋 `field_compute_demo` 時, 會發現錯誤,\n\n```cmd\n>>> self.env['demo.odoo.tutorial'].search([('field_compute_demo', '>', 1)])\n2022-10-08 14:50:42,851 15224 ERROR odoo odoo.osv.expression: Non-stored field demo.odoo.tutorial.field_compute_demo cannot be searched.\ndemo.odoo.tutorial(1,2)\n```\n\n雖然有撈出資料, 但是是撈出全部的資料(剛好裡面全部的資料有兩筆).\n\n原因很簡單, 因為這個 field 是 compute 出來的, 在 table 中也沒有這個欄位,\n\n所以不能搜尋.\n\n如果你想要搜尋, 一種簡單方法是設定 `store=True`, 但這種方法不一定是好的 :confused:\n\n(因為如果亂設很可能造成效能上的影響).\n\n另一種方法比較麻煩, 透過定義 `search` 完成,\n\n官方文件可參考 [Computed fields](https://www.odoo.com/documentation/12.0/developer/reference/orm.html#computed-fields)\n\n透過 `search` 去定義邏輯, 根據其他的 filed 欄位(或邏輯)搜尋出想要的結果,\n\n```python\n......\nfield_compute_demo = fields.Integer(compute=\"_get_field_compute\",\n                                    inverse=\"_set_input_number\",\n                                    search=\"_search_upper\")\n......\n\ndef _search_upper(self, operator, value):\n    return [('input_number', operator, value)]\n```\n\n定義完 `search` 之後, 就可以正常對 `field_compute_demo` 搜尋了 :smile:\n\n```cmd\n>>> self.env['demo.odoo.tutorial'].search([('field_compute_demo', '=', 2)])\ndemo.odoo.tutorial(2,)\n```\n\n前面有說到 compute field 預設為 readonly,\n\n如果今天想要讓他可以編輯, 該怎麼做呢 :question:\n\n需要定義 `inverse`,\n\n```python\n......\nfield_compute_demo = fields.Integer(compute=\"_get_field_compute\",\n                                    inverse=\"_set_input_number\",\n                                    search=\"_search_upper\")\n......\n\ndef _set_input_number(self):\n    for data in self:\n        data.input_number = data.field_compute_demo / 1000\n```\n\n定義完之後, 就可以對 `field_compute_demo` 進行編輯,\n\n任意改 `input_number` 或 `field_compute_demo` 都可以互相 trigger.\n\n`compute` 為 `_get_field_compute`, 透過 `@api.depends` 裝飾器的幫忙,\n\n這邊會根據 `input_number` field 的值 * 1000 之後,\n\n將值餵給 `field_compute_demo`.\n\n![alt tag](https://i.imgur.com/FQOPZTH.png)\n\n特別補充說明一下 onchange 也可以 return 一個 dict.\n\n```python\n......\n    @api.onchange('field_onchange_demo')\n    def onchange_demo(self):\n        ......\n\n        # warning message\n        result = dict()\n        result['warning'] = {\n            'title': 'HELLO',\n            'message': 'I am warning'\n        }\n        return result\n```\n\n透過上方的寫法, 使用者會跳出提醒視窗 (但不會中斷使用者)\n\n![alt tag](https://i.imgur.com/hO9rE2y.png)\n\n`_sql_constraints` 這個為設定一些限制(直接寫 postgresql),\n\n避免不允許(錯誤)的資料進入 db,\n\n像這邊設定 `name` field 必須為 unique,\n\n假如你有重複的 `name`, 系統就會提醒你(如下圖),\n\n![alt tag](https://i.imgur.com/4fOtkpJ.png)\n\n`def _check_date(self)` 這段是另一種方式限制, 透過 `@api.constrains` 裝飾器的幫忙,\n\n這邊限制了 `start_datetime` 必須大於 `stop_datetime`, 否則會出現 error,\n\n![alt tag](https://i.imgur.com/PtSLNjx.png)\n\n`def onchange_demo(self)` 這個則是使用了 `@api.onchange` 裝飾器的幫忙,\n\n主要是根據 `field_onchange_demo` 的改變, 將值餵給 `field_onchange_demo_set`,\n\n注意 view 中要有 `force_save=\"1\"`, 否則儲存時會消失.\n\n(原因是因為 `field_onchange_demo_set` 設定為 `readonly` 的關係)\n\n![alt tag](https://i.imgur.com/5Iq4Rgb.png)\n\n你可能會發現 `@api.depends` 和 `@api.onchange` 幾乎一樣,\n\n其實主要區分兩個比較容易的方法, 就是 `@api.depends` 可以使用在 `related` 欄位,\n\n像是之後會介紹的 `Many2one` `Many2many` `One2many` 之類的.\n\n而 `@api.onchange` 只能使用在同一個 model 上.\n\n### 介紹 security, menu, tree, form\n\n* [Youtube Tutorial - odoo 手把手建立第一個 addons - part2](https://youtu.be/EnD-VxuILWM) - 介紹 security, menu, tree, form\n\n接下來來看 [security](security) 這個很重要的資料夾, 既然有了 model,\n\n這樣要如何控制誰有權限讀寫修改刪除呢 :question:\n\n就是依靠 `ir.model.access.csv` 和 `security.xml` 這個檔案 :exclamation:\n\n`security.xml`\n\n```xml\n  <record id=\"module_demo_odoo_tutorial\" model=\"ir.module.category\">\n    <field name=\"name\">Demo odoo tutorial category</field>\n  </record>\n\n  <record id=\"demo_odoo_tutorial_group_user\" model=\"res.groups\">\n    <field name=\"name\">User</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_odoo_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('base.group_user'))]\"/>\n  </record>\n\n  <record id=\"demo_odoo_tutorial_group_manager\" model=\"res.groups\">\n    <field name=\"name\">Manager</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_odoo_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('demo_odoo_tutorial_group_user'))]\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n```\n\n通常會先建立一個 category, 然後建立兩個 group, 分別是 User 和 Manager,\n\n這邊使用線性的繼承方式, 也就是 Manager 擁有 User 一切的權限.\n\n`implied_ids` 也就是繼承, 裡面的數字分別代表不同的意思,\n\n```text\n\n(0, _ , {'field': value}) creates a new record and links it to this one.\n(1, id, {'field': value}) updates the values on an already linked record.\n(2, id, _) removes the link to and deletes the id related record.\n(3, id, _) removes the link to, but does not delete, the id related record. This is usually what you will use to delete related records on many-to-many fields.\n(4, id, _) links an already existing record.\n(5, _, _) removes all the links, without deleting the linked records.\n(6, _, [ids]) replaces the list of linked records with the provided list.\n```\n\n`_` 也可以改成 `0` or `False`,\n\n尾巴不相關的可以忽略, 像是 `(4, id, _)` 也可以寫成 `(4, id)`.\n\n`ir.model.access.csv` 為管理 user 和 manager CRUD 的權限,\n\n```csv\nid,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_odoo_user,Demo Odoo Tutorial User Access,model_demo_odoo_tutorial,demo_odoo_tutorial_group_user,1,0,0,0\naccess_demo_odoo_manager,Demo Odoo Tutorial Manager Access,model_demo_odoo_tutorial,demo_odoo_tutorial_group_manager,1,1,1,1\n```\n\n比較需要注意的地方是 model_id 的部份, 像這邊的 model 為 `demo.odoo.tutorial`,\n\n但這邊必須填入 `model_demo_odoo_tutorial`, 規則很簡單, 就是要前面要補上 `model`,\n\n然後將全部的 `.` 改成 `_` .\n\ngroup_id 的部份可以空白, 請看下面這個例子,\n\n代表這個 Access Rights 沒特別指定 group (但通常比較少這樣使用)\n\n```csv\nid,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_test,Test Access,model_demo_odoo_tutorial,,1,1,1,1\n```\n\n如果你去 odoo 後台的 Access Rights 查詢, 他會顯示黃色的.\n\n![alt tag](https://i.imgur.com/bGP9Fhb.png)\n\n到 user 中可以切換 group,\n\n![alt tag](https://i.imgur.com/CclfzfB.png)\n\n接著來看 [views](views) 資料夾,\n\n先看 `menu.xml`\n\n```xml\n    <!-- demo_odoo_tutorial App Menu -->\n    <menuitem id=\"demo_odoo_tutorial_menu\"\n              name=\"Demo Odoo Tutorial\" />\n\n    <!-- Action to open the demo_odoo_tutorial -->\n    <act_window id=\"action_odoo_tutorial\"\n      name=\"Demo Odoo Tutorial Action\"\n      res_model=\"demo.odoo.tutorial\"\n      view_mode=\"tree,form\"/>\n\n    <!-- Menu item to open the demo_odoo_tutorial -->\n    <menuitem id=\"menu_odoo_tutorial\"\n              name=\"Demo Odoo Tutorial\"\n              action=\"action_odoo_tutorial\"\n              parent=\"demo_odoo_tutorial_menu\" />\n```\n\n建立一個 menuitem, 然後去定義它的 Action, Action 中比較重要的是 `res_model` 和 `view_mode`,\n\n`res_model` 就填入對應的 model, `view_mode` 先簡單填入 tree 和 form,\n\n在 odoo 中有很多 view, 像是 pivot kanban 之類的.\n\n再來看 `view.xml`,\n\n這邊指定兩個最簡單的,\n\n首先是 tree, 記得將對應的 model 填進去,\n\n```xml\n......\n  <record id=\"view_tree_demo_odoo_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Odoo Tutorial List</field>\n    <field name=\"model\">demo.odoo.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n        <field name=\"name\"/>\n        <field name=\"name_track_always\"/>\n        <field name=\"is_done_track_onchange\"/>\n        <field name=\"start_datetime\"/>\n        <field name=\"stop_datetime\"/>\n      </tree>\n    </field>\n  </record>\n......\n```\n\ntree 如下\n\n![alt tag](https://i.imgur.com/Kz2iniQ.png)\n\n接著是 form, 記得將對應的 model 填進去,\n\n(如果都沒寫, 系統會自己產生對應的 form view, 但很醜 :sob:)\n\n```xml\n......\n<record id=\"view_form_demo_odoo_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Odoo Tutorial Form</field>\n    <field name=\"model\">demo.odoo.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Odoo Tutorial\">\n        <sheet>\n          <group>\n            <field name=\"name\"/>\n            <field name=\"name_track_always\"/>\n            <field name=\"is_done_track_onchange\"/>\n            <field name=\"start_datetime\"/>\n            <field name=\"stop_datetime\"/>\n            <field name=\"field_onchange_demo\"/>\n            <field name=\"field_onchange_demo_set\" force_save=\"1\"/>\n            <!-- <field name=\"input_number\" widget=\"percentage\"/> -->\n            <field name=\"input_number\"/>\n            <field name=\"field_compute_demo\"/>\n          </group>\n        </sheet>\n        <div class=\"oe_chatter\">\n          <field name=\"message_follower_ids\" widget=\"mail_followers\"/>\n          <field name=\"activity_ids\" widget=\"mail_activity\"/>\n          <field name=\"message_ids\" widget=\"mail_thread\"/>\n        </div>\n      </form>\n    </field>\n</record>\n......\n```\n\nform 如下\n\n![alt tag](https://i.imgur.com/vuQd9Bx.png)\n\n請注意最後一段的 `message_follower_ids` `activity_ids` `message_ids`,\n\n這並不是我們所建立的 field, 而是繼承 `mail.thread` `mail.activity.mixin` 所擁有的,\n\n這段 code 主要是產生這個區塊\n\n![alt tag](https://i.imgur.com/FOPV6i5.png)\n\n最後回到 `__manifest__.py` 中, 記得將對應的路徑填入 `data` 中,\n\n```python\n# always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'data/data_demo_odoo.xml',\n        'views/menu.xml',\n        'views/view.xml',\n        'reports/report.xml',\n        'views/demo_odoo_template.xml',\n    ],\n```\n\n接著來看 [data/data_demo_odoo.xml](data/data_demo_odoo.xml),\n\n```xml\n  <record id=\"demo_odoo_1\" model=\"demo.odoo.tutorial\">\n      <field name=\"name\">demo_odoo_1</field>\n      <field name=\"name_track_always\">demo_name_track_always_1</field>\n      <field name=\"is_done_track_onchange\">True</field>\n  </record>\n\n  <record id=\"demo_odoo_2\" model=\"demo.odoo.tutorial\">\n      <field name=\"name\">demo_odoo_2</field>\n      <field name=\"name_track_always\">demo_name_track_always_2</field>\n      <field name=\"is_done_track_onchange\">True</field>\n  </record>\n```\n\n這邊做的事情就是當你安裝了 addons, 它會預設幫你建立一些相關的資料.\n\n注意 :exclamation: 它和 [demo/demo.xml](demo/demo.xml) 資料夾不太一樣, demo 資料夾是當你有勾選\n\n產生 demo 資料時, 你安裝 addons 會自動產生 demo data (如下圖).\n\n![alt tag](https://i.imgur.com/LbcOiJL.png)\n\n### 介紹 report, controller\n\n* [Youtube Tutorial - odoo 手把手建立第一個 addons - part3](https://youtu.be/25MSbidCf1U) - 介紹 report, controller\n\n再來是報表的部份 [reports/report.xml](reports/report.xml),\n\n這邊是定義 report 的 template,\n\n`t-as=\"o\"` 你可以定義你喜歡的變數\n\n```xml\n    <template id=\"report_demo_odoo_tutorial\">\n        <t t-call=\"web.html_container\">\n            <t t-foreach=\"docs\" t-as=\"o\">\n                <t t-call=\"web.external_layout\">\n                    <div class=\"page\">\n                        <h2>Odoo Report</h2>\n                        <div>\n                            <strong>Name:</strong>\n                            <p t-field=\"o.name\"/>\n                        </div>\n                        <div>\n                            <strong>Name_track_always:</strong>\n                            <p t-field=\"o.name_track_always\"/>\n                        </div>\n                    </div>\n                </t>\n            </t>\n        </t>\n    </template>\n\n    ......\n```\n\n後面這段則是定義 report 的檔名, report_type, 指定 model\n\n```xml\n    <report\n        id=\"action_report_demo\"\n        string=\"Demo Report\"\n        model=\"demo.odoo.tutorial\"\n        report_type=\"qweb-pdf\"\n        name=\"demo_odoo_tutorial.report_demo_odoo_tutorial\"\n        file=\"demo_odoo_tutorial.report_demo_odoo_tutorial\"\n        print_report_name=\"'Demo Report - %s' % ((object.name).replace('/', ''))\"\n    />\n```\n\n\n記得要將路徑填入 `__manifest__.py`\n\n```python\n'data': [\n        ......\n        'reports/report.xml',\n        ......\n    ],\n```\n\n會顯示在這邊\n\n![alt tag](https://i.imgur.com/nHf4bxy.png)\n\n報表如下\n\n![alt tag](https://i.imgur.com/VoY55io.png)\n\n再來是 [controllers](controllers) 這個資料夾,\n\n如果你學過 Django,Flask 你會發現蠻像的 :smile:\n\n因為就是定義 route , 然後撈資料, 最後回傳到對應的 view,\n\n(記得要將 controller 填入 `__init__.py` 中)\n\n[controllers/controllers.py](controllers/controllers.py)\n\n```python\nclass DemoOdoo(http.Controller):\n\n    @http.route('/demo/odoo', auth='user')\n    def list(self, **kwargs):\n        obj = http.request.env['demo.odoo.tutorial']\n        objs = obj.search([])\n        return http.request.render(\n            'demo_odoo_tutorial.demo_odoo_template',{'objs': objs})\n```\n\n至於它的 view, 在 [views/demo_odoo_template.xml](views/demo_odoo_template.xml)\n\n```xml\n<template id=\"demo_odoo_template\" name=\"Demo odoo List\">\n  <div id=\"wrap\" class=\"container\">\n    <h1>Demo Odoo</h1>\n      <t t-foreach=\"objs\" t-as=\"obj\">\n        <div class=\"row\">\n          <span t-field=\"obj.name\" />,\n          <span t-field=\"obj.is_done_track_onchange\" />,\n          <span t-field=\"obj.name_track_always\" />\n        </div>\n      </t>\n  </div>\n</template>\n```\n\nroute 我們定義是 `@http.route('/demo/odoo', auth='user')`,\n\n`auth='user'` 代表要登入才可以觀看, 所以只要瀏覽 [http://0.0.0.0:8069/demo/odoo/](http://0.0.0.0:8069/demo/odoo/)\n\n就會看到下圖,\n\n![alt tag](https://i.imgur.com/kHYQhGR.png)\n\n### Odoo Controller Website 教學\n\n接著介紹在 Controller 中設定 `website=True`,\n\n* [Youtube Tutorial - Odoo Controller Website 教學](https://youtu.be/nfq0Uo455Vc)\n\n首先, 將你的 [controllers/controllers.py](controllers/controllers.py) 加上 `website=True`\n\n```python\nclass DemoOdoo(http.Controller):\n\n    @http.route('/demo/odoo', auth='user', website=True)\n    def list(self, **kwargs):\n        print(http.request.website.id)\n        ......\n```\n\n`print(http.request.website.id)` 這邊稍微注意一下,\n\n一定要設定 `website=True`, 才會有 website_id.\n\n然後在 [views/demo_odoo_template.xml](views/demo_odoo_template.xml) 中呼叫 `t-call=\"website.layout\"`\n\n```xml\n<template id=\"demo_odoo_template\" name=\"Demo odoo List\">\n  <t t-call=\"website.layout\">\n    ......\n  </t>\n</template>\n```\n\n`__manifest__.py` 中也請記得加入 `website` depend,\n\n這樣就會加上 odoo website 的模版了 :smile:\n\n![alt tag](https://i.imgur.com/xC8SqxZ.png)\n\n### Odoo Qweb 教學\n\n除了這些, 在 QWeb 中還可以實作出不少變化 :smile:\n\n* [Youtube Tutorial - Odoo Qweb 教學](https://youtu.be/FE9lvN62aTo)\n\nreport 和 controller 中的 view 都是 QWeb, 以下使用 report 中的 view 示範,\n\n繼續來看 [views/demo_odoo_template.xml](views/demo_odoo_template.xml)\n\n```xml\n<template id=\"report_demo_odoo_tutorial\">\n    <t t-call=\"web.html_container\">\n        <t t-foreach=\"docs\" t-as=\"o\">\n            <t t-call=\"web.external_layout\">\n                <div class=\"page\">\n                    ......\n                    <div>\n                        <strong>start datetime:</strong>\n                        <p t-field=\"o.start_datetime\"/>\n                    </div>\n                    <div>\n                        <strong>stop datetime:</strong>\n                        <p t-field=\"o.stop_datetime\" t-options='{\"format\": \"Y/MM/dd\"}'/>\n                    </div>\n                    <div>\n                        <strong>custom start datetime:</strong>\n                        <p t-esc=\"o.get_custom_portal_date()\"/>\n                    </div>\n                </div>\n            </t>\n        </t>\n    </t>\n</template>\n```\n\n可以透過 `t-options='{\"format\": \"Y/MM/dd\"}'` 來改變日期格式.\n\n也可以透過 model 的方式設定新的邏輯 `t-esc=\"o.get_custom_portal_date()\"`\n\nmodel 實作的部份請參考 [models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_odoo_tutorial/models/models.py)\n\n```python\nclass DemoOdooTutorial(models.Model):\n    ......\n\n    start_datetime = fields.Datetime('Start DateTime', default=fields.Datetime.now())\n    ......\n\n    def get_custom_portal_date(self):\n        str_time = datetime.strftime(self.start_datetime, '%Y/%m/%d')\n        return '>{}<'.format(str_time)\n```\n\n效果如下圖\n\n![alt tag](https://i.imgur.com/9lxOsRK.png)\n\n### 說明 odoo manifest 中的 auto_install\n\n* [Youtube Tutorial - 說明 odoo manifest 中的 auto_install](https://youtu.be/xTezPfJAJ_Q)\n\n特別說明一下 `__manifest__.py` 裡的 `auto_install`,\n\n```python\n    'installable': True,\n    'auto_install': False,\n    'application': True,\n```\n\n`auto_install`\n\n這個值很重要, 如果你不懂, 建議設定 `False`, 原因是假如你設定為 `True`,\n\n它會找到你路徑的全部 addons 中的 `__manifest__.py` 裡找 depends,\n\n你其實可以把他想成是一種反向的依賴, 很容易不小心被它雷到 :scream:\n\n舉個例子來看這個問題, 當你安裝 `hr_expense` addons 時, `sale_expense` addons 會自動被安裝起來 :exclamation: :exclamation:\n\n`hr_expense` addons 看不到相關 depends,\n\n![alt tag](https://i.imgur.com/NW5efUr.png)\n\n`sale_expense` addons 可以看到相關 depends\n\n![alt tag](https://i.imgur.com/i5N52OT.png)\n\n也就是當你安裝 `hr_expense` 時, 因為 `sale_expense` 裡的 `'auto_install': True`,\n\n所以自動會把 `sale_expense` 裝起來.\n\n以下為 `sale_expense` 的 `__manifest__.py`\n\n```python\n{\n    'name': 'Sales Expense',\n    ......\n    'depends': ['sale_management', 'hr_expense'],\n    ......\n    'auto_install': True,\n}\n\n```\n\n### odoo testing 教學\n\n* [Youtube Tutorial - odoo testing 教學](https://youtu.be/nfiBgXgYkYg)\n\n在 odoo 的世界中, testing 也扮演一個很重要的角色, 今天就來介紹這個 testing :smile:\n\n詳細說明可參考 [testing](https://www.odoo.com/documentation/14.0/reference/testing.html).\n\n這邊只會介紹 python 端的 testing, js 的部份就請自行看文件 :smirk:\n\n`TransactionCase`\n\n每個 function 執行完畢後都會 roll back, 每個 function 都是獨立的不互相影響.\n\n`SingleTransactionCase`\n\n全部 function 執行完畢後才會 roll back, function 會互相影響.\n\n`SavepointCase`\n\n使用在比較大型以及複雜的測試, 通常會搭配 `setUpClass()` 使用, 這邊就不另外介紹,\n\n可自行使用關鍵字查看 source code 如何規劃 :smile:\n\n先來看 `TransactionCase`\n\n請參考 [demo_odoo_tutorial/tests/test_demo_odoo_transactioncase.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_odoo_tutorial/tests/test_demo_odoo_transactioncase.py)\n\n```python\nfrom odoo.exceptions import UserError, AccessError, ValidationError\nfrom odoo.tests.common import TransactionCase, tagged\n\n# @tagged('-standard', 'nice')\nclass TestDemoOdooTransactionCase(TransactionCase):\n\n    def setUp(self, *args, **kwargs):\n        \"\"\"setUp\"\"\"\n        super(TestDemoOdooTransactionCase, self).setUp(*args, **kwargs)\n        print('Run setUp')\n\n    def test_hello_world(self):\n        \"\"\"test_hello_world\"\"\"\n        self.assertEqual(0, 0, 'test hello world')\n\n    def test_datetime_validation(self):\n        \"\"\"test_datetime_validation\"\"\"\n        values = {\n            'name': 'hello',\n            'start_datetime': '2020-02-01',\n            'stop_datetime': '2020-01-01',\n        }\n        with self.assertRaises(ValidationError):\n            self.env['demo.odoo.tutorial'].create(values)\n\n    def test_field_compute_demo(self):\n        \"\"\"test_field_compute_demo\"\"\"\n        values = {\n            'name': 'hello',\n            'input_number': 2\n        }\n        data = self.env['demo.odoo.tutorial'].create(values)\n        self.assertEqual(data.field_compute_demo, data.input_number * 1000)\n```\n\n注意 `__init__.py` 需要 import `test_demo_odoo_transactioncase`,\n\ntests 資料夾底下的 testing 都必須是 `test_` 開頭的,\n\n執行方法為加上 `--test-enable`, 範例如下\n\n```cmd\npython3 odoo-bin -i demo_odoo_tutorial -d odoo -c /home/twtrubiks/work/odoo12/odoo/config/odoo.conf --test-enable\n```\n\n執行時你會看到下方的輸出\n\n![alt tag](https://i.imgur.com/GRBN7LJ.png)\n\n注意, 這裡有3個 testing `test_hello_world` `test_datetime_validation` `test_field_compute_demo`\n\n而 `setUp` 會被執行三次, 因為每執行一個測試 function, setUp 就會被執行一次.\n\n3個 testing function 也都是獨立的, 互相不干擾.\n\n接著來看 `SingleTransactionCase`\n\n請參考 [demo_odoo_tutorial/tests/test_demo_odoo_singletransactioncase](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_odoo_tutorial/tests/test_demo_odoo_singletransactioncase.py)\n\n```python\n......\nclass TestDemoOdooSingleTransactionCase(SingleTransactionCase):\n\n    def setUp(self, *args, **kwargs):\n        \"\"\"setUp\"\"\"\n        super(TestDemoOdooSingleTransactionCase, self).setUp(*args, **kwargs)\n        print('Run setUp')\n    ......\n```\n\n執行時你會看到下方的輸出\n\n![alt tag](https://i.imgur.com/M0Kq5a4.png)\n\n這個範例和 `test_demo_odoo_transactioncase.py` 是一模一樣的,\n\n只是將它改成繼承 `SingleTransactionCase`.\n\n但你會發現這個會出現錯誤, 原因是因為 model 中有設定 `name_uniq`\n\n```python\n_sql_constraints = [\n    ('name_uniq', 'unique(name)', 'Description must be unique'),\n]\n```\n\n而我們兩個 testing 的 name 名稱 (create name) 都是一樣的, 也就是\n\n`test_datetime_validation` `test_field_compute_demo`, 所以會發生錯誤.\n\n(在 `TransactionCase` 沒錯誤是因為它和 `SingleTransactionCase` 的特性不一樣)\n\n除了這些功能之外, 還可以透過 tagged 這個 decorator 來幫助我們完成其他的需求.\n\n如果不了解 decorator, 可參考 [What is the python decorator\n](https://github.com/twtrubiks/python-notes/tree/master/what_is_the_python_decorator)\n\n(記得將 tagged 的註解取消)\n\n```python\nfrom odoo.tests.common import TransactionCase, tagged\n\n@tagged('-standard', 'nice')\nclass TestDemoOdooTransactionCase(TransactionCase):\n\n......\n```\n\n如果沒有特別設定, odoo defaults 為 standard,\n\n`+` `-` 則代表啟用或不改用(排除), 像上面這個例子,\n\n代表只有在 `nice` tag 才會生效, 在 `standard` 中不會生效的,\n\n因為前面有加個 `-`, 更多詳細文件可參考 [invocation](https://www.odoo.com/documentation/14.0/reference/testing.html#invocation).\n\n範例指令,\n\n代表只執行有 `nice` tag 的測試,\n\n```cmd\npython3 odoo-bin -i demo_odoo_tutorial -d odoo -c /home/twtrubiks/work/odoo12/odoo/config/odoo.conf --test-enable --test-tags nice\n```\n\n代表執行有 `nice` 以及 `standard` tag 的測試,\n\n```cmd\npython3 odoo-bin -i demo_odoo_tutorial -d odoo -c /home/twtrubiks/work/odoo12/odoo/config/odoo.conf --test-enable --test-tags 'standard,nice'\n```\n\n### 使用 SQL VIEW 定義 model\n\n這部份是比較進階的, 如果你是新手, 請跳過這部份 :smirk:\n\n* [進階 - Youtube Tutorial - 使用 SQL VIEW 定義 model](https://youtu.be/LPigYLtxeoA)\n\n使用時機, 如果你有比較特別的報表, 或是特別的 pivot 使用原生的 ORM 可能比較不好實現,\n\n這時候可以考慮使用原生的 SQL 來完成.\n\n請參考 [models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_odoo_tutorial/models/models.py) 資料夾\n\n```python\n......\n\nclass DemoOdooTutorialStatistics(models.Model):\n    _name = 'demo.odoo.tutorial.statistics'\n    _description = 'Demo Odoo Tutorial Statistics'\n    _auto = False\n\n    create_uid = fields.Many2one('res.users', 'Created by', readonly=True)\n    average_input_number = fields.Float(string=\"Average Input Number\", readonly=True)\n\n    @api.model_cr\n    def init(self):\n        tools.drop_view_if_exists(self.env.cr, self._table)\n        query = \"\"\"\n        CREATE OR REPLACE VIEW demo_odoo_tutorial_statistics AS\n        (\n            SELECT\n                min(demo.id) as id,\n                create_uid,\n                avg(input_number) AS average_input_number\n            FROM\n                demo_odoo_tutorial AS demo\n            GROUP BY demo.create_uid\n        );\n        \"\"\"\n        self.env.cr.execute(query)\n```\n\n`_auto = False` 通常都不會去設定他, 也就是預設都是 True, 代表 table 會由 odoo\n\n幫助我們產生, 所以也不需要額外去實作 `init`.\n\n但這邊設定了 `_auto = False` 代表我們要自己去管, 而不是由 odoo 協助,\n\n當然, 也需要我們自己去維護, 必須實作 `init`.\n\n定義 fields 的部份, 就看你需要 `demo.odoo.tutorial` 中的哪些資料,\n\n把需要的 fields 填上即可, 又或是透過 `compute` 自己實現邏輯.\n\n( 這邊都設定 `readonly=True`, 因為 VIEW 本來就應該是唯讀的,\n\n如果你不了解, 可 google TABLE VS VIEW, 他們是不一樣的 )\n\n而在 `init` 中, 透過 SQL 實作一個 VIEW.\n\n也請記得必須補上, `menu.xml` `view.xml` `ir.model.access.csv`.\n\n這邊使用 pivot 來呈現,\n\n當我們更新或裝上這個 addons 的時候, 可以先透過 pgadmin4 查看 db,\n\n注意 :exclamation: 我們看的是 VIEW, 不是 TABLE\n\n![alt tag](https://i.imgur.com/S8koB3r.png)\n\n這個 VIEW 的 code 就是在 `init` 定義的,\n\n![alt tag](https://i.imgur.com/nGbTNva.png)\n\n這邊補充一下為甚麼要使用 `min(demo.id) as id,`,\n\n原因是 odoo 規定要產生 id, 否則會噴錯, 當然你也可以填 `max(demo.id) as id,`,\n\n反正一定要給他一個 id 即可.\n\n實際畫面\n\n![alt tag](https://i.imgur.com/BFsJsBM.png)\n\n### 使用 RAW SQL 說明\n\n* [Youtube Tutorial - odoo 使用 RAW SQL 說明](https://youtu.be/hfOLmoIfO9E)\n\n前面和大家說明過了有時候會使用原生的 SQL 來完成.\n\n這部份將更詳細的說明 RAW SQL 的使用方法以及應該注意的事項 :smile:\n\n可參考 [models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_odoo_tutorial/models/models.py)\n\n```python\n......\ndef demo_raw_sql(self):\n    query = \"\"\"\n        SELECT\n            id, name,\n            is_done_track_onchange,\n            name_track_always,\n            start_datetime,\n            stop_datetime,\n            field_onchange_demo,\n            field_onchange_demo_set,\n            input_number\n        FROM\n            demo_odoo_tutorial;\n    \"\"\"\n    self.env.cr.execute(query)\n\n    print('self.env.cr.fetchall:', self.env.cr.fetchall())\n    # print('self.env.cr.fetchone:', self.env.cr.fetchone())\n    # print('self.env.cr.dictfetchall:', self.env.cr.dictfetchall())\n......\n```\n\n你會發現有三種取值的方法\n\n`self.env.cr.fetchall()`\n\n![alt tag](https://i.imgur.com/sy3kVxW.png)\n\n`self.env.cr.fetchone()`\n\n![alt tag](https://i.imgur.com/ffN1rSE.png)\n\n`self.env.cr.dictfetchall()`\n\n![alt tag](https://i.imgur.com/RcAw0Pr.png)\n\n當你在使用 原生的 SQL 時, 要很小心 :exclamation: :exclamation: :exclamation:\n\n因為這種搜尋方式跳過了 ORM 那層, 所以權限以及安全規則的部份都會全被跳過 :exclamation:\n\n所以在使用 `INSERT/UPDATE` 時也不會觸發 `create()` `write()`,\n\n所以請特別注意 :exclamation::exclamation:\n\n也要小心 SQL注入(SQL injection) :exclamation: :exclamation:\n\n```python\n# SQL injection possible\nself.env.cr.execute('SELECT * FROM demo_odoo_tutorial where id >' + '1' + ';')\n\n# good\nself.env.cr.execute('SELECT * FROM demo_odoo_tutorial where id > %s;', (1,))\n```\n\n如果今天有使用 like 要注意一下(要加上跳脫字元),\n\n```python\nquery = \"\"\"\n    SELECT\n        id, name,\n        is_done_track_onchange,\n        name_track_always,\n        start_datetime,\n        stop_datetime,\n        field_onchange_demo,\n        field_onchange_demo_set,\n        input_number\n  FROM\n        demo_odoo_tutorial\n  WHERE\n        name like '%%odoo%%' and 1 = %s;\n\"\"\"\n\nquery_sql_params = (1,)\nself.env.cr.execute(query, query_sql_params)\n\n```\n\n或是使用\n\n```python\nquery = \"\"\"\n    SELECT\n        id, name,\n        is_done_track_onchange,\n        name_track_always,\n        start_datetime,\n        stop_datetime,\n        field_onchange_demo,\n        field_onchange_demo_set,\n        input_number\n  FROM\n        demo_odoo_tutorial\n  WHERE\n        name like %s and 1 = %s;\n\"\"\"\n\nquery_sql_params = ('%%odoo%%', 1,)\n# query_sql_params = ('%odoo%', 1,)\n\nself.env.cr.execute(query, query_sql_params)\n```\n\n更好的方法是使用 [更好的處理方式 - SQL string composition](https://github.com/twtrubiks/postgresql-note/tree/main/avoid-sql-injection-tutorial#%E6%9B%B4%E5%A5%BD%E7%9A%84%E8%99%95%E7%90%86%E6%96%B9%E5%BC%8F---sql-string-composition)"
  },
  {
    "path": "demo_odoo_tutorial/__init__.py",
    "content": "from . import controllers\nfrom . import models"
  },
  {
    "path": "demo_odoo_tutorial/__manifest__.py",
    "content": "{\n    'name': \"demo odoo tutorial\",\n    'summary': \"\"\"\n        basic tutorial -\n        demo odoo tutorial\n    \"\"\",\n    'description': \"\"\"\n        basic tutorial -\n        demo odoo tutorial\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['base', 'mail', 'website'],\n\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'data/data_demo_odoo.xml',\n        'views/menu.xml',\n        'views/view.xml',\n        'reports/report.xml',\n        'views/demo_odoo_template.xml',\n    ],\n    # only loaded in demonstration mode\n    # 'demo': [\n    #     'demo/demo.xml',\n    # ],\n    'installable': True,\n    'auto_install': False,\n    'application': True,\n}\n"
  },
  {
    "path": "demo_odoo_tutorial/controllers/__init__.py",
    "content": "from . import controllers"
  },
  {
    "path": "demo_odoo_tutorial/controllers/controllers.py",
    "content": "from odoo import http\n\nclass DemoOdoo(http.Controller):\n\n    @http.route('/demo/odoo', auth='user', website=True)\n    def list(self, **kwargs):\n        obj = http.request.env['demo.odoo.tutorial']\n        objs = obj.search([])\n        print(http.request.website.id)\n        return http.request.render(\n            'demo_odoo_tutorial.demo_odoo_template',{'objs': objs})\n"
  },
  {
    "path": "demo_odoo_tutorial/data/data_demo_odoo.xml",
    "content": "<odoo noupdate=\"1\">\n  <record id=\"demo_odoo_1\" model=\"demo.odoo.tutorial\">\n      <field name=\"name\">demo_odoo_1</field>\n      <field name=\"name_track_always\">demo_name_track_always_1</field>\n      <field name=\"is_done_track_onchange\">True</field>\n  </record>\n\n  <record id=\"demo_odoo_2\" model=\"demo.odoo.tutorial\">\n      <field name=\"name\">demo_odoo_2</field>\n      <field name=\"name_track_always\">demo_name_track_always_2</field>\n      <field name=\"is_done_track_onchange\">True</field>\n  </record>\n\n</odoo>\n"
  },
  {
    "path": "demo_odoo_tutorial/demo/demo.xml",
    "content": "<odoo>\n    <data>\n\n    </data>\n</odoo>"
  },
  {
    "path": "demo_odoo_tutorial/models/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_odoo_tutorial/models/models.py",
    "content": "from odoo import models, fields, api, tools\nfrom odoo.exceptions import ValidationError\nfrom datetime import datetime\n\nclass DemoOdooTutorial(models.Model):\n    _name = 'demo.odoo.tutorial'\n    _description = 'Demo Odoo Tutorial'\n    _inherit = ['mail.thread', 'mail.activity.mixin'] # track_visibility\n\n    name = fields.Char('Description', required=True)\n\n    # track_visibility='always' 和 track_visibility='onchange'\n    is_done_track_onchange = fields.Boolean(\n        string='Is Done?', default=False, track_visibility='onchange')\n    name_track_always = fields.Char(string=\"track_name\", track_visibility='always')\n\n    start_datetime = fields.Datetime('Start DateTime', default=fields.Datetime.now())\n    stop_datetime = fields.Datetime('End Datetime', default=fields.Datetime.now())\n\n    field_onchange_demo = fields.Char('onchange_demo')\n    field_onchange_demo_set = fields.Char('onchange_demo_set', readonly=True)\n\n    # float digits\n    # field tutorial\n    input_number = fields.Float(string='input number', digits=(10,3))\n    field_compute_demo = fields.Integer(compute=\"_get_field_compute\") # readonly\n\n    # field_compute_demo = fields.Integer(compute=\"_get_field_compute\",\n    #                                     inverse=\"_set_input_number\",\n    #                                     search=\"_search_upper\")\n\n    _sql_constraints = [\n        ('name_uniq', 'unique(name)', 'Description must be unique'),\n    ]\n\n    @api.constrains('start_datetime', 'stop_datetime')\n    def _check_date(self):\n        for data in self:\n            if data.start_datetime > data.stop_datetime:\n                raise ValidationError(\n                    \"data.stop_datetime  > data.start_datetime\"\n                )\n\n    @api.depends('input_number')\n    def _get_field_compute(self):\n        for data in self:\n            data.field_compute_demo = data.input_number * 1000\n\n    def _set_input_number(self):\n        for data in self:\n            data.input_number = data.field_compute_demo / 1000\n\n    def _search_upper(self, operator, value):\n        return [('input_number', operator, value)]\n\n    @api.onchange('field_onchange_demo')\n    def onchange_demo(self):\n        if self.field_onchange_demo:\n            self.field_onchange_demo_set = 'set {}'.format(self.field_onchange_demo)\n\n        # warning message\n        result = dict()\n        result['warning'] = {\n            'title': 'HELLO',\n            'message': 'I am warning'\n        }\n        return result\n\n    def demo_raw_sql(self):\n        query = \"\"\"\n            SELECT\n                id, name,\n                is_done_track_onchange,\n                name_track_always,\n                start_datetime,\n                stop_datetime,\n                field_onchange_demo,\n                field_onchange_demo_set,\n                input_number\n\t        FROM\n                demo_odoo_tutorial;\n        \"\"\"\n        self.env.cr.execute(query)\n\n        print('self.env.cr.fetchall:', self.env.cr.fetchall())\n        # print('self.env.cr.fetchone:', self.env.cr.fetchone())\n        # print('self.env.cr.dictfetchall:', self.env.cr.dictfetchall())\n\n\n        # query = \"\"\"\n        #     SELECT\n        #         id, name,\n        #         is_done_track_onchange,\n        #         name_track_always,\n        #         start_datetime,\n        #         stop_datetime,\n        #         field_onchange_demo,\n        #         field_onchange_demo_set,\n        #         input_number\n        # FROM\n        #         demo_odoo_tutorial\n        # WHERE\n        #         name like %s and 1 = %s;\n        # \"\"\"\n\n        # query_sql_params = ('%%odoo%%', 1,)\n        # # query_sql_params = ('%odoo%', 1,)\n        # self.env.cr.execute(query)\n\n    def get_custom_portal_date(self):\n        str_time = datetime.strftime(self.start_datetime, '%Y/%m/%d')\n        return '>{}<'.format(str_time)\n\nclass DemoOdooTutorialStatistics(models.Model):\n    _name = 'demo.odoo.tutorial.statistics'\n    _description = 'Demo Odoo Tutorial Statistics'\n    _auto = False\n\n    create_uid = fields.Many2one('res.users', 'Created by', readonly=True)\n    average_input_number = fields.Float(string=\"Average Input Number\", readonly=True)\n\n    @api.model_cr\n    def init(self):\n        tools.drop_view_if_exists(self.env.cr, self._table)\n        query = \"\"\"\n        CREATE OR REPLACE VIEW demo_odoo_tutorial_statistics AS\n        (\n            SELECT\n                min(demo.id) as id,\n                create_uid,\n                avg(input_number) AS average_input_number\n            FROM\n                demo_odoo_tutorial AS demo\n            GROUP BY demo.create_uid\n        );\n        \"\"\"\n        self.env.cr.execute(query)\n"
  },
  {
    "path": "demo_odoo_tutorial/reports/report.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n    <template id=\"report_demo_odoo_tutorial\">\n        <t t-call=\"web.html_container\">\n            <t t-foreach=\"docs\" t-as=\"o\">\n                <t t-call=\"web.external_layout\">\n                    <div class=\"page\">\n                        <h2>Odoo Report</h2>\n                        <div>\n                            <strong>Name:</strong>\n                            <p t-field=\"o.name\"/>\n                        </div>\n                        <div>\n                            <strong>Name_track_always:</strong>\n                            <p t-field=\"o.name_track_always\"/>\n                        </div>\n                        <div>\n                            <strong>start datetime:</strong>\n                            <p t-field=\"o.start_datetime\"/>\n                        </div>\n                        <div>\n                            <strong>stop datetime:</strong>\n                            <p t-field=\"o.stop_datetime\" t-options='{\"format\": \"Y/MM/dd\"}'/>\n                        </div>\n                        <div>\n                            <strong>custom start datetime:</strong>\n                            <p t-esc=\"o.get_custom_portal_date()\"/>\n                        </div>\n                    </div>\n                </t>\n            </t>\n        </t>\n    </template>\n\n    <report\n        id=\"action_report_demo\"\n        string=\"Demo Report\"\n        model=\"demo.odoo.tutorial\"\n        report_type=\"qweb-pdf\"\n        name=\"demo_odoo_tutorial.report_demo_odoo_tutorial\"\n        file=\"demo_odoo_tutorial.report_demo_odoo_tutorial\"\n        print_report_name=\"'Demo Report - %s' % ((object.name).replace('/', ''))\"\n    />\n\n</odoo>\n"
  },
  {
    "path": "demo_odoo_tutorial/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_odoo_user,Demo Odoo Tutorial User Access,model_demo_odoo_tutorial,demo_odoo_tutorial_group_user,1,0,0,0\naccess_demo_odoo_manager,Demo Odoo Tutorial Manager Access,model_demo_odoo_tutorial,demo_odoo_tutorial_group_manager,1,1,1,1\n\naccess_demo_odoo_statistics,Demo Odoo Tutorial Statistics,model_demo_odoo_tutorial_statistics,demo_odoo_tutorial_group_user,1,1,1,1"
  },
  {
    "path": "demo_odoo_tutorial/security/security.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n\n  <record id=\"module_demo_odoo_tutorial\" model=\"ir.module.category\">\n    <field name=\"name\">Demo odoo tutorial category</field>\n  </record>\n\n  <record id=\"demo_odoo_tutorial_group_user\" model=\"res.groups\">\n    <field name=\"name\">User</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_odoo_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('base.group_user'))]\"/>\n  </record>\n\n  <record id=\"demo_odoo_tutorial_group_manager\" model=\"res.groups\">\n    <field name=\"name\">Manager</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_odoo_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('demo_odoo_tutorial_group_user'))]\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_odoo_tutorial/tests/__init__.py",
    "content": "from . import test_demo_odoo_transactioncase\nfrom . import test_demo_odoo_singletransactioncase\n"
  },
  {
    "path": "demo_odoo_tutorial/tests/test_demo_odoo_singletransactioncase.py",
    "content": "from odoo.exceptions import UserError, AccessError, ValidationError\nfrom odoo.tests.common import SingleTransactionCase\n\nclass TestDemoOdooSingleTransactionCase(SingleTransactionCase):\n\n    def setUp(self, *args, **kwargs):\n        \"\"\"setUp\"\"\"\n        super(TestDemoOdooSingleTransactionCase, self).setUp(*args, **kwargs)\n        print('Run setUp')\n\n    def test_hello_world(self):\n        \"\"\"test_hello_world\"\"\"\n        self.assertEqual(0, 0, 'test hello world')\n\n    def test_datetime_validation(self):\n        \"\"\"test_datetime_validation\"\"\"\n        values = {\n            'name': 'hello',\n            'start_datetime': '2020-02-01',\n            'stop_datetime': '2020-01-01',\n        }\n        with self.assertRaises(ValidationError):\n            self.env['demo.odoo.tutorial'].create(values)\n\n    def test_field_compute_demo(self):\n        \"\"\"test_field_compute_demo\"\"\"\n        values = {\n            'name': 'hello',\n            'input_number': 2\n        }\n        data = self.env['demo.odoo.tutorial'].create(values)\n        self.assertEqual(data.field_compute_demo, data.input_number * 1000)\n"
  },
  {
    "path": "demo_odoo_tutorial/tests/test_demo_odoo_transactioncase.py",
    "content": "from odoo.exceptions import UserError, AccessError, ValidationError\nfrom odoo.tests.common import TransactionCase, tagged\n\n# @tagged('-standard', 'nice')\nclass TestDemoOdooTransactionCase(TransactionCase):\n\n    def setUp(self, *args, **kwargs):\n        \"\"\"setUp\"\"\"\n        super(TestDemoOdooTransactionCase, self).setUp(*args, **kwargs)\n        print('Run setUp')\n\n    def test_hello_world(self):\n        \"\"\"test_hello_world\"\"\"\n        self.assertEqual(0, 0, 'test hello world')\n\n    def test_datetime_validation(self):\n        \"\"\"test_datetime_validation\"\"\"\n        values = {\n            'name': 'hello',\n            'start_datetime': '2020-02-01',\n            'stop_datetime': '2020-01-01',\n        }\n        with self.assertRaises(ValidationError):\n            self.env['demo.odoo.tutorial'].create(values)\n\n    def test_field_compute_demo(self):\n        \"\"\"test_field_compute_demo\"\"\"\n        values = {\n            'name': 'hello',\n            'input_number': 2\n        }\n        data = self.env['demo.odoo.tutorial'].create(values)\n        self.assertEqual(data.field_compute_demo, data.input_number * 1000)\n"
  },
  {
    "path": "demo_odoo_tutorial/views/demo_odoo_template.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n\n<template id=\"demo_odoo_template\" name=\"Demo odoo List\">\n  <t t-call=\"website.layout\">\n    <div id=\"wrap\" class=\"container\">\n      <h1>Demo Odoo</h1>\n        <t t-foreach=\"objs\" t-as=\"obj\">\n          <div class=\"row\">\n            <span t-field=\"obj.name\" />,\n            <span t-field=\"obj.is_done_track_onchange\" />,\n            <span t-field=\"obj.name_track_always\" />\n          </div>\n        </t>\n    </div>\n  </t>\n</template>\n\n</odoo>\n"
  },
  {
    "path": "demo_odoo_tutorial/views/menu.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <!-- demo_odoo_tutorial App Menu -->\n    <menuitem id=\"demo_odoo_tutorial_menu\"\n              name=\"Demo Odoo Tutorial\" />\n\n    <!-- Action to open the demo_odoo_tutorial -->\n    <act_window id=\"action_odoo_tutorial\"\n      name=\"Demo Odoo Tutorial Action\"\n      res_model=\"demo.odoo.tutorial\"\n      view_mode=\"tree,form\"/>\n\n    <!-- Menu item to open the demo_odoo_tutorial -->\n    <menuitem id=\"menu_odoo_tutorial\"\n              name=\"Demo Odoo Tutorial\"\n\t            action=\"action_odoo_tutorial\"\n              parent=\"demo_odoo_tutorial_menu\" />\n\n    <act_window id=\"action_odoo_statistics_tutorial\"\n      name=\"Demo Odoo Tutorial Statistics Action\"\n      res_model=\"demo.odoo.tutorial.statistics\"\n      view_mode=\"pivot\"/>\n\n    <menuitem id=\"menu_odoo_statistics_tutorial\"\n              name=\"Demo Odoo Statistics Tutorial\"\n\t            action=\"action_odoo_statistics_tutorial\"\n              parent=\"demo_odoo_tutorial_menu\" />\n</odoo>\n\n\n"
  },
  {
    "path": "demo_odoo_tutorial/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n\n  <record id=\"view_form_demo_odoo_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Odoo Tutorial Form</field>\n    <field name=\"model\">demo.odoo.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Odoo Tutorial\">\n        <header>\n          <button name=\"demo_raw_sql\" string=\"demo raw sql\" type=\"object\" class=\"oe_highlight o_expense_sheet_submit\"/>\n        </header>\n        <sheet>\n          <group>\n            <field name=\"name\"/>\n            <field name=\"name_track_always\"/>\n            <field name=\"is_done_track_onchange\"/>\n            <field name=\"start_datetime\"/>\n            <field name=\"stop_datetime\"/>\n            <field name=\"field_onchange_demo\"/>\n            <field name=\"field_onchange_demo_set\" force_save=\"1\"/>\n            <!-- <field name=\"input_number\" widget=\"percentage\"/> -->\n            <field name=\"input_number\"/>\n            <field name=\"field_compute_demo\"/>\n          </group>\n        </sheet>\n        <div class=\"oe_chatter\">\n          <field name=\"message_follower_ids\" widget=\"mail_followers\"/>\n          <field name=\"activity_ids\" widget=\"mail_activity\"/>\n          <field name=\"message_ids\" widget=\"mail_thread\"/>\n        </div>\n      </form>\n    </field>\n  </record>\n\n  <record id=\"view_tree_demo_odoo_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Odoo Tutorial List</field>\n    <field name=\"model\">demo.odoo.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n        <field name=\"name\"/>\n        <field name=\"name_track_always\"/>\n        <field name=\"is_done_track_onchange\"/>\n        <field name=\"start_datetime\"/>\n        <field name=\"stop_datetime\"/>\n      </tree>\n    </field>\n  </record>\n\n  <record id=\"view_demo_odoo_tutorial_statistics_pivot\" model=\"ir.ui.view\">\n    <field name=\"name\">demo.odoo.tutorial.statistics.pivot</field>\n    <field name=\"model\">demo.odoo.tutorial.statistics</field>\n    <field name=\"arch\" type=\"xml\">\n      <pivot string=\"Statistics\" disable_linking=\"True\">\n          <field name=\"create_uid\" type=\"row\"/>\n          <field name=\"average_input_number\" type=\"measure\"/>\n      </pivot>\n    </field>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_odoo_tutorial_wizard/README.md",
    "content": "# odoo 觀念 - TransientModel - Wizard\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 觀念 - TransientModel - Wizard](https://youtu.be/Gc-wRnAhbKs)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n本篇文章主要介紹 odoo 中的 wizard 這部份\n\n## 說明\n\n在 odoo 中, wizard 是一個很特別的 model, 之前除了介紹過最基本的 BaseModel (可參考 [demo_odoo_tutorial](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial))\n之外,\n\n今天要來介紹另一個 model, 也就是 TransientModel,\n\n`TransientModel` 繼承自 Model, `_transient = True`,\n\n先簡單看一下它的定義,\n\n```python\nclass TransientModel(Model):\n    \"\"\" Model super-class for transient records, meant to be temporarily\n    persisted, and regularly vacuum-cleaned.\n\n    A TransientModel has a simplified access rights management, all users can\n    create new records, and may only access the records they created. The super-\n    user has unrestricted access to all TransientModel records.\n    \"\"\"\n    _auto = True                # automatically create database backend\n    _register = False           # not visible in ORM registry, meant to be python-inherited only\n    _abstract = False           # not abstract\n    _transient = True           # transient\n```\n\nTransientModel 是一種特殊的 model, TransientModel 所產生的 model 會在一個時間定期被刪除,\n\n所以 TransientModel 只適合建立暫時的數據, 也就是接著要介紹的 wizard.\n\n先來看 [wizard/model_wizard.py](wizard/model_wizard.py)\n\n```python\nclass DemoWizard(models.TransientModel):\n    _name = \"demo.wizard\"\n    _description = \"Demo Wizard\"\n\n    wizard_partner_id = fields.Many2one('res.partner', string='Partner')\n    wizard_test_context = fields.Char('wizard_test_context')\n\n    @api.model\n    def default_get(self, fields):\n        res = super(DemoWizard, self).default_get(fields)\n        default_partner_id = self.env.context.get('default_partner_id', [])\n        if default_partner_id:\n            res.update({\n                'wizard_partner_id': default_partner_id,\n            })\n            # or\n            # res['wizard_partner_id'] = default_partner_id\n        return res\n\n    @api.multi\n    def btn_validate(self):\n        self.ensure_one()\n        context = dict(self._context or {})\n        default_test_pass_data = context.get('default_test_pass_data', [])\n\n        _logger.warning('============= btn_validate ==================')\n        _logger.warning('default_test_pass_data: %s', default_test_pass_data)\n        _logger.warning('wizard_test_context: %s', self.wizard_test_context)\n\n        _logger.warning('active_id: %s', context['active_id'])\n        _logger.warning('active_ids: %s', context['active_ids'])\n\n        return {'type': 'ir.actions.act_window_close'}\n```\n\n注意 :exclamation: 這邊是使用 `models.TransientModel`.\n\n注意 :exclamation: Transient models 是不需要 access rules\n\n( odoo14 開始 Transient models 需要設定 access rules, 可參考 [odoo14 分支](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/14.0/demo_odoo_tutorial_wizard))\n\n(因為它們是 disposable 一次性的資料), 所以不需要加入 `security/ir.model.access.csv`\n\n在路徑中的 [security/ir.model.access.csv](security/ir.model.access.csv) 裡面定義的東西是\n\n屬於 [models/models.py](models/models.py) 中的哦, 請不要搞錯 :smile:\n\n[views/view.xml](views/view.xml)\n\n```xml\n......\n\n  <record id=\"view_form_demo_odoo_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Odoo Tutorial Form</field>\n    <field name=\"model\">demo.odoo.wizard.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Odoo Tutorial\">\n        <header>\n          <button name=\"%(demo_odoo_tutorial_wizard.demo_wizard_action)d\"\n                  type=\"action\"\n                  string=\"Call Wizard\"\n                  class=\"oe_highlight\"\n                  context=\"{'default_partner_id': partner_id}\"/>\n        </header>\n        <sheet>\n          <group>\n            <field name=\"name\"/>\n            <field name=\"partner_id\"/>\n          </group>\n        </sheet>\n      </form>\n    </field>\n  </record>\n......\n```\n\n重點在 button 這段, 這段是去呼叫 `demo_wizard_action`,\n\n寫法是 `name=\"%(demo_odoo_tutorial_wizard.demo_wizard_action)d\"`,\n\n這邊提醒一下大家 :exclamation: :exclamation: 如果你把 developer mode 打開,\n\n你會發現他沒有 name, 只會有 Action ID\n\n![alt tag](https://i.imgur.com/dxkPuY0.png)\n\n如果你想快速找到這個 ID 是屬於哪一個 name (反查),\n\n你可以進入 Technical -> Actions -> Window Actions 尋找\n\n![alt tag](https://i.imgur.com/zzi1lJh.png)\n\n路徑在 [wizard/model_wizard.xml](wizard/model_wizard.xml)\n\n```xml\n......\n    <record id=\"demo_wizard_view_form\" model=\"ir.ui.view\">\n        <field name=\"name\">demo.wizard.form</field>\n        <field name=\"model\">demo.wizard</field>\n        <field name=\"arch\" type=\"xml\">\n            <form string=\"Wizard Form\">\n                <sheet>\n                    <div class=\"oe_title\">\n                        <h1>Wizard Title</h1>\n                    </div>\n                    <group>\n                        <field name=\"wizard_partner_id\"/>\n                        <field name=\"wizard_test_context\"/>\n                    </group>\n                </sheet>\n                <footer>\n                    <button string=\"Validate\" name=\"btn_validate\" type=\"object\" class=\"btn-primary\"/>\n                    <button string=\"Cancel\" class=\"btn-secondary\" special=\"cancel\"/>\n                </footer>\n            </form>\n        </field>\n    </record>\n\n    <!-- demo_wizard_action -->\n    <record id=\"demo_wizard_action\" model=\"ir.actions.act_window\">\n        <field name=\"name\">Demo Wizard Action</field>\n        <field name=\"res_model\">demo.wizard</field>\n        <field name=\"view_type\">form</field>\n        <field name=\"view_mode\">form</field>\n        <field name=\"view_id\" ref=\"demo_wizard_view_form\"/>\n        <field name=\"target\">new</field>\n        <field name=\"context\">{'default_test_pass_data': 'hello 123'}</field>\n        <field name=\"binding_model_id\" ref=\"demo_odoo_tutorial_wizard.model_demo_odoo_wizard_tutorial\" />\n    </record>\n......\n\n```\n\n點選 Call Wizard,\n\n![alt tag](https://i.imgur.com/CXK9ePn.png)\n\n會跳出 Wizard,\n\n![alt tag](https://i.imgur.com/aIOT2mI.png)\n\nPartner 會幫你自動帶入 partner_id,\n\n原因是因為使用了 context `{'default_partner_id': partner_id`,\n\n以及 `def default_get(self, fields)` 的方法實現.\n\n當你在 `wizard_test_context` 輸入任何內容 (twtrubiks), 然後點選 Validate\n\n![alt tag](https://i.imgur.com/2WWQQCj.png)\n\n它會 call `def btn_validate`,\n\n然後從 CLI 你會看到兩條 log,\n\n![alt tag](https://i.imgur.com/nZDDTmp.png)\n\nlog 1, `default_test_pass_data: hello 123`\n\n會出現這條訊息的原因是因為設定了預設的 context,\n\n`<field name=\"context\">{'default_test_pass_data': 'hello 123'}</field>`\n\nlog 2, `wizard_test_context: twtrubiks`\n\n這條訊息則是顯示剛剛輸入的內容.\n\n再來看 [model_wizard.xml](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_odoo_tutorial_wizard/wizard/model_wizard.xml)\n\n```xml\n<record id=\"demo_wizard_action\" model=\"ir.actions.act_window\">\n    <field name=\"name\">Demo Wizard Action</field>\n    ......\n    <field name=\"binding_model_id\" ref=\"demo_odoo_tutorial_wizard.model_demo_odoo_wizard_tutorial\" />\n</record>\n```\n\n`binding_model_id` 代表要綁定的 model,\n\n綁定後在 `demo_odoo_tutorial_wizard.model_demo_odoo_wizard_tutorial` tree 中\n\n不管單選還是多選, 都會出現在 Action 中,\n\n![alt tag](https://i.imgur.com/LVdJuPl.png)\n\n另一種傳值的方式, 也可以全部透過 python 來完成,\n\n可參考 [models/models.py](models/models.py)\n\n```python\n......\n@api.multi\ndef action_context_demo(self):\n    # if self._context.get('context_data', False):\n    if self.env.context.get('context_data'):\n        raise ValidationError('have context data')\n    raise ValidationError('hello')\n\n@api.multi\ndef action_button(self):\n    for record in self:\n        record.with_context(context_data=True).action_context_demo()\n......\n```\n\n[views/view.xml](views/view.xml) 的部份,\n\n```xml\n......\n  <button name=\"action_context_demo\"\n          type=\"object\"\n          string=\"action context demo\"\n          class=\"oe_highlight\"/>\n\n  <button name=\"action_button\"\n          type=\"object\"\n          string=\"action button\"\n          class=\"oe_highlight\"/>\n......\n```\n\n![alt tag](https://i.imgur.com/oqnr1Ox.png)\n\n當點下 `action context demo` button 時,\n\n會跳出 hello, 因為 `context_data` 為 `False`.\n\n![alt tag](https://i.imgur.com/6rMlJHK.png)\n\n當點下 `action button` button 時,\n\n會跳出 have context data, 因為 `context_data` 為 `True`,\n\n主要透過 `record.with_context(context_data=True).action_context_demo()` 這段,\n\n將 `context_data` 送進去.\n\n![alt tag](https://i.imgur.com/YIoy0yL.png)"
  },
  {
    "path": "demo_odoo_tutorial_wizard/__init__.py",
    "content": "from . import models\nfrom . import wizard"
  },
  {
    "path": "demo_odoo_tutorial_wizard/__manifest__.py",
    "content": "{\n    'name': \"demo odoo tutorial wizard\",\n    'summary': \"\"\"\n        wizard tutorial -\n        demo odoo tutorial wizard\n    \"\"\",\n    'description': \"\"\"\n        wizard tutorial -\n        demo odoo tutorial wizard\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['base'],\n\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'wizard/model_wizard.xml',\n        'views/menu.xml',\n        'views/view.xml',\n    ],\n    'installable': True,\n    'auto_install': False,\n    'application': True,\n}\n"
  },
  {
    "path": "demo_odoo_tutorial_wizard/models/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_odoo_tutorial_wizard/models/models.py",
    "content": "from odoo import models, fields, api\nfrom odoo.exceptions import ValidationError\n\nclass DemoOdooWizardTutorial(models.Model):\n    _name = 'demo.odoo.wizard.tutorial'\n    _description = 'Demo Odoo Wizard Tutorial'\n\n    name = fields.Char('Description', required=True)\n    partner_id = fields.Many2one('res.partner', string='Partner')\n\n    @api.multi\n    def action_context_demo(self):\n        # if self._context.get('context_data', False):\n        if self.env.context.get('context_data'):\n            raise ValidationError('have context data')\n        raise ValidationError('hello')\n\n    @api.multi\n    def action_button(self):\n        for record in self:\n            record.with_context(context_data=True).action_context_demo()"
  },
  {
    "path": "demo_odoo_tutorial_wizard/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_odoo_wizard_user,Demo Odoo Wizard Tutorial User Access,model_demo_odoo_wizard_tutorial,demo_odoo_wizard_tutorial_group_user,1,0,0,0\naccess_demo_odoo_wizard_manager,Demo Odoo Wizard Tutorial Manager Access,model_demo_odoo_wizard_tutorial,demo_odoo_wizard_tutorial_group_manager,1,1,1,1"
  },
  {
    "path": "demo_odoo_tutorial_wizard/security/security.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n\n  <record id=\"module_demo_odoo_wizard_tutorial\" model=\"ir.module.category\">\n    <field name=\"name\">Demo odoo wizard tutorial category</field>\n  </record>\n\n  <record id=\"demo_odoo_wizard_tutorial_group_user\" model=\"res.groups\">\n    <field name=\"name\">User</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_odoo_wizard_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('base.group_user'))]\"/>\n  </record>\n\n  <record id=\"demo_odoo_wizard_tutorial_group_manager\" model=\"res.groups\">\n    <field name=\"name\">Manager</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_odoo_wizard_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('demo_odoo_wizard_tutorial_group_user'))]\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_odoo_tutorial_wizard/views/menu.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <!-- demo_odoo_tutorial App Menu -->\n    <menuitem id=\"demo_odoo_tutorial_menu\"\n              name=\"Demo Odoo Wizard\" />\n\n    <!-- Action to open the demo_odoo_tutorial -->\n    <act_window id=\"action_odoo_tutorial\"\n      name=\"Demo Odoo Wizard Action\"\n      res_model=\"demo.odoo.wizard.tutorial\"\n      view_mode=\"tree,form\"/>\n\n    <!-- Menu item to open the demo_odoo_tutorial -->\n    <menuitem id=\"menu_odoo_tutorial\"\n              name=\"Demo Odoo Wizard\"\n\t            action=\"action_odoo_tutorial\"\n              parent=\"demo_odoo_tutorial_menu\" />\n</odoo>\n\n\n"
  },
  {
    "path": "demo_odoo_tutorial_wizard/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n\n  <record id=\"view_form_demo_odoo_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Odoo Tutorial Form</field>\n    <field name=\"model\">demo.odoo.wizard.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <form string=\"Demo Odoo Tutorial\">\n        <header>\n          <button name=\"%(demo_odoo_tutorial_wizard.demo_wizard_action)d\"\n                  type=\"action\"\n                  string=\"Call Wizard\"\n                  class=\"oe_highlight\"\n                  context=\"{'default_partner_id': partner_id}\"/>\n\n          <button name=\"action_context_demo\"\n                  type=\"object\"\n                  string=\"action context demo\"\n                  class=\"oe_highlight\"/>\n\n          <button name=\"action_button\"\n                  type=\"object\"\n                  string=\"action button\"\n                  class=\"oe_highlight\"/>\n        </header>\n        <sheet>\n          <group>\n            <field name=\"name\"/>\n            <field name=\"partner_id\"/>\n          </group>\n        </sheet>\n      </form>\n    </field>\n  </record>\n\n  <record id=\"view_tree_demo_odoo_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Odoo Tutorial List</field>\n    <field name=\"model\">demo.odoo.wizard.tutorial</field>\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n        <field name=\"name\"/>\n        <field name=\"partner_id\"/>\n      </tree>\n    </field>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_odoo_tutorial_wizard/wizard/__init__.py",
    "content": "from . import model_wizard\n"
  },
  {
    "path": "demo_odoo_tutorial_wizard/wizard/model_wizard.py",
    "content": "from odoo import api, fields, models\nimport logging\n\n_logger = logging.getLogger(__name__)\n\nclass DemoWizard(models.TransientModel):\n    _name = \"demo.wizard\"\n    _description = \"Demo Wizard\"\n\n    wizard_partner_id = fields.Many2one('res.partner', string='Partner')\n    wizard_test_context = fields.Char('wizard_test_context')\n\n    @api.model\n    def default_get(self, fields):\n        res = super(DemoWizard, self).default_get(fields)\n        default_partner_id = self.env.context.get('default_partner_id', [])\n        if default_partner_id:\n            res.update({\n                'wizard_partner_id': default_partner_id,\n            })\n            # or\n            # res['wizard_partner_id'] = default_partner_id\n        return res\n\n    @api.multi\n    def btn_validate(self):\n        self.ensure_one()\n        context = dict(self._context or {})\n        default_test_pass_data = context.get('default_test_pass_data', [])\n\n        _logger.warning('============= btn_validate ==================')\n        _logger.warning('default_test_pass_data: %s', default_test_pass_data)\n        _logger.warning('wizard_test_context: %s', self.wizard_test_context)\n\n        _logger.warning('active_id: %s', context['active_id'])\n        _logger.warning('active_ids: %s', context['active_ids'])\n\n        return {'type': 'ir.actions.act_window_close'}"
  },
  {
    "path": "demo_odoo_tutorial_wizard/wizard/model_wizard.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<odoo>\n    <record id=\"demo_wizard_view_form\" model=\"ir.ui.view\">\n        <field name=\"name\">demo.wizard.form</field>\n        <field name=\"model\">demo.wizard</field>\n        <field name=\"arch\" type=\"xml\">\n            <form string=\"Wizard Form\">\n                <sheet>\n                    <div class=\"oe_title\">\n                        <h1>Wizard Title</h1>\n                    </div>\n                    <group>\n                        <field name=\"wizard_partner_id\"/>\n                        <field name=\"wizard_test_context\"/>\n                    </group>\n                </sheet>\n                <footer>\n                    <button string=\"Validate\" name=\"btn_validate\" type=\"object\" class=\"btn-primary\"/>\n                    <button string=\"Cancel\" class=\"btn-secondary\" special=\"cancel\"/>\n                </footer>\n            </form>\n        </field>\n    </record>\n\n    <!-- demo_wizard_action -->\n    <record id=\"demo_wizard_action\" model=\"ir.actions.act_window\">\n        <field name=\"name\">Demo Wizard Action</field>\n        <field name=\"res_model\">demo.wizard</field>\n        <field name=\"view_type\">form</field>\n        <field name=\"view_mode\">form</field>\n        <field name=\"view_id\" ref=\"demo_wizard_view_form\"/>\n        <field name=\"target\">new</field>\n        <field name=\"context\">{'default_test_pass_data': 'hello 123'}</field>\n        <field name=\"binding_model_id\" ref=\"demo_odoo_tutorial_wizard.model_demo_odoo_wizard_tutorial\" />\n    </record>\n</odoo>\n"
  },
  {
    "path": "demo_orm_cache/README.md",
    "content": "# odoo 觀念 - orm cache 說明\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - orm cache 說明](https://youtu.be/AXi7c4EQuYE)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n本篇文章主要介紹 odoo 中的 orm cache\n\n## 說明\n\ncache 的觀念在 python 中其實主要就是使用 dict 來完成的,\n\n如果有興趣也可以參考底下的兩篇文章 :smile:\n\n[什麼是 functools.lru_cache in python](https://github.com/twtrubiks/python-notes/tree/master/what_is_the_functools.lru_cache)\n\n[fibonacci numbers ( 費氏數列 )](https://github.com/twtrubiks/python-notes/tree/master/fibonacci_numbers_tutorial)\n\n要記住 cache 都是保存在 ram 中, 所以請不要拿來存大型的圖片.\n\n範例 code 都是放在 [models/model.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/blob/master/demo_orm_cache/models/model.py) 之中,\n\n先來看一個最簡單的例子,\n\n```python\nfrom odoo import models, fields, tools\n......\n    @tools.ormcache()\n    def demo_ormcache(self):\n        result = '{} {}'.format('hello', 'world')\n        _logger.warning(result)\n        return result\n```\n\n點選對應的 `demo ormcache` button,\n\n![alt tag](https://i.imgur.com/SV6uZOx.png)\n\n你會看到只有第一次會輸出 logger, 第二次開始就不會輸出了, 因為他直接從 cache 中取資料.\n\n![alt tag](https://i.imgur.com/fqQ3zWb.png)\n\n還可以依照 user 快取,\n\n```python\n......\n    @tools.ormcache('self.env.uid')\n    def demo_ormcache_by_env(self):\n        result = '{} {} env uid'.format('hello', 'world')\n        _logger.warning(result)\n        return result\n```\n\n測試方法就是分別登入兩個 user, 你會發現 user 自己會擁有自己快取.\n\n甚至可以依照語言去快取 (注意, 這邊是使用 `ormcache_context` )\n\n```python\n......\n    @tools.ormcache_context(keys=('lang',))\n    def demo_ormcache_context(self):\n        result = '{} {} context'.format('hello', 'world')\n        _logger.warning(result)\n        return result\n```\n\n測試方法就是多建立一個語言, 你會發現語言之間也會擁有自己的快取.\n\n還有一個是 `ormcache_multi`, 但這個似乎比較少用.\n\n清除 cache 的方式\n\n```python\n......\n def demo_clear_cache(self):\n    # self.env[model_name].clear_caches()\n    self.env['demo.cache'].clear_caches()\n    raise ValidationError('clear cache')\n```\n\n這邊要注意一下, 當你重新啟動 odoo 時, cache 也會自動被清除哦 :exclamation: :exclamation:\n\n然後我們也可以去查看 cache 的狀況,\n\n透過 htop 去查看他的 PID, 然後發送 SIGUSR1 訊號,\n\n如果不知道甚麼是 htop, 可參考 [Linux htop tutorial](https://github.com/twtrubiks/linux-note/tree/master/htop-tutorial)\n\n```cmd\nkill -SIGUSR1 PID\n```\n\n![alt tag](https://i.imgur.com/xyge3Zx.png)\n"
  },
  {
    "path": "demo_orm_cache/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_orm_cache/__manifest__.py",
    "content": "{\n    'name': \"demo orm cache\",\n\n    'summary': \"\"\"\"\"\",\n\n    'description': \"\"\"\"\"\",\n\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['base'],\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'views/menu.xml',\n        'views/view.xml',\n    ],\n    'application': True,\n}\n"
  },
  {
    "path": "demo_orm_cache/models/__init__.py",
    "content": "from . import model\n"
  },
  {
    "path": "demo_orm_cache/models/model.py",
    "content": "from odoo import models, fields, tools\nfrom odoo.exceptions import ValidationError\nimport logging\n\n_logger = logging.getLogger(__name__)\n\nclass DemoCache(models.Model):\n    _name = 'demo.cache'\n    _description = 'demo cache'\n\n    name = fields.Char('Description', required=True)\n\n    @tools.ormcache()\n    def demo_ormcache(self):\n        result = '{} {}'.format('hello', 'world')\n        _logger.warning(result)\n        return result\n\n    @tools.ormcache('self.env.uid')\n    def demo_ormcache_by_env(self):\n        result = '{} {} env uid'.format('hello', 'world')\n        _logger.warning(result)\n        return result\n\n    @tools.ormcache_context(keys=('lang',))\n    def demo_ormcache_context(self):\n        result = '{} {} context'.format('hello', 'world')\n        _logger.warning(result)\n        return result\n\n    def demo_clear_cache(self):\n        # self.env[model_name].clear_caches()\n        self.env['demo.cache'].clear_caches()\n        raise ValidationError('clear cache')\n"
  },
  {
    "path": "demo_orm_cache/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_cache_user,Demo Cache Tutorial User Access,model_demo_cache,demo_cache_group_user,1,1,1,1\n"
  },
  {
    "path": "demo_orm_cache/security/security.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n\n  <record id=\"module_demo_cache_tutorial\" model=\"ir.module.category\">\n    <field name=\"name\">Demo cache tutorial category</field>\n  </record>\n\n  <record id=\"demo_cache_group_user\" model=\"res.groups\">\n    <field name=\"name\">User</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_cache_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('base.group_user'))]\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_orm_cache/views/menu.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <!-- Demo Cache Menu -->\n    <menuitem id=\"demo_cache_menu\"\n      name=\"Demo Cache\" />\n\n    <!-- Action to open the demo_cache list -->\n    <act_window id=\"action_demo_cache\"\n      name=\"Demo Cache\"\n      res_model=\"demo.cache\"\n      view_mode=\"tree,form\"/>\n\n    <!-- Menu item to open the demo_cache list -->\n    <menuitem id=\"menu_demo_cache\"\n      name=\"Demo Cache\"\n\t    action=\"action_demo_cache\"\n      parent=\"demo_cache_menu\" />\n</odoo>\n\n\n"
  },
  {
    "path": "demo_orm_cache/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n  <record id=\"view_form_demo_cache\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Cache Form</field>\n    <field name=\"model\">demo.cache</field>\n    <field name=\"arch\" type=\"xml\">\n      <form>\n        <header>\n          <button name=\"demo_ormcache\" string=\"demo ormcache\" type=\"object\" class=\"oe_highlight o_expense_sheet_submit\"/>\n          <button name=\"demo_ormcache_by_env\" string=\"demo ormcache by env\" type=\"object\" class=\"oe_highlight o_expense_sheet_submit\"/>\n          <button name=\"demo_ormcache_context\" string=\"demo ormcache context\" type=\"object\" class=\"oe_highlight o_expense_sheet_submit\"/>\n          <button name=\"demo_clear_cache\" string=\"demo clear cache\" type=\"object\" class=\"oe_highlight o_expense_sheet_submit\"/>\n        </header>\n        <group>\n          <field name=\"name\"/>\n        </group>\n      </form>\n    </field>\n  </record>\n\n  <record id=\"view_tree_demo_cache\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Cache List</field>\n    <field name=\"model\">demo.cache</field>\n    <field name=\"arch\" type=\"xml\">\n      <tree>\n        <field name=\"name\"/>\n      </tree>\n    </field>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_prototype_inheritance/README.md",
    "content": "# odoo 繼承 - prototype inheritance\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 繼承 - prototype inheritance](https://youtu.be/sJrik0jjuas)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 繼承 - class inheritance](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_class_inheritance)\n\n本篇文章主要介紹 prototype inheritance 這部份\n\n## 說明\n\n`_inherit` prototype inheritance\n\n注意 :exclamation: 還有一個是 `_inherits`, 不要搞錯了哦.\n\n![alt tag](https://i.imgur.com/kjtCar6.png)\n\n注意 :exclamation: Stored in different tables.\n\n注意 :exclamation: 此類別會擁有父類別的所有特性, 在此類別中的任何修改, 都不會去影響到父類別.\n\nclass inheritance 和 prototype inheritance 其實很好分辨,\n\nprototype inheritance 會自己額外定義新的 `_name`,\n\n(注意 :exclamation: 如果 `_name` 和被繼承/父類別的名稱一樣, 就等同是 **class inheritance** 哦)\n\n使用的時機通常是繼承 `mail.thread` 這類的 `models.AbstractModel`,\n\n可參考 [odoo 手把手教學 - AbstractModel](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_abstractmodel_tutorial).\n\n[models/model.py](models/model.py)\n\n```python\n......\nclass PrototypeInheritance(models.Model):\n    _name = 'demo.prototype'\n    _description = 'PrototypeInheritance'\n    _inherit = ['mail.thread']\n\n    # 'demo.prototype' 擁有 'mail.thread'(父類別) 的所有特性,\n    # 在這裡面的修改, 都不會去影響到 'mail.thread'(父類別).\n\n    test_field = fields.Char('test_field')\n\n```\n\ndb 中的狀況\n\n![alt tag](https://i.imgur.com/DdOAF2b.png)\n\n[views/views.xml](views/views.xml)\n\n```xml\n......\n<record id=\"view_tree_demo_prototype_tutorial\" model=\"ir.ui.view\">\n<field name=\"name\">Demo Prototype List</field>\n<field name=\"model\">demo.prototype</field>\n<field name=\"arch\" type=\"xml\">\n    <tree>\n        <field name=\"test_field\"/>\n    </tree>\n</field>\n</record>\n......\n```\n\n[views/views.xml](views/views.xml)\n\nform 的部份\n\n```xml\n......\n  <record id=\"view_form_demo_prototype_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Prototype Form</field>\n    <field name=\"model\">demo.prototype</field>\n    <field name=\"arch\" type=\"xml\">\n        <form>\n          <sheet>\n            <group>\n              <field name=\"test_field\"/>\n            </group>\n          </sheet>\n          <div class=\"oe_chatter\">\n              <field name=\"message_follower_ids\" widget=\"mail_followers\"/>\n              <field name=\"message_ids\" widget=\"mail_thread\"/>\n          </div>\n        </form>\n    </field>\n  </record>\n......\n```\n\n在這邊可以使用 `message_follower_ids` `message_ids` 的原因是因為繼承了 `mail.thread`\n\n![alt tag](https://i.imgur.com/1x3qwGZ.png)\n\n![alt tag](https://i.imgur.com/NGhD6H9.png)\n\n因為它是 prototype inheritance, 所以在 db 中的 `demo_prototype` 是不會有 message 的資訊的.\n\n`mail.thread` odoo 原始碼的路徑 `addons/mail/models/mail_thread.py`\n\n```python\nclass MailThread(models.AbstractModel):\n  ......\n  _name = 'mail.thread'\n  _description = 'Email Thread'\n  _mail_flat_thread = True  # flatten the discussino history\n  _mail_post_access = 'write'  # access required on the document to post on it\n  _Attachment = namedtuple('Attachment', ('fname', 'content', 'info'))\n\n  ......\n  message_follower_ids = fields.One2many(\n        'mail.followers', 'res_id', string='Followers')\n\n  ......\n\n  message_ids = fields.One2many(\n          'mail.message', 'res_id', string='Messages',\n          domain=lambda self: [('message_type', '!=', 'user_notification')], auto_join=True)\n  ......\n```\n\n從 `mail.thread` 可以看出它分別儲存在 `mail.followers` 和 `mail.message` table 中.\n"
  },
  {
    "path": "demo_prototype_inheritance/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_prototype_inheritance/__manifest__.py",
    "content": "{\n    'name': 'demo_prototype_inheritance',\n    'version': '12.0.1.0.0',\n    'summary': 'demo_prototype_inheritance',\n    'description': 'demo_prototype_inheritance',\n    'depends': ['mail',],\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'views/menu.xml',\n        'views/view.xml',\n    ],\n    'license': 'AGPL-3',\n    'images': [\n    ],\n    'qweb': [\n    ],\n    'installable': True,\n    'auto_install': False,\n    'application': False,\n}\n"
  },
  {
    "path": "demo_prototype_inheritance/models/__init__.py",
    "content": "from . import model\n"
  },
  {
    "path": "demo_prototype_inheritance/models/model.py",
    "content": "from odoo import models, fields, api\n\n# prototype inheritance\nclass PrototypeInheritance(models.Model):\n    _name = 'demo.prototype'\n    _description = 'PrototypeInheritance'\n    _inherit = ['mail.thread']\n\n    # 'demo.prototype' 擁有 'mail.thread'(父類別) 的所有特性,\n    # 在這裡面的修改, 都不會去影響到 'mail.thread'(父類別).\n\n    test_field = fields.Char('test_field')"
  },
  {
    "path": "demo_prototype_inheritance/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_prototype_user,Demo Prototype Tutorial User Access,model_demo_prototype,demo_prototype_tutorial_group_user,1,0,0,0\naccess_demo_prototype_manager,Demo Prototype Tutorial Manager Access,model_demo_prototype,demo_prototype_tutorial_group_manager,1,1,1,1\n"
  },
  {
    "path": "demo_prototype_inheritance/security/security.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n\n  <record id=\"module_demo_prototype_tutorial\" model=\"ir.module.category\">\n    <field name=\"name\">Demo prototype tutorial category</field>\n  </record>\n\n  <record id=\"demo_prototype_tutorial_group_user\" model=\"res.groups\">\n    <field name=\"name\">User</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_prototype_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('base.group_user'))]\"/>\n  </record>\n\n\n  <record id=\"demo_prototype_tutorial_group_manager\" model=\"res.groups\">\n    <field name=\"name\">Manager</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_prototype_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('demo_prototype_tutorial_group_user'))]\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_prototype_inheritance/views/menu.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <!-- demo_prototype_tutorial App Menu -->\n    <menuitem id=\"demo_prototype_tutorial_menu\"\n              name=\"Demo Prototype Tutorial\" />\n\n    <!-- Action to open the demo_prototype_tutorial -->\n    <act_window id=\"action_demo_prototype_tutorial\"\n                name=\"Demo Prototype Tutorial Action\"\n                res_model=\"demo.prototype\"\n                view_mode=\"tree,form\"/>\n\n    <!-- Menu item to open the demo_prototype_tutorial -->\n    <menuitem id=\"menu_demo_prototype_tutorial\"\n              name=\"Demo Prototype Tutorial\"\n\t          action=\"action_demo_prototype_tutorial\"\n              parent=\"demo_prototype_tutorial_menu\" />\n</odoo>\n\n\n"
  },
  {
    "path": "demo_prototype_inheritance/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n  <record id=\"view_form_demo_prototype_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Prototype Form</field>\n    <field name=\"model\">demo.prototype</field>\n    <field name=\"arch\" type=\"xml\">\n        <form>\n          <sheet>\n            <group>\n              <field name=\"test_field\"/>\n            </group>\n          </sheet>\n          <div class=\"oe_chatter\">\n              <field name=\"message_follower_ids\" widget=\"mail_followers\"/>\n              <field name=\"message_ids\" widget=\"mail_thread\"/>\n          </div>\n        </form>\n    </field>\n  </record>\n\n\n  <record id=\"view_tree_demo_prototype_tutorial\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Prototype List</field>\n    <field name=\"model\">demo.prototype</field>\n    <field name=\"arch\" type=\"xml\">\n       <tree>\n          <field name=\"test_field\"/>\n       </tree>\n    </field>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_recruitment_website_form/README.md",
    "content": "# odoo 觀念 - recruitment_website_form 介紹\n\n[Youtube Tutorial - odoo 觀念 - recruitment_website_form 介紹](https://youtu.be/FDvl1eBIC_Q)\n\n今天來介紹 odoo 中的 website_form, 順便來看一下 code 是怎麼跑的,\n\n就使用 `website_hr_recruitment` 這個 addons 來介紹 :smile:\n\n請先把 `website_hr_recruitment` 裝起來 (記得安裝 demo data),\n\n接著到 [http://0.0.0.0:8069/jobs](http://0.0.0.0:8069/jobs) 裡面隨便點一個,\n\n你會看到以下的畫面, 今天就是要來介紹當你把這個 form 送出的時候會發生甚麼事情,\n\n以及如果你要在這個 form 上加上新的 field 要注意些甚麼,\n\n![alt tag](https://i.imgur.com/GIxr4pC.png)\n\n首先, 先找到這個頁面對應的 view,\n\n路徑在 odoo source code 中的 `addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml`\n\n```xml\n<template id=\"apply\">\n    <t t-call=\"website.layout\">\n        <t t-set=\"additional_title\">Apply Job</t>\n\n        <div id=\"wrap\"  class=\"container\">\n            <h1 class=\"text-center mt-2\">\n                Job Application Form\n            </h1>\n            <h2 t-if=\"job\" class=\"text-center text-muted\">\n                <span t-field=\"job.name\"/>\n            </h2>\n\n            <div class=\"row mt-3\">\n                <section id=\"forms\" class=\"col\">\n                    <form action=\"/website_form/\" method=\"post\" class=\"s_website_form\" enctype=\"multipart/form-data\" data-model_name=\"hr.applicant\" data-success_page=\"/job-thank-you\" t-att-data-form_field_department_id=\"job and job.department_id.id or False\" t-att-data-form_field_job_id=\"job and job.id or False\">\n                        <div class=\"form-group row form-field o_website_form_required_custom\">\n                            <div class=\"col-lg-3 col-md-4 text-right\">\n                                <label class=\"col-form-label\" for=\"partner_name\">Your Name</label>\n                            </div>\n                            <div class=\"col-lg-7 col-md-8\">\n                                <input type=\"text\" class=\"form-control o_website_form_input\" name=\"partner_name\" required=\"\"/>\n                            </div>\n                        </div>\n                        ......\n                    </form>\n                </section>\n            </div>\n        </div>\n    </t>\n</template>\n```\n\n從這邊可以看出 `action=\"/website_form/\" method=\"post\"`, 也就代表會對 website_form route post,\n\n而且個 website_form 被定義在 odoo source code 中的 `addons/website_form/controllers/main.py`\n\n```python\nclass WebsiteForm(http.Controller):\n\n    # Check and insert values from the form on the model <model>\n    @http.route('/website_form/<string:model_name>', type='http', auth=\"public\", methods=['POST'], website=True, csrf=False)\n    def website_form(self, model_name, **kwargs):\n        # Partial CSRF check, only performed when session is authenticated, as there\n        # is no real risk for unauthenticated sessions here. It's a common case for\n        # embedded forms now: SameSite policy rejects the cookies, so the session\n        # is lost, and the CSRF check fails, breaking the post for no good reason.\n        csrf_token = request.params.pop('csrf_token', None)\n        if request.session.uid and not request.validate_csrf(csrf_token):\n            raise BadRequest('Session expired (invalid CSRF token)')\n\n        model_record = request.env['ir.model'].sudo().search([('model', '=', model_name), ('website_form_access', '=', True)])\n        if not model_record:\n            return json.dumps(False)\n\n        try:\n            data = self.extract_data(model_record, request.params)\n        # If we encounter an issue while extracting data\n        except ValidationError as e:\n            # I couldn't find a cleaner way to pass data to an exception\n            return json.dumps({'error_fields' : e.args[0]})\n    ......\n\n    # Extract all data sent by the form and sort its on several properties\n    def extract_data(self, model, values):\n        dest_model = request.env[model.sudo().model]\n\n        data = {\n            'record': {},        # Values to create record\n            'attachments': [],  # Attached files\n            'custom': '',        # Custom fields values\n            'meta': '',         # Add metadata if enabled\n        }\n\n        authorized_fields = model.sudo()._get_form_writable_fields()\n        error_fields = []\n        custom_fields = []\n    ......\n```\n\nwebsite_form 裡面要注意的是 `data = self.extract_data(model_record, request.params)`,\n\n在 extract_data 裡面去呼叫了 `_get_form_writable_fields()` 也就是底下的\n\n`authorized_fields = model.sudo()._get_form_writable_fields()`\n\n而定義 _get_form_writable_fields() 在 odoo source code 中的\n\n`addons/website_form/models/models.py`\n\n```python\n......\nclass website_form_model(models.Model):\n    _name = 'ir.model'\n    _description = 'Models'\n    _inherit = 'ir.model'\n\n    ......\n\n    def _get_form_writable_fields(self):\n        \"\"\"\n        Restriction of \"authorized fields\" (fields which can be used in the\n        form builders) to fields which have actually been opted into form\n        builders and are writable. By default no field is writable by the\n        form builder.\n        \"\"\"\n        included = {\n            field.name\n            for field in self.env['ir.model.fields'].sudo().search([\n                ('model_id', '=', self.id),\n                ('website_form_blacklisted', '=', False)\n            ])\n        }\n        return {\n            k: v for k, v in self.get_authorized_fields(self.model).items()\n            if k in included\n        }\n    ......\n\n```\n\n你應該發現了 `website_form_blacklisted` 這個 field,\n\n他會將 `website_form_blacklisted` 為 False 的撈出來,\n\n我們再來看 `website_form_blacklisted` 這個 field 定義了哪些東西,\n\n路徑在 odoo source code 中的 `addons/website_form/models/models.py`\n\n```python\n......\n\nclass website_form_model_fields(models.Model):\n    \"\"\" fields configuration for form builder \"\"\"\n    _name = 'ir.model.fields'\n    _description = 'Fields'\n    _inherit = 'ir.model.fields'\n\n    @api.model_cr\n    def init(self):\n        # set all existing unset website_form_blacklisted fields to ``true``\n        #  (so that we can use it as a whitelist rather than a blacklist)\n        self._cr.execute('UPDATE ir_model_fields'\n                         ' SET website_form_blacklisted=true'\n                         ' WHERE website_form_blacklisted IS NULL')\n        # add an SQL-level default value on website_form_blacklisted to that\n        # pure-SQL ir.model.field creations (e.g. in _reflect) generate\n        # the right default value for a whitelist (aka fields should be\n        # blacklisted by default)\n        self._cr.execute('ALTER TABLE ir_model_fields '\n                         ' ALTER COLUMN website_form_blacklisted SET DEFAULT true')\n\n    @api.model\n    def formbuilder_whitelist(self, model, fields):\n        \"\"\"\n        :param str model: name of the model on which to whitelist fields\n        :param list(str) fields: list of fields to whitelist on the model\n        :return: nothing of import\n        \"\"\"\n        # postgres does *not* like ``in [EMPTY TUPLE]`` queries\n        if not fields: return False\n\n        # only allow users who can change the website structure\n        if not self.env['res.users'].has_group('website.group_website_designer'):\n            return False\n\n        # the ORM only allows writing on custom fields and will trigger a\n        # registry reload once that's happened. We want to be able to\n        # whitelist non-custom fields and the registry reload absolutely\n        # isn't desirable, so go with a method and raw SQL\n        self.env.cr.execute(\n            \"UPDATE ir_model_fields\"\n            \" SET website_form_blacklisted=false\"\n            \" WHERE model=%s AND name in %s\", (model, tuple(fields)))\n        return True\n\n    website_form_blacklisted = fields.Boolean(\n        'Blacklisted in web forms', default=True, index=True, # required=True,\n        help='Blacklist this field for web forms'\n    )\n\n```\n\n`website_form_blacklisted` field 預設會是 True,\n\n在 odoo 的設計中, 所有 website_form 的 field 預設都是 True,\n\n並需要設定成 False 才可以搭配 website_form 使用.\n\n這樣我們該怎麼設定 `website_form_blacklisted` 為 False 呢 :question:\n\nodoo source code 路徑為 `addons/website_hr_recruitment/data/config_data.xml`\n\n```xml\n......\n<function model=\"ir.model.fields\" name=\"formbuilder_whitelist\">\n            <value>hr.applicant</value>\n            <value eval=\"[\n                'description',\n                'email_from',\n                'partner_name',\n                'partner_phone',\n                'job_id',\n                'department_id',\n            ]\"/>\n</function>\n```\n\n這段程式碼會去呼叫 `addons/website_form/models/models.py` 裡面的 `formbuilder_whitelist` function,\n\n然後你可以看到 `formbuilder_whitelist` 中有一段 code\n\n```python\nself.env.cr.execute(\n    \"UPDATE ir_model_fields\"\n    \" SET website_form_blacklisted=false\"\n    \" WHERE model=%s AND name in %s\", (model, tuple(fields)))\n```\n\n會將我們設定的 fields (必須存在) 以及對應的 model (必須存在) 改為 `website_form_blacklisted=false`,\n\n而 `addons/website_hr_recruitment/data/config_data.xml` 這段 code 執行的時間點是安裝 addons 的時候,\n\n安裝 addons 完, 也可以到資料庫裡面看\n\n```sql\nSELECT id, name, model, relation, relation_field, website_form_blacklisted\n\tFROM public.ir_model_fields where model='hr.applicant' and website_form_blacklisted='False';\n```\n\n![alt tag](https://i.imgur.com/U8gNTsu.png)\n\n你會發現所設定的 fields 的 `website_form_blacklisted` 都變成 False 了.\n\n### 結論\n\n如果我們希望在 odoo source code 中的\n\n`addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml` 加一個欄位,\n\n舉個例子, 就加 `reference` 這個 fields,\n\n除了在 `website_hr_recruitment_templates.xml` 上面多加 input 之外,\n\n```xml\n......\n\n<div class=\"form-group row form-field o_website_form_required_custom\">\n    <div class=\"col-lg-3 col-md-4 text-right\">\n        <label class=\"col-form-label\" for=\"reference\">Reference</label>\n    </div>\n    <div class=\"col-lg-7 col-md-8\">\n        <input type=\"text\" class=\"form-control o_website_form_input\" name=\"reference\"/>\n    </div>\n</div>\n\n......\n\n```\n\n你還必須加入 `reference` fields (並呼叫 `formbuilder_whitelist` function),\n\n```xml\n<function model=\"ir.model.fields\" name=\"formbuilder_whitelist\">\n    <value>hr.applicant</value>\n    <value eval=\"[\n      ......\n      'reference',\n    ]\"/>\n</function>\n```\n\n這樣才可以成功的把 form 的 input 帶到 model 裡面, 因為預設的 field 都被列為黑名單,\n\n必須透過 data 中呼叫 `formbuilder_whitelist` function,\n\n將需要的 fields 的 `website_form_blacklisted` 設為 False,\n\n因為 odoo 只會取出 `website_form_blacklisted` 為 False 的 fields.\n"
  },
  {
    "path": "demo_sale_scan_barcode/README.md",
    "content": "# odoo 實作 scan barcode\n\n建議觀看影片, 因為 scanner 會看的更清楚 :smile:\n\n[Youtube Tutorial - odoo - 實作 scan barcode](https://youtu.be/o2THTpLmUec)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\nodoo 搭配 scanner,\n\n主要介紹 odoo 中如何實作 scan barcode.\n\n## 說明\n\n首先是 `__manifest__.py` 的部份,  記得要加上 `barcodes`\n\n```python\n......\n\n'depends': [......, 'barcodes'],\n......\n```\n\n接著看 [models/models.py](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_sale_scan_barcode/models/models.py)\n\n\n```python\n......\n\nclass SaleOrderBarcodes(models.Model):\n    _name = \"sale.order\"\n    _inherit = [\"sale.order\", \"barcodes.barcode_events_mixin\"]\n\n    _barcode_scanned = fields.Char(string='Barcode', help=\"Here you can provide the barcode for the product\")\n\n    @api.multi\n    def on_barcode_scanned(self, barcode):\n        product_obj = self.env['product.product'].search([('barcode', '=', barcode)], limit=1)\n        val = {\n            'product_id': product_obj,\n            'product_uom_qty': 1,\n            'price_unit': product_obj.lst_price\n        }\n        self.order_line = [(0, 0, val)]\n```\n\n`_inherit` 必須繼承 `barcodes.barcode_events_mixin`,\n\n然後要定義 `_barcode_scanned`,\n\n資料庫中是不會有 `_barcode_scanned` 的欄位, 因為 BarcodeEventsMixin 是 AbstractModel,\n\n如果不了解甚麼是 AbstractModel, 請參考 [odoo 手把手教學 - AbstractModel](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_abstractmodel_tutorial).\n\n也需要實作 `on_barcode_scanned`, 當 scanner 掃描到 barcode 時, 就會去觸發這個 method.\n\n實作的邏輯很簡單,\n\n就是使用所掃描到的 barcode 去 `product.product` 中尋找對應的產品,\n\n如果有, 就自動增加到 sale order line 中.\n\n如果不知道如何增加 One2many M2X record,\n\n可參考 [odoo 手把手教學 - 使用 python 增加取代 One2many M2X record](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)\n\n`barcode_events_mixin.py` 可參考原始碼中的 `addons/barcodes/models/barcode_events_mixin.py`\n\n```python\n......\nclass BarcodeEventsMixin(models.AbstractModel):\n    \"\"\" Mixin class for objects reacting when a barcode is scanned in their form views\n        which contains `<field name=\"_barcode_scanned\" widget=\"barcode_handler\"/>`.\n        Models using this mixin must implement the method on_barcode_scanned. It works\n        like an onchange and receives the scanned barcode in parameter.\n    \"\"\"\n\n    _name = 'barcodes.barcode_events_mixin'\n    _description = 'Barcode Event Mixin'\n\n    _barcode_scanned = fields.Char(\"Barcode Scanned\", help=\"Value of the last barcode scanned.\", store=False)\n\n    @api.onchange('_barcode_scanned')\n    def _on_barcode_scanned(self):\n        barcode = self._barcode_scanned\n        if barcode:\n            self._barcode_scanned = \"\"\n            return self.on_barcode_scanned(barcode)\n\n    def on_barcode_scanned(self, barcode):\n        raise NotImplementedError(\"In order to use barcodes.barcode_events_mixin, method on_barcode_scanned must be implemented\")\n```\n\nviews 的部份可參考 [views/view.xml](https://github.com/twtrubiks/demo_config_settings/tree/master/demo_sale_scan_barcode/views/view.xml)\n\n```xml\n<?xml version=\"1.0\"?>\n<odoo>\n    <record id=\"view_order_form_scan_barcode\" model=\"ir.ui.view\">\n        <field name=\"name\">sale.order.form.scan.barcode</field>\n        <field name=\"model\">sale.order</field>\n        <field name=\"inherit_id\" ref=\"sale.view_order_form\"/>\n        <field name=\"arch\" type=\"xml\">\n            <field name=\"partner_id\"  position=\"after\">\n                <!-- invisible=\"1\" -->\n                <field name=\"_barcode_scanned\" widget=\"barcode_handler\"/>\n            </field>\n        </field>\n    </record>\n</odoo>\n```\n\n這邊主要是加上 `<field name=\"_barcode_scanned\" widget=\"barcode_handler\"/>`,\n\n也可以選擇將它隱藏起來, 是不影響工作的.\n\n將 addons 裝起來之後, 先到 Product Variants 設定 barcode,\n\n![alt tag](https://i.imgur.com/m9o8vHY.png)\n\n範例 barcode\n\n![alt tag](https://i.imgur.com/0S5Bsu9.png)\n\n記住一定要進入編輯 (Edit) 狀態, 否則會出現錯誤 :exclamation: :exclamation:\n\n![alt tag](https://i.imgur.com/cNzb2VJ.png)\n\n也請記得要 focus 在當下的 odoo 畫面 (否則系統會抓不到) :exclamation: :exclamation:\n\nscanner 掃到條碼後, 就會將對應的產品帶入 sale order line 中.\n\n這邊建議觀看影片, 會比較清楚 :smile:\n\nscanner 其實也是將 barcode 或是 qrcode 內的資料讀取出來而已\n\n![alt tag](https://i.imgur.com/K457P5w.png)"
  },
  {
    "path": "demo_sale_scan_barcode/__init__.py",
    "content": "from . import models"
  },
  {
    "path": "demo_sale_scan_barcode/__manifest__.py",
    "content": "{\n    'name': \"demo_sale_scan_barcode\",\n    'summary': \"\"\"\n        demo_sale_scan_barcode\n    \"\"\",\n    'description': \"\"\"\n        demo_sale_scan_barcode\n    \"\"\",\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['sale', 'sale_management', 'barcodes'],\n\n    # always loaded\n    'data': [\n        'views/view.xml',\n    ],\n    'installable': True,\n    'auto_install': False,\n    'application': True,\n}\n"
  },
  {
    "path": "demo_sale_scan_barcode/models/__init__.py",
    "content": "from . import models\n"
  },
  {
    "path": "demo_sale_scan_barcode/models/models.py",
    "content": "from odoo import models, fields, api\n\nclass SaleOrderBarcodes(models.Model):\n    _name = \"sale.order\"\n    _inherit = [\"sale.order\", \"barcodes.barcode_events_mixin\"]\n\n    _barcode_scanned = fields.Char(string='Barcode', help=\"Here you can provide the barcode for the product\")\n\n    @api.multi\n    def on_barcode_scanned(self, barcode):\n        product_obj = self.env['product.product'].search([('barcode', '=', barcode)], limit=1)\n        val = {\n            'product_id': product_obj,\n            'product_uom_qty': 1,\n            'price_unit': product_obj.lst_price\n        }\n        self.order_line = [(0, 0, val)]"
  },
  {
    "path": "demo_sale_scan_barcode/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <record id=\"view_order_form_scan_barcode\" model=\"ir.ui.view\">\n        <field name=\"name\">sale.order.form.scan.barcode</field>\n        <field name=\"model\">sale.order</field>\n        <field name=\"inherit_id\" ref=\"sale.view_order_form\"/>\n        <field name=\"arch\" type=\"xml\">\n            <field name=\"partner_id\"  position=\"after\">\n                <!-- invisible=\"1\" -->\n                <field name=\"_barcode_scanned\" widget=\"barcode_handler\"/>\n            </field>\n        </field>\n    </record>\n</odoo>"
  },
  {
    "path": "demo_scheduler/README.md",
    "content": "# odoo 觀念 - scheduler\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - scheduler](https://youtu.be/uvQTHsKu3Ic)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n本篇文章主要介紹 odoo 中的 scheduler 這部份\n\n## 說明\n\n先來看 [models/models.py](models/models.py)\n\n```python\n......\n\n_logger = logging.getLogger(__name__)\n\nclass DemoScheduler(models.Model):\n    _name = 'demo.scheduler'\n    _description = 'Demo Scheduler'\n\n    def action_schedule(self):\n        _logger.warning('============= Action Schedule ==================')\n\n```\n\n簡單定義一個 class, 裡面就只有一個 function ( 用來測試 schedule ),\n\n雖然沒有定義 fields, 但是還是會在 db 中建立 model `demo_scheduler`,\n\n![alt tag](https://i.imgur.com/w8ztB9s.png)\n\n也請記得設定 security\n\n[security/ir.model.access.csv](security/ir.model.access.csv)\n\n[security/security.xml](security/security.xml)\n\n接著看 [views/scheduler.xml](views/scheduler.xml), schedule 的重點\n\n```xml\n<?xml version=\"1.0\" ?>\n<odoo>\n  <data noupdate=\"0\">\n    <record id=\"demo_scheduler\" model=\"ir.cron\">\n      <field name=\"interval_type\">days</field>\n      <field name=\"name\">demo scheduler</field>\n      <field name=\"numbercall\">-1</field>\n      <field name=\"priority\">5</field>\n      <field name=\"doall\">False</field>\n      <field name=\"active\">True</field>\n      <field name=\"interval_number\">1</field>\n      <field name=\"model_id\" ref=\"model_demo_scheduler\"/>\n      <field name=\"state\">code</field>\n      <field name=\"code\">model.action_schedule()</field>\n    </record>\n  </data>\n</odoo>\n```\n\n`interval_type` 分, 小時, 天, 禮拜, 月, 最小單位為 1 分鐘 :exclamation: :exclamation:\n\n`interval_number` 次數, 搭配 `interval_type`, 像範例就是一天執行一次.\n\n`numbercall` `-1` 代表不限制(無線循環), 如果今天設定為 `2`, 代表執行兩次之後就不會執行了.\n\n`model_id` 指定 model.\n\n`state` 使用的方式, 這邊使用 python code.\n\n`code` 呼叫 `model.action_schedule()` models 中的 methond.\n\n`priority` 0 最優先, 10最不重要(不優先).\n\n安裝完 addons 之後, 也可以到 odoo 中的後台查看 schedule, 請到以下的路徑\n\nTechnical -> Automation -> Scheduled Actions\n\n![alt tag](https://i.imgur.com/JFZD2Io.png)\n\n你應該會看到一個 demo schedule\n\n![alt tag](https://i.imgur.com/PVvYzl0.png)\n\n你也可以在這邊改細項(設定)\n\n![alt tag](https://i.imgur.com/EOsdBGg.png)\n\nRun Manually\n\n也可以手動觸發 schedule, 確保他是正常的 :smile:\n\n![alt tag](https://i.imgur.com/OrCh1mr.png)\n\n最後記得也要將 `scheduler.xml` 加入 `__manifest__.py` 哦\n\n```python\n......\n  # any module necessary for this one to work correctly\n    'depends': ['base'],\n\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'views/scheduler.xml',\n    ],\n    'application': True,\n}\n```"
  },
  {
    "path": "demo_scheduler/__init__.py",
    "content": "from . import models"
  },
  {
    "path": "demo_scheduler/__manifest__.py",
    "content": "{\n    'name': \"demo scheduler\",\n\n    'summary': \"\"\"\"\"\",\n\n    'description': \"\"\"\"\"\",\n\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['base'],\n\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'views/scheduler.xml',\n    ],\n    'application': True,\n}\n"
  },
  {
    "path": "demo_scheduler/models/__init__.py",
    "content": "from . import scheduler\n"
  },
  {
    "path": "demo_scheduler/models/scheduler.py",
    "content": "from odoo import models, fields, api\nimport logging\n\n_logger = logging.getLogger(__name__)\n\nclass DemoScheduler(models.Model):\n    _name = 'demo.scheduler'\n    _description = 'Demo Scheduler'\n\n    def action_schedule(self):\n        _logger.warning('============= Action Schedule ==================')\n"
  },
  {
    "path": "demo_scheduler/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_scheduler_user,Demo Scheduler Tutorial User Access,model_demo_scheduler,demo_scheduler_group_user,1,0,0,0\naccess_demo_scheduler_manager,Demo Scheduler Tutorial Manager Access,model_demo_scheduler,demo_scheduler_group_manager,1,1,1,1\n"
  },
  {
    "path": "demo_scheduler/security/security.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n\n  <record id=\"module_demo_scheduler_tutorial\" model=\"ir.module.category\">\n    <field name=\"name\">Demo scheduler tutorial category</field>\n  </record>\n\n  <record id=\"demo_scheduler_group_user\" model=\"res.groups\">\n    <field name=\"name\">User</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_scheduler_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('base.group_user'))]\"/>\n  </record>\n\n  <record id=\"demo_scheduler_group_manager\" model=\"res.groups\">\n    <field name=\"name\">Manager</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_scheduler_tutorial\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('demo_scheduler_group_user'))]\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_scheduler/views/scheduler.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n  <data noupdate=\"0\">\n    <record id=\"demo_scheduler\" model=\"ir.cron\">\n      <field name=\"interval_type\">days</field>\n      <field name=\"name\">demo scheduler</field>\n      <field name=\"numbercall\">-1</field>\n      <field name=\"priority\">5</field>\n      <field name=\"doall\">False</field>\n      <field name=\"active\">True</field>\n      <field name=\"interval_number\">1</field>\n      <field name=\"model_id\" ref=\"model_demo_scheduler\"/>\n      <field name=\"state\">code</field>\n      <field name=\"code\">model.action_schedule()</field>\n    </record>\n  </data>\n</odoo>\n"
  },
  {
    "path": "demo_sequence/README.md",
    "content": "# odoo 觀念 - sequence\n\n建議觀看影片, 會更清楚 :smile:\n\n[Youtube Tutorial - odoo 手把手教學 - sequence](https://youtu.be/u8v0hzEXwpc)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[odoo 手把手建立第一個 addons](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial)\n\n本篇文章主要介紹 odoo 中的 sequence 這部份\n\n## 說明\n\n先來看 [data/sequence_data.xml](data/sequence_data.xml)\n\n```xml\n<data noupdate=\"0\">\n  <record id=\"demo_sequence_id\" model=\"ir.sequence\">\n      <field name=\"name\">demo_sequence_sequence</field>\n      <field name=\"code\">demo.sequence</field>\n      <field name=\"active\">True</field>\n      <field name=\"prefix\">%(year)s%(month)s%(day)s</field>\n      <field name=\"padding\">5</field>\n      <field name=\"number_next\">1</field>\n      <field name=\"number_increment\">1</field>\n  </record>\n</data>\n```\n\n這邊是設定 sequence 的資料, 以下說明參數所代表的意義,\n\n`name` sequence 名稱.\n\n`code` sequence 的 code. (通常我會把它定義和 model 名稱一樣).\n\n`active` 是否 active.\n\n`prefix` 依照自己的需求定義, 這邊範例式增加 年月日.\n\n`padding` sequence size (字元數).\n\n`number_next` 下一個顯示的數字.\n\n`number_increment` 每次增加數字的單位.\n\n這些資訊也可以在 odoo 的介面上看到\n\n路徑 Technical -> Sequences & Identifiers -> Sequences\n\n![alt tag](https://i.imgur.com/pEQQonC.png)\n\n點進去會看到\n\n![alt tag](https://i.imgur.com/0g87J44.png)\n\n當然, 你也可以在這邊修改.\n\n再來看 [models/models.py](models/models.py)\n\n```python\n......\nclass DemoSequence(models.Model):\n    _name = 'demo.sequence'\n    _description = 'Demo Sequence'\n\n    name = fields.Char(string='Name', required=True)\n    sequence = fields.Char(string=\"sequence\", readonly=True)\n\n    @api.model\n    def create(self, vals):\n        seq = self.env['ir.sequence'].next_by_code('demo.sequence') or '/'\n        vals['name'] = '{}_{}'.format(seq, vals['name'])\n        new_record = super().create(vals)\n        return new_record\n```\n\n重點就在 `create` 裡面, 在 odoo 中, 只要你建立一比 record, 就會去呼叫 `create`,\n\n這邊我們去覆寫它, 將 name 加上 sequence.\n\n`next_by_code('demo.sequence')` 這端就是去呼叫你自己定義的 sequence code.\n\n來看一下效果,\n\n新建一筆 record, 名稱為 test\n\n![alt tag](https://i.imgur.com/9vL348K.png)\n\n儲存後你會發現加上了你自己定義的 sequence\n\n![alt tag](https://i.imgur.com/62Sg69B.png)\n\n這邊要另外說明一下 `Implementation` 這個參數, 它有兩個選項,\n\n分別是 `Standard`(預設), `No gap`\n\n![alt tag](https://i.imgur.com/TRSEzrZ.png)\n\n假設今天的 sequence 是 001, 我們再建立一比, 這時候是 002,\n\n當我們把 002 這筆刪除, 然後再建立一筆新的, 這時候差異就多來了 :smile:\n\n`Standard`(預設)\n\n新的那一筆會是 003.\n\n`No gap`\n\n新的那一筆會是 002.\n\n如果你把此範例改成 `No gap`, 是不會產生效果的, 應該是要搭配其他的 code.\n\n(以後我再補充給大家, 或自行去閱讀 source code 參考寫法 :smile:)\n"
  },
  {
    "path": "demo_sequence/__init__.py",
    "content": "from . import models"
  },
  {
    "path": "demo_sequence/__manifest__.py",
    "content": "{\n    'name': \"demo sequence\",\n    'summary': \"\"\"\"\"\",\n\n    'description': \"\"\"\"\"\",\n\n    'author': \"My Company\",\n    'website': \"http://www.yourcompany.com\",\n\n    # Categories can be used to filter modules in modules listing\n    # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml\n    # for the full list\n    'category': 'Uncategorized',\n    'version': '0.1',\n\n    # any module necessary for this one to work correctly\n    'depends': ['base'],\n\n    # always loaded\n    'data': [\n        'security/security.xml',\n        'security/ir.model.access.csv',\n        'data/sequence_data.xml',\n        'views/menu.xml',\n        'views/view.xml',\n    ],\n    'application': True,\n}\n"
  },
  {
    "path": "demo_sequence/data/sequence_data.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n\n  <!-- demo_sequence -->\n  <data noupdate=\"0\">\n    <record id=\"demo_sequence_id\" model=\"ir.sequence\">\n        <field name=\"name\">demo_sequence_sequence</field>\n        <field name=\"code\">demo.sequence</field>\n        <field name=\"active\">True</field>\n        <field name=\"prefix\">%(year)s%(month)s%(day)s</field>\n        <field name=\"padding\">5</field>\n        <field name=\"number_next\">1</field>\n        <field name=\"number_increment\">1</field>\n    </record>\n  </data>\n  <!-- demo_sequence -->\n\n</odoo>\n"
  },
  {
    "path": "demo_sequence/models/__init__.py",
    "content": "from . import model"
  },
  {
    "path": "demo_sequence/models/model.py",
    "content": "from odoo import models, fields, api\n\nclass DemoSequence(models.Model):\n    _name = 'demo.sequence'\n    _description = 'Demo Sequence'\n\n    name = fields.Char(string='Name', required=True)\n\n    @api.model\n    def create(self, vals):\n        seq = self.env['ir.sequence'].next_by_code('demo.sequence') or '/'\n        vals['name'] = '{}_{}'.format(seq, vals['name'])\n        new_record = super().create(vals)\n        return new_record\n"
  },
  {
    "path": "demo_sequence/security/ir.model.access.csv",
    "content": "id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink\naccess_demo_sequence_user,Demo Sequence User Access,model_demo_sequence,demo_sequence_group_user,1,0,0,0\naccess_demo_sequence_manager,Demo Sequence Manager Access,model_demo_sequence,demo_sequence_group_manager,1,1,1,1"
  },
  {
    "path": "demo_sequence/security/security.xml",
    "content": "<?xml version=\"1.0\" ?>\n<odoo>\n\n  <record id=\"module_demo_sequence_category\" model=\"ir.module.category\">\n    <field name=\"name\">Demo Sequence</field>\n  </record>\n\n  <record id=\"demo_sequence_group_user\" model=\"res.groups\">\n    <field name=\"name\">User</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_sequence_category\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('base.group_user'))]\"/>\n  </record>\n\n  <record id=\"demo_sequence_group_manager\" model=\"res.groups\">\n    <field name=\"name\">Manager</field>\n    <field name=\"category_id\"\n           ref=\"module_demo_sequence_category\"/>\n    <field name=\"implied_ids\"\n           eval=\"[(4, ref('demo_sequence_group_user'))]\"/>\n    <field name=\"users\"\n           eval=\"[(4, ref('base.user_root')),\n                  (4, ref('base.user_admin'))]\"/>\n  </record>\n\n</odoo>"
  },
  {
    "path": "demo_sequence/views/menu.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n    <!-- Demo Sequence Menu -->\n    <menuitem id=\"demo_sequence_menu\"\n      name=\"Demo Sequence\" />\n\n    <!-- Action to open the demo_sequence list -->\n    <act_window id=\"action_demo_sequence\"\n      name=\"Demo Sequence\"\n      res_model=\"demo.sequence\"\n      view_mode=\"tree,form\"/>\n\n    <!-- Menu item to open the demo_sequence list -->\n    <menuitem id=\"menu_demo_sequence\"\n      name=\"Demo Sequence\"\n\t    action=\"action_demo_sequence\"\n      parent=\"demo_sequence_menu\" />\n</odoo>\n\n\n"
  },
  {
    "path": "demo_sequence/views/view.xml",
    "content": "<?xml version=\"1.0\"?>\n<odoo>\n\n\n  <record id=\"view_form_demo_sequence\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Sequence Form</field>\n    <field name=\"model\">demo.sequence</field>\n    <field name=\"arch\" type=\"xml\">\n      <form>\n        <group>\n          <field name=\"name\"/>\n        </group>\n      </form>\n    </field>\n  </record>\n\n  <record id=\"view_tree_demo_sequence\" model=\"ir.ui.view\">\n    <field name=\"name\">Demo Sequence List</field>\n    <field name=\"model\">demo.sequence</field>\n    <field name=\"arch\" type=\"xml\">\n        <tree>\n            <field name=\"name\"/>\n        </tree>\n    </field>\n  </record>\n\n</odoo>"
  },
  {
    "path": "domain_operator_tutorial/README.md",
    "content": "# Odoo Domain Operator 教學\n\n[Youtube Tutorial - odoo 手把手教學 - Odoo Domain Operator 教學](https://youtu.be/E0LrhmozZQI)\n\n建議在閱讀這篇文章前, 先了解這些概念\n[Odoo Domain 教學](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/odoo_domain_tutorial), [odoo - 如何透過 log_level 了解 ORM RAW SQL](https://github.com/twtrubiks/odoo-docker-tutorial#odoo---%E5%A6%82%E4%BD%95%E9%80%8F%E9%81%8E-log_level-%E4%BA%86%E8%A7%A3-orm-raw-sql)\n\n主要是要介紹 sql `like` 的一些變化,\n\n請隨便找一個 model , 進入 `shell` 測試, 例如這邊使用 `res.partner`,\n\n```python\nself.env['res.partner'].search(domain)\n```\n\n先說明一下,\n\n`like` 區分大小寫\n\n`ilike` 不區分大小寫\n\n範例一\n\n```python\ndomain = [('name', 'like', 'twtrubiks')]\n```\n\n```sql\nWHERE \"name\"::text like '%twtrubiks%'\n```\n\n`\"name\"::text` 這個是 Type Casts, 可參考 [postgresql - 4.2.9. Type Casts](https://www.postgresql.org/docs/10/sql-expressions.html#SQL-SYNTAX-TYPE-CASTS)\n\n以下兩種寫法都可以, 主要是轉換類型.\n\n例如這邊是將 `name` 轉換成 `text`\n\n```sql\nCAST ( expression AS type )\nexpression::type\n```\n\n範例二\n\n```python\ndomain = [('name', 'ilike', 'twtrubiks')]\n```\n\n```sql\nWHERE \"name\"::text ilike '%twtrubiks%'\n```\n\n範例三\n\n```python\ndomain = [('name', '=like', 'twtrubiks')]\n```\n\n在這種情況下, `=like` 和 `=` 是一樣的.\n\n```sql\nWHERE \"name\"::text like 'twtrubiks'\n```\n\n範例四\n\n```python\ndomain = [('name', '=ilike', 'twtrubiks')]\n```\n\n```sql\nWHERE \"name\"::text ilike 'twtrubiks'\n```\n\n~~在這種情況下, `=ilike` 和 `=` 是一樣的.~~\n\n範例五\n\n```python\ndomain = [('name', '=like', 'twtrubiks%')]\n```\n\n```sql\nWHERE \"name\"::text like 'twtrubiks%'\n```\n\n範例六\n\n```python\ndomain = [('name', '=ilike', 'twtrubiks%')]\n```\n\n```sql\nWHERE \"name\"::text ilike 'twtrubiks%')\n```\n"
  },
  {
    "path": "odoo_domain_tutorial/README.md",
    "content": "# Odoo Domain 教學\n\n[Youtube Tutorial - odoo 手把手教學 - Odoo Domain 教學](https://youtu.be/Gr8eXYRSrtM)\n\n先附上 odoo [domains](https://www.odoo.com/documentation/12.0/howtos/backend.html#domains) 的文件\n\n首先, odoo 是使用 波蘭表示法. (小技巧, 從後面看回來)\n\n這邊先說一下規則, 等等會帶大家看,\n\n從 右 向 左 開始看, 當遇到運算符號的時候, 找它的右側的兩個值去做運算.\n\n符號說明如下,\n\n`&` -> `AND` (這是預設邏輯，可以不寫, 不寫就代表 `&`)\n\n`|` -> `OR`\n\n`!` -> `NOT`\n\n來看幾個範例\n\n```python\ndomain = [A, B]\n\ndomain = ['&', A, B]\n```\n\n上述兩個都是 A AND B\n\n```python\ndomain = ['&', A, '&', B, C]\n```\n\n上述代表 A AND (B AND C)\n\n```python\ndomain = [A, B, C]\n```\n\n上述代表 (A AND B) AND C\n\n如果都是 AND 的情況下, 上述兩個可以看成是相等的.\n\n```python\ndomain = ['|', A, B, C]\n```\n\n分解如下\n\n```text\n從右邊開始看\n\n-> (A OR B), C\n\n-> (A OR B) AND C\n```\n\n上述代表 (A OR B) AND C\n\n```python\ndomain = [A, B, '|', C, D]\n```\n\n上述代表 (A AND B) AND (C OR D)\n\n分解如下\n\n```text\n從右邊開始看\n\n-> A, B, (C OR D)\n\n-> A, B AND (C OR D)\n\n-> A AND B AND (C OR D)\n```\n\n```python\ndomain = ['|', A, B, C, D]\n```\n\n上述代表 ((A OR B) AND C ) AND D\n\n分解如下\n\n```text\n從右邊開始看\n\n-> (A OR B), C, D\n\n-> (A OR B) AND C, D\n\n-> (A OR B) AND C AND D\n```\n\n接下來看一個比較複雜的\n\n```python\ndomain = ['|', A, '!', '&', B, C]\n```\n\n上述代表 A OR ( NOT B OR NOT C)\n\n也等於 A OR ( NOT (B AND C))\n\n分解如下\n\n```text\n從右邊開始看\n\n-> '|', A, '!', (B AND C)\n\n-> '|', A, NOT (B AND C)\n\n-> A OR ( NOT (B AND C) )\n```\n\n接下來看一個更複雜的\n\n```python\ndomain = [\n     '&', '&', '&',\n     A,\n     B,\n     C,\n     '|', '&',\n     D,\n     E,\n     F,\n]\n```\n\n上述代表 A AND B AND C AND ((D AND E) OR F)\n\n分解如下\n\n```text\n從右邊開始看\n\n-> '&', '&', '&', A, B, C, '|', (D AND E), F\n\n-> '&', '&', '&', A, B, C, (D AND E) OR F\n\n-> '&', '&', '&', A, B, C AND ((D AND E) OR F)\n\n-> A AND B AND C AND ((D AND E) OR F)\n```\n\n推薦這個網站, 他可以幫助你檢查邏輯是不是相同的 [wolframalpha](https://www.wolframalpha.com)\n\n如果你和我一樣實在還是搞不懂 波蘭表示法\n\n可以嘗試另外一個土法煉鋼的方法, 看看下面這個例子,\n\n透過之前教過的 [odoo - 如何透過 log_level 了解 ORM RAW SQL](https://github.com/twtrubiks/odoo-docker-tutorial#odoo---%E5%A6%82%E4%BD%95%E9%80%8F%E9%81%8E-log_level-%E4%BA%86%E8%A7%A3-orm-raw-sql) 搭配 shell,\n\n在 odoo shell 底下執行,\n\n```python\ndomain = [('parent_id', '=', 9), ('company_id', '=', 1),\n          '|', ('city','=','Columbia'), ('city','=','Jonesboro')]\nself.env['res.partner'].search(domain)\n```\n\n會輸出以下的 sql ( 經過 格式化 處理 )\n\n```sql\nSELECT \"res_partner\".id\nFROM \"res_partner\"\nWHERE ((((\"res_partner\".\"active\" = TRUE)\n         AND (\"res_partner\".\"parent_id\" = 9))\n        AND (\"res_partner\".\"company_id\" = 1))\n       AND ((\"res_partner\".\"city\" = 'Columbia')\n            OR (\"res_partner\".\"city\" = 'Jonesboro')))\nORDER BY \"res_partner\".\"display_name\"\n```\n\n我們先把 domain 改成比較簡單的格式\n\n```python\ndomain = [A, B, '|', C, D]\n```\n\n再把 SQL 也修改一下\n\n```sql\nSELECT \"res_partner\".id\nFROM \"res_partner\"\nWHERE ((((\"res_partner\".\"active\" = TRUE)\n         AND A)\n        AND B)\n       AND (C\n            OR D))\nORDER BY \"res_partner\".\"display_name\"\n```\n\n再把不相關的移除就變成了我們想要看到的邏輯了\n\n(A AND B) AND (C OR D)\n\n延伸閱讀 [Odoo Domain Operator 教學](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/domain_operator_tutorial)\n\n## 更進階複雜的查詢\n\n有時候我們會需要更複雜的查詢, 這時候可以這樣使用\n\n`from odoo.osv.expression import AND, OR`\n\n```python\nfrom odoo.osv.expression import AND, OR\ncondition1 = [('login', '=', 'admin')]\ncondition2 = [('partner_id', '=', 7)]\n\ncombined_condition_or = OR([condition1, condition2])\ncombined_condition_and = AND([condition1, condition2])\n\nself.env['res.users'].search(combined_condition_or)\nself.env['res.users'].search(combined_condition_and)\n```"
  },
  {
    "path": "odoo_index_tutorial/README.md",
    "content": "# odoo index 教學\n\n* [Youtube Tutorial - odoo index 教學](https://youtu.be/S9jahU9aG-s)\n\nodoo 的 index 主要就是 postgresql 的 index,\n\n所以讓我們來看看 postgresql 的官方文件吧 :smile:\n\n首先, 請先看看 [postgresql-explain-plan](https://use-the-index-luke.com/sql/explain-plan/postgresql/operations),\n\n然後 Scan 的方式也有多種, 可參考 [postgresql-explain](https://docs.postgresql.tw/the-sql-language/performance-tips/using-explain)\n\n`Seq Scan` `Index Scan` `Index Only Scan (since PostgreSQL 9.2)`\n\n`Bitmap Index Scan` `Bitmap Heap Scan` `Recheck Cond`\n\n接著看一下 odoo sourcecode 中的 [odoo/fields.py](odoo/fields.py),\n\n你會發現, 預設的 index 為 `False`\n\n```python\n......\nclass Field(MetaField('DummyField', (object,), {})):\n    \"\"\"\n\t......\n    :param bool index: whether the field is indexed in database. Note: no effect\n        on non-stored and virtual fields. (default: ``False``)\n\n\t......\n    \"\"\"\n\n    ......\n\n    store = True                        # whether the field is stored in database\n    index = False                       # whether the field is indexed in database\n......\n\n```\n\n使用方法也很簡單, 在需要的 fields 中加上 `index=True` 即可,\n\n```python\nsequence = fields.Integer(index=True, help=\"Gives the sequence order\", default=1)\n```\n\n確認 index 有成功加入的方法,\n\n可以從 odoo 的後台觀看\n\n![alt tag](https://i.imgur.com/CfAfRxr.png)\n\n也可以從工具(pgadmin4)觀看\n\n![alt tag](https://i.imgur.com/VQ3ffeG.png)\n\n使用 `explain` 指令查看 explain\n\n```sql\nexplain\nSELECT * FROM demo_expense_tutorial where name='123';\n```\n\n![alt tag](https://i.imgur.com/nP1W2JU.png)\n\n也可以使用工具(pgadmin4)觀看\n\n![alt tag](https://i.imgur.com/AKHXfym.png)\n\n但這邊要注意一下 :exclamation: 不是你有設定 index, 就代表 postgresql 會去使用你的 index,\n\n它可能會根據你的資料量決定要不要使用 index, 所以你會發現儘管你設定了 index,\n\n但它卻還是使用 `Seq Scan`, 因為 postgresql 的演算法可能覺得在這個情況下 `Seq Scan` 更快 :smile:\n\n\n可以吃到 index\n\n```sql\nexplain\nSELECT * FROM demo_expense_tutorial where name='123%';\n```\n\n無法吃到 index, 建議改用 full-text search.\n\n```sql\nexplain\nSELECT * FROM demo_expense_tutorial where name='%123%';\n```\n\n無法吃到 index\n\n```sql\nexplain\nSELECT * FROM demo_expense_tutorial where name='%123';\n```"
  },
  {
    "path": "session_redis_tutorial/README.md",
    "content": "# odoo session_redis 教學\n\n[Youtube Tutorial - odoo 手把手教學 - session_redis 教學](https://youtu.be/WD7W9RwusS0)\n\n## 簡介\n\n這篇主要是教大家如何在 odoo 中使用 redis,\n\n就使用現成的 addons [https://apps.odoo.com/apps/modules/12.0/session_redis/](https://apps.odoo.com/apps/modules/12.0/session_redis/) 來示範.\n\n在開始介紹前, 要知道在 odoo 中, session 是保存在實體檔案 (filesystem) 的,\n\n通常也就是 [odoo.conf](https://github.com/twtrubiks/odoo-docker-tutorial/blob/master/config/odoo.conf) 中的 data_dir 路徑,\n\n如果你到該路徑的 sessions 底下查看, 會看到很多 `.sess` 檔\n\n![alt tag](https://i.imgur.com/SxfvBfE.png)\n\n如果你把這些檔案全部都刪除, 你會發現每個 user 都被登出了,\n\n因為 odoo 用這些檔案決定使用者多久後會被登出(也就是使用這些session).\n\n這個 addon 只要是將保存在 file 中的 session 全部改成存在 redis 中,\n\n你可能會問我有甚麼好處 :question:\n\n速度絕對變快, 然後如果你有多 odoo 的機器, 也比較好方便統一管理 session.\n\n## redis\n\nredis 的部份這邊我就簡單放個 [docker-compose.yml](docker-compose.yml),\n\n因為之前在 [django-docker-redis-tutorial](https://github.com/twtrubiks/django-docker-redis-tutorial) 都有介紹過了.\n\ndocker redis 的 image 是使用 [https://hub.docker.com/_/redis](https://hub.docker.com/_/redis)\n\n直接執行即可\n\n```cmd\ndocker-compose up -d\n```\n\n## session_redis\n\n有了這些觀念, 接下來就來看 [https://apps.odoo.com/apps/modules/12.0/session_redis/](https://apps.odoo.com/apps/modules/12.0/session_redis/)\n\n請先把 addon 下載下來, 然後放到目錄資料夾底下,\n\n之後先安裝 redis 套件,\n\n```cmd\npip install redis\n```\n\n接著設定環境變數,\n\n暫時的設定環境參數(退出 shell 會自動消失)\n\n```cmd\nexport ODOO_SESSION_REDIS=1\nexport ODOO_SESSION_REDIS_COPY_EXISTING_FS_SESSIONS=1\nexport ODOO_SESSION_REDIS_PURGE_EXISTING_FS_SESSIONS=1\n\n# 還有很多設定請參考文件說明\nexport ODOO_SESSION_REDIS_EXPIRATION\n```\n\n如果要查看設定\n\n```cmd\necho $ODOO_SESSION_REDIS\necho $ODOO_SESSION_REDIS_COPY_EXISTING_FS_SESSIONS\n```\n\n最後要到 [odoo.conf](https://github.com/twtrubiks/odoo-docker-tutorial/blob/master/config/odoo.conf) 中多設定 `server_wide_modules`,\n\n```conf\n......\nserver_wide_modules=base,web,session_redis\n......\n```\n\n或是也可以寫在 command line 上 ( 兩者擇一即可 ),\n\n```cmd\npython3 odoo-bin -d odoo -c odoo.conf --load=base,web,session_redis\n```\n\n`--load` 和 `server_wide_modules` 是一樣的.\n\n可參考原始碼中的 `odoo/tools/config.py`\n\n```python\nclass configmanager(object):\n    def __init__(self, fname=None):\n        ......\n        group.add_option(\"--load\", dest=\"server_wide_modules\", help=\"Comma-separated list of server-wide modules.\", my_default='base,web')\n        ......\n```\n\n`session_redis` 就是這個 addon 的名稱,\n\n其中, `base,web` 是預設的, 可參考以下文件\n\n[https://odoo-development.readthedocs.io/en/latest/admin/server_wide_modules.html](https://odoo-development.readthedocs.io/en/latest/admin/server_wide_modules.html)\n\n預設為 `base,web` 也可參考原始碼中的 `addons/web/controllers/main.py`\n\n```python\n......\ndef module_boot(db=None):\n    server_wide_modules = odoo.conf.server_wide_modules or []\n    serverside = ['base', 'web']\n    ......\n\n```\n\n關於 `load` 的說明還可以參考官方文件,\n\n[https://www.odoo.com/documentation/15.0/developer/misc/other/cmdline.html#cmdoption-odoo-bin-load](https://www.odoo.com/documentation/15.0/developer/misc/other/cmdline.html#cmdoption-odoo-bin-load)\n\n`--load <modules>`\n\n```text\nlist of server-wide modules to load. Those modules are supposed to provide features not necessarily tied to a particular database. This is in contrast to modules that are always bound to a specific database when they are installed (i.e. the majority of Odoo addons). Default is base,web.\n```\n\n將著啟動 odoo, 然後 **不需要** 安裝 `session_redis`.\n\n( 因為已經透過 `server_wide_modules` 的方式啟動了 )\n\n理論上, 這時候你的 `data_dir` 中的 sessions 資料夾應該是空的.\n\n然後你可以 access redis 確認是否有資料寫入,\n\n```cmd\ndocker exec -it <container id> redis-cli\n```\n\n查看全部的 keys\n\n```cmd\nkeys *\n```\n\n查看某個 key 的資料\n\n```cmd\nget <key>\n```\n\n![alt tag](https://i.imgur.com/7i4UT2h.png)\n\n也可以透過 `ttl` 查看還有多久這個 session 會過期\n\n```cmd\nttl <key>\n```\n\n刪除全部的 keys\n\n```cmd\nflushall\n```\n\n如果 redis 中也有資料, 這樣就對表成功了 :smile:\n\n成功將 session 從檔案中移動至 redis,\n\n新的 session 也都會保存在 redis 裡面.\n\n如果你想看實作, 可以再參考 [https://apps.odoo.com/apps/modules/12.0/session_redis/](https://apps.odoo.com/apps/modules/12.0/session_redis/).\n"
  },
  {
    "path": "session_redis_tutorial/docker-compose.yml",
    "content": "version: '3.5'\n\nservices:\n  cache:\n    image: redis\n    restart: always\n    ports:\n      - '6379:6379'\n#     command: redis-server --save 60 1 --loglevel warning --requirepass change_me --appendonly yes\n#     command: redis-server --save 60 1 --loglevel warning  --appendonly yes\n#     volumes:\n#       - cache:/data\n\n# volumes:\n#   cache:"
  },
  {
    "path": "xml-rpc-odoo/README.md",
    "content": "# 如何使用 python xmlrpc 連接 odoo-12\n\n此版本為 odoo12, odoo14 版本請參考 [odoo14](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/14.0/xml-rpc-odoo) 分支.\n\n建議觀看影片, 會更清楚 :smile:\n\n* [Youtube Tutorial - 如何使用 python xmlrpc 連接 odoo - part1](https://youtu.be/MuMBF8a9ko8)\n\n* [Youtube Tutorial - 如何使用 python xmlrpc 連接 odoo - part2](https://youtu.be/KFBaTB_XRJM)\n\n建議在閱讀這篇文章之前, 請先確保了解看過以下的文章 (因為都有連貫的關係)\n\n[demo_odoo_tutorial](https://github.com/twtrubiks/odoo-demo-addons-tutorial/tree/master/demo_odoo_tutorial) -  odoo 手把手建立第一個 addons\n\n主要介紹 xmlrpc\n\n## 說明\n\nExternal API 官方文件\n\n[https://www.odoo.com/documentation/12.0/webservices/odoo.html](https://www.odoo.com/documentation/12.0/webservices/odoo.html)\n\n`xmlrpc/2/common`\n\nprovides meta-calls which don’t require authentication.\n\n`xmlrpc/2/object`\n\nis used to call methods of odoo models via the execute_kw RPC function.\n\n程式碼請參考 [demo.py](demo.py), 每個 function 都能執行,\n\n(記得要先啟動一個 odoo 並填上 url, name, password, 也要選擇載入 demo data 哦)\n\n此外, 裡面用到很多的 m2x 的 add, edit, update, delete 語法, 請參考下方\n\n```xml\n(0, 0,  { values })    link to a new record that needs to be created with the given values dictionary\n(1, ID, { values })    update the linked record with id = ID (write *values* on it)\n(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)\n(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)\n(4, ID)                link to existing record with id = ID (adds a relationship)\n(5)                    unlink all (like using (3,ID) for all linked records)\n(6, 0, [IDs])          replace the list of linked IDs (like using (5) then (4,ID) for each ID in the list of IDs)\n```\n\n## 自定義 function\n\n有時候使用標準的 xml rpc 的 `read_search` 不是那麼方便, 像是我要拿 Many2one 的東西,\n\n就必須透過 id 再 access 一次, 蠻麻煩的, 所以還可以自己定義 function 以及 回傳格式.\n\n在 odoo 端要這樣寫, 記得要加上 `@api.model`,\n\n```python\nclass HrExpenseCustom(models.Model):\n    _inherit = \"hr.expense\"\n\n    @api.model\n    def custom_func(self, expense_sheet_id):\n        sheet = self.env['hr.expense.sheet'].browse(expense_sheet_id)\n        return {\n            'name': sheet.name,\n            'line_ids': [{\n                'id': rec.id,\n                'name': rec.name,\n            } for rec in sheet.expense_line_ids]\n        }\n```\n\n透過 xml rpc 呼叫的方式如下,\n\n```python\n......\n\ndata = models.execute_kw(db, uid, password,\n    'hr.expense', 'custom_func', [], {'expense_sheet_id': 5})\n\n# print(data)\n# {'name': 'Screen', 'line_ids': [{'id': 3, 'name': 'Travel by car'}, {'id': 1, 'name': 'Screen'}]}\n```\n\n這樣整體方便不少, 開發也比較快速.\n\n## 遇到 None 值\n\n在使用 xmlrpc 的時候, 如果你會傳 null 值, 可能會出現以下的錯誤\n\n```text\ncannot marshal None unless allow_none is enabled\n```\n\n要修正這個錯誤也很簡單, 加上 `allow_none=True`\n\n```python\ncommon = xmlrpc.client.ServerProxy(f'{odoo_url}/xmlrpc/2/common')\nuid = common.authenticate(db, username, password, {})\n\n# 如果值傳 None 會出現以上錯誤, 要記得加上 allow_none=True\nmodels = xmlrpc.client.ServerProxy(f'{odoo_url}/xmlrpc/2/object', allow_none=True)\n```\n\n## 其他第三方工具\n\n多數都是從 xmlrpc 衍生出來的\n\n[https://github.com/OCA/odoorpc](https://github.com/OCA/odoorpc)\n\n[https://github.com/tinyerp/erppeek](https://github.com/tinyerp/erppeek)"
  },
  {
    "path": "xml-rpc-odoo/demo.py",
    "content": "import xmlrpc.client\nfrom pprint import pprint\n\n# use xmlrpc 時, 建議回傳 true\n# The reason is that not all client implementations of the XML-RPC protocol\n# support None/Null values, and may raise errors when such a value is returned\n# by a method.\n\nurl = 'http://0.0.0.0:8069'\ndb = 'odoo'\nusername = 'admin'\npassword = 'admin'\n\ndef common_version():\n    # provides meta-calls which don’t require authentication\n    common = xmlrpc.client.ServerProxy('{}/xmlrpc/2/common'.format(url))\n    common.version()\n    print(common.version())\n    return common\n\ndef get_uid():\n    # Logging in\n    common = common_version()\n    uid = common.authenticate(db, username, password, {})\n    print('uid:', uid)\n    return uid\n\ndef endpoint_object():\n    #  is used to call methods of odoo models via the execute_kw RPC function.\n    return xmlrpc.client.ServerProxy('{}/xmlrpc/2/object'.format(url))\n\ndef call_check_access_rights():\n    # Calling methods\n    models = endpoint_object()\n    uid = get_uid()\n    data = models.execute_kw(db, uid, password,\n        'res.partner', 'check_access_rights',\n        ['read'], {'raise_exception': False})\n    print(data)\n\ndef list_all_records():\n    models = endpoint_object()\n    uid = get_uid()\n    # List all records\n    records_data = models.execute_kw(db, uid, password,\n        'res.partner', 'search',[[]])\n    print(records_data)\n\ndef list_records():\n    models = endpoint_object()\n    uid = get_uid()\n    # List records\n    records_data = models.execute_kw(db, uid, password,\n        'res.partner', 'search',\n        [[['is_company', '=', True], ['customer', '=', True]]])\n    print(records_data)\n\ndef count_records():\n    models = endpoint_object()\n    uid = get_uid()\n    # Count records\n    records_count = models.execute_kw(db, uid, password,\n        'res.partner', 'search_count',\n        [[['is_company', '=', True], ['customer', '=', True]]])\n    print(records_count)\n\ndef read_records():\n    models = endpoint_object()\n    uid = get_uid()\n    # Read records\n    ids = models.execute_kw(db, uid, password,\n        'res.partner', 'search',\n        [[['is_company', '=', True], ['customer', '=', True]]],\n        {'limit': 1})\n    print('ids:', ids)\n    return ids\n\ndef read_all_field():\n    models = endpoint_object()\n    uid = get_uid()\n    # Read records ids\n    ids = read_records()\n    # all field\n    record = models.execute_kw(db, uid, password,\n        'res.partner', 'read', [ids])\n    print(record)\n\ndef read_need_field():\n    models = endpoint_object()\n    uid = get_uid()\n    # Read records ids\n    ids = read_records()\n    # need field\n    record = models.execute_kw(db, uid, password,\n        'res.partner', 'read',\n        [ids], {'fields': ['name', 'country_id', 'comment']})\n    print('record:', record)\n\ndef listing_record_fields_attributes():\n    models = endpoint_object()\n    uid = get_uid()\n    # Listing record fields attributes\n    listing_record_fields = models.execute_kw(\n        db, uid, password, 'res.partner', 'fields_get',\n        [], {'attributes': ['string', 'help', 'type']})\n    print('Listing record fields')\n    pprint(listing_record_fields)\n\ndef search_and_read():\n    models = endpoint_object()\n    uid = get_uid()\n    # Search and read\n    search_and_read = models.execute_kw(db, uid, password,\n        'res.partner', 'search_read',\n        [[['is_company', '=', True], ['customer', '=', True]]],\n        {'fields': ['name', 'country_id', 'comment'], 'limit': 5})\n    print('Search and read')\n    pprint(search_and_read)\n\ndef create_reads():\n    models = endpoint_object()\n    uid = get_uid()\n    # Create records\n    models.execute_kw(db, uid, password, 'res.partner', 'create', [{\n        'name': \"New Partner_2\",\n    }])\n\n    # read Create records\n    search_and_read = models.execute_kw(db, uid, password,\n        'res.partner', 'search_read',\n        [[['name', '=', 'New Partner_2']]],\n        {'fields': ['name'], 'limit': 5})\n    pprint(search_and_read)\n\ndef update_records():\n    models = endpoint_object()\n    uid = get_uid()\n\n    # read res.partner\n    search_and_read = models.execute_kw(db, uid, password,\n        'res.partner', 'search_read',\n        [[['name', '=', 'New Partner_2']]],\n        {'fields': ['id'], 'limit': 1})\n    my_partner_id = search_and_read[0]['id']\n    print('my_partner_id:', my_partner_id)\n\n    # Update records\n    models.execute_kw(db, uid, password, 'res.partner', 'write', [[my_partner_id], {\n        'name': \"hello\"\n    }])\n    # get record name after having changed it\n    my_data = models.execute_kw(db, uid, password, 'res.partner', 'name_get',[[my_partner_id]])\n    pprint(my_data)\n\ndef delete_record():\n    # please installl sale addons\n    models = endpoint_object()\n    uid = get_uid()\n\n    # read res.partner\n    my_partner_id = 40\n\n    # Delete records\n    models.execute_kw(db, uid, password, 'res.partner', 'unlink', [[my_partner_id]])\n    # check if the deleted record is still in the database\n    my_data = models.execute_kw(db, uid, password,\n        'res.partner', 'search', [[['id', '=', my_partner_id]]])\n    pprint(my_data)\n\ndef many2one_create():\n    # please installl sale addons\n    models = endpoint_object()\n    uid = get_uid()\n\n    # read res.partner\n    search_and_read = models.execute_kw(db, uid, password,\n        'res.partner', 'search_read',\n        [[['name', '=', 'hello']]],\n        {'fields': ['id'], 'limit': 1})\n    my_partner_id = search_and_read[0]['id']\n    print('my_partner_id:', my_partner_id)\n\n    # Many2one - create\n    id_ = models.execute_kw(db, uid, password, 'sale.order', 'create', [{\n        'partner_id': my_partner_id,\n    }])\n\n    # get record name after having changed it\n    # check form pgadmin4\n    my_data = models.execute_kw(db, uid, password, 'sale.order', 'name_get',[[id_]])\n    pprint(my_data)\n\ndef many2many_add_record():\n    models = endpoint_object()\n    uid = get_uid()\n\n    # res.partner.category\n    # check form pgadmin4\n    category_id = 7\n\n    # res.partner\n    # check form pgadmin4\n    res_partner_id = 38\n\n    # (4, id, _) links an already existing record.\n    # add many2many field,\n    models.execute_kw(db, uid, password, 'res.partner', 'write', [[res_partner_id], {\n        'category_id': [(4, category_id, 0)]\n    }])\n\n    record = models.execute_kw(db, uid, password,\n        'res.partner', 'read',\n        [res_partner_id], {'fields': ['id', 'name', 'category_id']})\n    print('record:', record)\n\ndef many2many_add_mutil_record():\n    models = endpoint_object()\n    uid = get_uid()\n\n    # res.partner.category\n    # check form pgadmin4\n    category_ids = [6, 7]\n\n    # res.partner\n    # check form pgadmin4\n    res_partner_id = 37\n\n    # (6, _, [ids]) replaces the list of linked records with the provided list.\n    # add mutil many2many field\n    models.execute_kw(db, uid, password, 'res.partner', 'write', [[res_partner_id], {\n        'category_id': [(6, 0, category_ids)]\n    }])\n\n    record = models.execute_kw(db, uid, password,\n        'res.partner', 'read',\n        [res_partner_id], {'fields': ['id', 'name', 'category_id']})\n    print('record:', record)\n\ndef many2many_update_record():\n    models = endpoint_object()\n    uid = get_uid()\n\n    # res.partner.category\n    # check form pgadmin4\n    category_id = 6\n\n    # res.partner\n    # check form pgadmin4\n    res_partner_id = 37\n\n    record = models.execute_kw(db, uid, password, 'res.partner', 'read',\n        [res_partner_id], {'fields': ['id', 'name', 'category_id']})\n    print('record:', record)\n\n    # update many2many field value\n    # (1, ID, { values }) update the linked record with id = ID\n    models.execute_kw(db, uid, password, 'res.partner', 'write', [[res_partner_id], {\n        'category_id': [(1, category_id, {'name':'hello2'})]\n    }])\n\n    record = models.execute_kw(db, uid, password,\n        'res.partner.category', 'read',\n        [category_id], {'fields': ['id', 'name']})\n    print('record:', record)\n\n\ndef many2many_delete_record_2():\n    models = endpoint_object()\n    uid = get_uid()\n\n    # res.partner.category\n    # check form pgadmin4\n    category_id = 6\n\n    # res.partner\n    # check form pgadmin4\n    res_partner_id = 37\n\n    record = models.execute_kw(db, uid, password,\n        'res.partner', 'read',\n        [res_partner_id], {'fields': ['id', 'name', 'category_id']})\n    print('record:', record)\n\n    # delete many2many field.\n    # 2, ID) remove and delete the linked record with id = ID\n    # (calls unlink on ID, that will delete the object completely,\n    # and the link to it as well)\n    models.execute_kw(db, uid, password, 'res.partner', 'write', [[res_partner_id], {\n        'category_id': [(2, category_id, 0)]\n    }])\n\n    record = models.execute_kw(db, uid, password,\n        'res.partner', 'read',\n        [res_partner_id], {'fields': ['id', 'name', 'category_id']})\n    print('record:', record)\n\n    # res.partner.category\n    # check form pgadmin4\n    # id = 6 deleted\n\n    record = models.execute_kw(db, uid, password,\n        'res.partner.category', 'read',\n        [category_id], {'fields': ['id', 'name']})\n    print('record:', record)\n\ndef many2many_delete_record_3():\n    models = endpoint_object()\n    uid = get_uid()\n\n    # res.partner.category\n    # check form pgadmin4\n    category_id = 7\n\n    # res.partner\n    # check form pgadmin4\n    res_partner_id = 37\n\n    record = models.execute_kw(db, uid, password,\n        'res.partner', 'read',\n        [res_partner_id], {'fields': ['id', 'name', 'category_id']})\n    print('record:', record)\n\n    # delete many2many field.\n    # (3, ID) cut the link to the linked record with id = ID\n    # (delete the relationship between the two objects\n    # but does not delete the target object itself)\n    models.execute_kw(db, uid, password, 'res.partner', 'write', [[res_partner_id], {\n        'category_id': [(3, category_id, 0)]\n    }])\n\n    record = models.execute_kw(db, uid, password,\n        'res.partner', 'read',\n        [res_partner_id], {'fields': ['id', 'name', 'category_id']})\n    print('record:', record)\n\n    # res.partner.category\n    # check form pgadmin4\n    # id = 7 not deleted\n\n    record = models.execute_kw(db, uid, password,\n        'res.partner.category', 'read',\n        [category_id], {'fields': ['id', 'name']})\n    print('record:', record)\n\n\ndef many2many_delete_record_5():\n    models = endpoint_object()\n    uid = get_uid()\n\n    # res.partner\n    # check form pgadmin4\n    res_partner_id = 38\n\n    record = models.execute_kw(db, uid, password,\n        'res.partner', 'read',\n        [res_partner_id], {'fields': ['id', 'name', 'category_id']})\n    print('record:', record)\n\n    # delete many2many field.\n    # (5, 0, 0) unlink all\n    # (like using (3,ID, 0) for all linked records)\n    models.execute_kw(db, uid, password, 'res.partner', 'write', [[res_partner_id], {\n        'category_id': [(5, 0, 0)]\n    }])\n\n    # res.partner.category\n    # check form pgadmin4\n    # ids not deleted\n    record = models.execute_kw(db, uid, password,\n        'res.partner', 'read',\n        [res_partner_id], {'fields': ['id', 'name', 'category_id']})\n    print('record:', record)\n\n\n\n\n# common_version()\n# get_uid()\ncall_check_access_rights()\n# list_all_records()\n# list_records()\n# count_records()\n# read_records()\n# read_all_field()\n# read_need_field()\n# listing_record_fields_attributes()\n# search_and_read()\n\n# create_reads()\n# update_records()\n# delete_record()\n# many2one_create()\n# many2many_add_record()\n# many2many_add_mutil_record()\n\n# many2many_update_record()\n# many2many_delete_record_2()\n# many2many_delete_record_3()\n# many2many_delete_record_5()\n"
  }
]