# odoo-tutorial **Repository Path**: twfb/odoo-tutorial ## Basic Information - **Project Name**: odoo-tutorial - **Description**: 这是你没有看过的全新教程 - **Primary Language**: Python - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-09-19 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 1. 激活开发者模式 - 点击设置(点下左上角的四个小方块, 就能看到) - 点击页面右侧的激活开发者模式 2. 成为超级用户 - 点击右上方开发者工具(小虫子按钮) - 点击成为超级用户 3. 创建应用 - ` python odoo-bin scaffold example addons` - `python odoo-bin scaffold 模型名称 放置它的位置` - 执行后会发现在odoo-12.0/addons里面有个新建的文件夹example, 里面会包含`__init__.py __manifest__.py controllers demo models security views`这几个文件夹 - 应用目录 - controllers - 控制器 (HTTP路径) - data - 演示和数据XML - doc - 模型说明 - models - 定义模型 - report - 报告 - security - 权限管理 - i18n - 翻译 - views - 视图和模型 - static - CSS - JS - IMG - LIB - ... - tests - 存放 python 和 yml 测试用例 - wizard - 放临时的 model 和视图 __manifest__.py
```python # -*- coding: utf-8 -*- { # 模型名 'name': "example", # 摘要 'summary': """ Short (1 phrase/line) summary of the module's purpose, used as subtitle on modules listing or apps.openerp.com""", # 介绍 'description': """ Long description of module's purpose """, # 作者 'author': "My Company", # 网址 # 'website': "http://www.yourcompany.com", # Categories can be used to filter modules in modules listing # Check https://github.com/odoo/odoo/blob/12.0/odoo/addons/base/data/ir_module_category_data.xml # for the full list # 类别 'category': 'Uncategorized', # 版本号 'version': '0.1', # any module necessary for this one to work correctly # 依赖 'depends': ['base'], # always loaded # 数据文件 'data': [ # 'security/ir.model.access.csv', 'views/views.xml', 'views/templates.xml', ], # only loaded in demonstration mode # 演示文件 'demo': [ 'demo/demo.xml', ], } ```
4. 安装应用 1. 重启odoo服务 2. 点击`刷新本地模型列表` 3. 弹出窗口点击`更新`按钮 4. 删除搜索框内容,然后输入`example` 5. 点击安装 - 以后执行步骤4时, 点击三个小点点. 然后点击升级按钮 5. 新建模型 修改addons/example/models/models.py
```python # -*- coding: utf-8 -*- import datetime from odoo import models, fields, api class example(models.Model): _name = 'example.example' name = fields.Char() active = fields.Boolean(default=True, string='归档', required=True) # 系统保留变量, False时不在主页显示,需要筛选改为True才能显示 price = fields.Float() note = fields.Text() content = fields.Html(readonly=True) my_datetime = fields.Datetime(default=fields.Datetime.now) # my_date = fields.Date(default=fields.Date.today) my_date = fields.Date(default=lambda self: datetime.date.today() + datetime.timedelta(days=4)) select = fields.Selection([ ('1', 'em'), ('2', 'emm'), ('3', 'emmm'), ]) em1_mo = fields.Many2one(comodel_name='example.example1') em1_om = fields.One2many(comodel_name='example.example1', inverse_name='em_mo') em1_mm = fields.Many2many(comodel_name='example.example1', column1='name', column2='fuck_id') my_reference = fields.Reference(selection='_select_objects') @api.model def _select_objects(self): records = self.env['ir.model'].search([]) return [(record.model, record.name) for record in records] + [('', '')] class em1(models.Model): _name = 'example.example1' fuck_id = fields.Integer() em_mo = fields.Many2one('example.example') ```
知识内容
- 模型的字段 - 系统字段 - `_name` - 必需的 - 定义了Odoo系统中模型的名称 - `_description` - 为模型添加更为友好的描述,更新后在Settings下可发现相应的变化(需开启调试模式Database Structure>Models) - `_order` - 默认情况下Odoo使用内置的id进行排序,可以通过逗号分隔指定多个字段进行排序,desc表示降序,仅能使用数据库中存储的字段排序,外部计算后的字段不适用,_order有些类似SQL语句中的ORDER BY,但无法使用NULL FIRST之类的语句 - 字段共同属性 - string(unicode, 默认:字段名称) - UI中字段的标签(用户可见) - required(bool默认:False) - 如果True该字段不能为空, 则它必须具有默认值或在创建记录时始终给定值 - help(unicode默认:'') - 长格式, 在UI中为用户提供帮助工具提示 - index(bool默认:False) - 请求Odoo 在列上创建数据库索引 - readonly(bool默认:False) - 是否只读 - Model字段类型 - 字段共同属性 - string(unicode, 默认:字段名称) - UI中字段的标签(用户可见) - required(bool默认:False) - 如果True该字段不能为空, 则它必须具有默认值或在创建记录时始终给定值 - help(unicode默认:'') - 长格式, 在UI中为用户提供帮助工具提示 - index(bool默认:False) - 请求Odoo 在列上创建数据库索引 - readonly(bool默认:False) - 是否只读 - 常用字段属性 - default - 默认值 - Binary - Binary字段用于存储二进制文件,如图片或文档 - `a = fields.Binary()` - Float - Float用于存储数值,其精度可以通过数字长度和小数长度一对值来进行指定 ```python a = fields.Float( string='float', digits=(14, 4), # Optional precision (total, decimals) ) ``` - Boolean - Boolean字段用于存储True/False布尔值 - `a = fields.Boolean()` - Integer - Integer即为整型 - `a = fields.Integer()` - Char - Char用于字符串 - `a = fields.Char(string='aa', required=True)` - Date - Date字段用于存储日期,ORM中以字符串格式对其进行处理,但以日期形式存放在数据库中,该格式在odoo.fileds.DATE_FORMAT中定义 - `my_date = fields.Date(default=fields.Date.today)` - `my_date = fields.Date(default=lambda self: datetime.date.today() + datetime.timedelta(days=4))` - Datetime - Datetime用于存储日期时间,在数据库中以UTC无时区时间(naive)存放,ORM中以字符串和UTC时间表示,该格式在odoo.fields.DATETIME_FORMAT中定义 - `my_datetime = fields.Datetime(default=fields.Datetime.now)` - Text - Text用于多行字符串 - `notes = fields.Text()` - Monetary - 货币 - `a=fields.Monetary()` - Html - Used to store HTML, provides an HTML widget. - Html类似text字段,但一般用于存储富文本格式的HTML - strip_style=True:清除所有样式元素 - strip_class=True:清除类属性 - `description = fields.Html()` - Selection - Store Text in database but propose a selection widget. It induces no selection constraint in database. Selection must be set as a list of tuples or a callable that returns a list of tuples - Selection用于选择列表,由值和描述对组成,选择的值将存储在数据库中,可以为字符串或整型,描述默认可翻译,虽然整型的值看似简洁,但注意Odoo会把0解析为未设置(unset),因而当存储值为0时不会显示描述 - index: Tells Odoo to index for faster searches replaces the select kwarg ```python select = fields.Selection([ ('1', 'em'), ('2', 'emm'), ('3', 'emmm'), ], required=True, default='1') ``` - Many2one - Store a relation against a co-model - comodel_name指定绑定多对一的模型名, 即类名 - `em1_mo = fields.Many2one(comodel_name='example.example1')` - Many2many - Store a relation against many2many rows of co-model - Many2many会新建一个中间表, relation指定中间表的表名 - 例: ```python _name = 'emm.a' e = Many2many( comodel_name='emmm.b', # 关联的表 relation='emmm_a_b_rel', # 可选, 中间表名 column1='a_id', # 当前表的字段名 column2='b_id', # 关联的其他表的字段名 string='Tags') ``` - One2many - Store a relation against many rows of co-model - 新建one2many前需要先写many2one, 并且数据库的外键会建立在many2one上 - comodel_name: 指定绑定一对多的模型名, 即类名 - inverse_name: 指定绑定一对多的模型的Many2one字段的名 - domain - 过滤, 例:`domain=[('id','=',1)]` ```python class em(models.Model): _name = 'example.example' em1_om = fields.One2many(comodel_name='example.example1', inverse_name='em_mo') class em1(models.Model): _name = 'example.example1' em_mo = fields.Many2one('example.example') ``` - Reference - Store an arbitrary reference to a model and a row - 事先不能决定关联的目标模型时, 这种情况需要使用reference将目标模型的选择权留给用户 ```python my_reference = fields.Reference(selection='_select_objects') @api.model def _select_objects(self): records = self.env['ir.model'].search([]) return [(record.model, record.name) for record in records] + [('', '')] ``` - 保留字段 - Odoo在所有模型中都创建了以下几个字段,这些字段由系统管理,是系统保留的字段, 用户不应定义 - id(Id) - 模型中记录的唯一标识符 - create_date(Datetime) - 记录的创建日期 - create_uid(Many2one) - 创建记录的用户 - write_date(Datetime) - 记录的最后修改日期 - write_uid(Many2one) - 上次修改记录的用户 - Method and decorator - @api.returns - This decorator guaranties unity of returned value. It will return a RecordSet of specified model based on original returned value: ```python @api.returns('res.partner') def afun(self): ... return x # a RecordSet ``` - @api.one - This decorator loops automatically on Records of RecordSet for you. Self is redefined as current record: ```python @api.one def afun(self): self.name = 'toto' ``` - @api.multi - Self will be the current RecordSet without iteration. It is the default behavior: ```python @api.multi def afun(self): len(self) ``` - @api.model - This decorator will convert old API calls to decorated function to new API signature. It allows to be polite when migrating code. ```python @api.model def afun(self): pass ``` - @api.constrains - This decorator will ensure that decorated function will be called on create, write, unlink operation. If a constraint is met the function should raise a openerp.exceptions.Warning with appropriate message. - @api.depends - This decorator will trigger the call to the decorated function if any of the fields specified in the decorator is altered by ORM or changed in the form ```python @api.depends('name', 'an_other_field') def afun(self): pass ``` - @api.onchange - This decorator will trigger the call to the decorated function if any of the fields specified in the decorator is changed in the form ```python @api.onchange('fieldx') def do_stuff(self): if self.fieldx == x: self.fieldy = 'toto' ``` - @api.noguess - This decorator prevent new API decorators to alter the output of a method
6. 升级应用 1. 重启odoo服务 2. 在搜索框输入`example` 3. 点击升级 7. 修改数据文件 修改addons/example/views/views.xml文件
```xml example模型表头 example.example example act_window example.example tree,form example server code action = { "type": "ir.actions.act_window", "view_mode": "tree,form", "res_model": "example.example", } ```
重复步骤4 升级应用 知识内容
- 数据文件仅在安装或更新模块时才加载数据文件的内容 - 模块的数据通过带有``元素的数据文件, XML文件声明.每个``元素都创建或更新数据库记录 - 数据文件必须在要manifest文件中声明数据文件, 它们可以在`data`列表(始终加载)或`demo`列表中声明(仅在演示模式下加载). - 属性 - model - 记录的Odoo模块的名称 - id - 一个外部标识符, 用于被引用 - `` - name标识字段名称 - ``的innnerText是``的值 - menuitem - 必须先声明相应的Action, 因为数据文件按顺序执行, 在id创建菜单之前, Action必须存在于数据库中 - 在创建完菜单后必须成为超级用户才能正常显示新建的菜单 - menuitem 只有绑定action, 或子menuitem绑定了action才能显示出来 - 属性 - id: 定义唯一标记 - action: 绑定动作 - parent: 父菜单 - sequence: 优先级, 数字越小优先级越高, 显示越靠前,最小为0 - groups: 绑定权限 - name: 菜单名称 - view - **视图定义了模块记录的显示方式(Views define the way the records of a model are displayed)**.每种类型的视图代表一种可视化模式(记录列表, 其聚合图, ......).可以通过类型(例如合作伙伴列表)或特别是通过其ID 来一般性地请求视图.对于通用请求, 将使用具有正确类型和最低优先级的视图(因此每种类型的最低优先级视图是该类型的默认视图). - 对于view 中的``的id, 会被存到数据库中, 而且当你修改``的model, 然后再次执行, 则``的model仍为修改前的model, 不报错, 且正常使用 - 解决方法: - 修改``的id - 或将``删除, 重启服务,升级模块, 然后再写``
8. 自定义添加数据的表单 修改addons/example/models/models.py
```python # -*- coding: utf-8 -*- import datetime from odoo import models, fields, api class example(models.Model): _name = 'example.example' name = fields.Char() active = fields.Boolean(default=True, string='归档',required=True) # 系统保留变量, False时不在主页显示,需要筛选改为True才能显示 price = fields.Float() note = fields.Text() content = fields.Html(readonly=True) my_datetime = fields.Datetime(default=fields.Datetime.now) # my_date = fields.Date(default=fields.Date.today) my_date = fields.Date(default=lambda self: datetime.date.today() + datetime.timedelta(days=4)) select = fields.Selection([ ('1', 'em'), ('2', 'emm'), ('3', 'emmm'), ]) em1_mo = fields.Many2one(comodel_name='example.example1') em1_om = fields.One2many(comodel_name='example.example1', inverse_name='em_mo') em1_mm = fields.Many2many(comodel_name='example.example1', column1='name', column2='fuck_id') my_reference = fields.Reference(selection='_select_objects') @api.model def _select_objects(self): records = self.env['ir.model'].search([]) return [(record.model, record.name) for record in records] + [('', '')] # 添加状态值和, 按钮事件 state = fields.Selection( [('1', '状态1'), ('2', '状态2'), ('3', '状态3')], readonly=True, default='1' ) """ [(状态值, 状态条中显示的内容), ('1', '状态1'),...] """ def button1(self): return self.write({'state': '1'}) def button2(self): return self.write({'state': '2'}) def button3(self): return self.write({'state': '3'}) class em1(models.Model): _name = 'example.example1' fuck_id = fields.Integer() em_mo = fields.Many2one('example.example') ```
修改addons/example/views/views.xml文件
```xml 列表显示字段 example.example ir.actions.act_window 我会被显示出来 example.example example.example
```
重复步骤4: 升级应用 知识内容
- **`model="ir.ui.view"`的recoder 不需要绑定任何只要在xml文件中出现既能正常显示** - 视图被声明为ir.ui.view的Model的record.视图类型由arch字段的根元素声明 - 表单视图 - 属性 - create="0": 不可新建 - edit="0": 编辑 - delete="0": 删除 - field属性 - widget - statusbar - 头部状态条标签 - email - 电子邮件地址标签 - selection - 下拉选择标签 - mail_followers - 关注者标签 - mail_thread - 消息标签 - progressbar - 进度条,按百分比标签 - one2many_list - 一对多列表标签 - many2many_tags - 多对多显示标签 - url - 网站链接标签 - image - 图片标签 - many2many_kanban - 看版标签 - handler - 触发标签 - radio - 单选标签 - char_domain - 字符域标签 - monetary - 价格(和精度位数相关)标签 - float_time - 单精度时间标签 - html - html相关标签 - pad - pad显示相关标签 - date - 日期标签 - monetary - 金额标签 - text - 文本标签 - sparkline_bar - 燃尽标签 - checkbox - 复选框标签 - reference - 关联标签 - `required` - 必填 - `readonly` - 只读 - `invisible` - 不可见 - 根据条件变化 - `name='123'`时`invisible="1"` - `active=True`时`required="1"` - 当前字段不是'many2many', 'many2one'状态时`readonly="1"` ```xml attrs="{ 'invisible':[('name','=','123')], 'required':[('active','=', True)], 'readonly':[('ttype','not in', ['many2many', 'many2one'])] }" ``` - 过滤one2many, many2many, 后面的many - domain - `domain="[('id','=',1)]"` 表单视图还可以使用纯HTML来实现更灵活的布局
```xml
```
- 树视图 > 树视图(也称为列表视图)以表格形式显示记录.他的根元素是``.最简单的树形视图, 即只需列出要在表中显示的所有字段(每个字段作为列) - 属性 - create="0": 不可新建 - edit="0": 编辑 - delete="0": 删除 ```xml ``` - 搜索视图 > 搜索视图通过列表视图(以及其他聚合视图)自定义关联的搜索字段.他的根元素是``, 他包含的字段, 定义了哪些时用于搜索的字段 ```xml ```
```xml course.search openacademy.course ```
- 如果模块不存在搜索视图, 则Odoo会生成仅允许在该name字段上搜索的视图.
9. 权限管理 添加文件addons/em/security/security.xml
```xml example.example_category example.example_groups_a example.example_groups_b ```
修改addons/example/__manifest__.py
```python ... # always loaded # 数据文件 'data': [ 'security/security.xml', 'views/views.xml', 'views/templates.xml', ], ... ```
- 两种方法设置字段指定用户组可见 法一. 修改addons/example/models/models.py
```python ... # 设置example.example_groups_b组的用户可见 select = fields.Selection([ ('1', 'em'), ('2', 'emm'), ('3', 'emmm'), ], groups="example.example_groups_b") ... ```
法二. 修改addons/example/views/views.xml ```xml ... ... ``` 个人推荐法一
```xml example act_window example.example tree,form ```
- 重复步骤4: 升级应用 - 验证权限 1. 修改群组访问权限 1. 设置 2. 用户&公司 3. 群组 4. 点击 `example.example_category / example.example_groups_a` 1. 编辑 2. 访问权限 3. 添加明细行 - 名称随意 - 对象, 点击搜索更多后搜索并选择`example.example` - 读, 写, 创建, 删除权限都勾上 4. 添加明细行 - 名称随意 - 对象, 点击搜索更多后搜索并选择`example.example1` - 读, 写, 创建, 删除权限都勾上 5. 保存 2. 新建用户 1. 设置 2. 用户&公司 3. 用户 4. 创建 5. example.example_category 选择 example.example_groups_a, 其他随意 6. 保存 7. 点击中间的`动作`下拉框 - 更改密码 - 输入密码 - 更改密码 3. 登录刚才新建的用户 - 点击创建 - 是不是发现select没有了, 哈哈哈哈哈哈 10. 给name整个sequence - 新建文件夹`addons/example/data` - 新建文件`addons/example/data/example_sequence.xml` 修改addons/example/data/example_sequence.xml
```xml example.example example.example OUT%(year)s%(month)s%(day)s 4 ```
- 将文件路径添加到__manifest__.py中 ```python 'data': [ 'data/example_sequence.xml', ... ``` - 修改addons/example/models/models.py ```python ... name = fields.Char(default=lambda self: self.env['ir.sequence'].next_by_code(self._name)) ... ``` - 重复步骤4 - 此时会发现新建数据时. 会自动生成name 11. 继承mail模块 - 修改`__manifest__.py` ```python ... 'depends': ['base', 'mail'], ... ``` - 重启odoo后, 查看example应用信息的技术数据, 可以发现依赖多了个mail 修改addons/example/models/models.py
```python ... _inherit = ['mail.thread'] my_datetime = fields.Datetime(default=fields.Datetime.now, track_visibility='onchange') # my_date = fields.Date(default=fields.Date.today) my_date = fields.Date(default=lambda self: datetime.date.today() + datetime.timedelta(days=4), track_visibility='always' ) def button4(self): # 通过用户test的id, 实现点击关注用户test, # 获取id方法设置->用户&公司->test->相关的业务伙伴->test 查看当前URL id self.message_subscribe(partner_ids=[7]) def button5(self): # 点击添加备注 self.message_post(body='emmm') ... ```
修改addons/example/views/views.xml文件
```xml ...
...
- track_visibility > 记录到备注中 - track_visibility='onchange' :修改该字段时记录 - track_visibility='always': 编辑track_visibility为onchange或always对应的字段时记录 - track_visibility='always', odoo12.0 不可用时参考: https://www.cnblogs.com/edhg/p/11434625.html - 重复步骤4 - 此时会发现多了块区域显示备注啥的 12. 重载系统方法 修改addons/example/models/models.py
```python ... @api.onchange('active') def onchange_active(self): # 修改记录的active字段时, 设置note内容 self.update(dict(note='你敢改我active, 我就敢改我自己!')) def unlink(self): # 重写系统删除记录函数 for order in self: if len(order.name) > 10: raise UserError('这谁起的名字这么长, 俺不愿意删, 改短点!!!') return super().unlink() @api.model def create(self, vals): # 新建记录后, 点击保存后执行 # 很奇怪的地方就是float, int类型设定的默认值, 没有获取到 return super().create(vals) def write(self, vals): # 修改记录后, 点击保存后执行, vals包括被修改后的值 # 修改name后: vals = {'name': 'OUT201908sa300012asooss'} return super().write(vals) ... ```
- onchange: 修改指定字段时执行 - unlink: 删除数据(记录)时执行 - create: 创建数据(记录)时执行 - write: 修改数据(记录)`后`执行 - 重复步骤4 13. 计算字段 > 会自动计算的字段 修改addons/example/models/models.py
```python ... name_and_price = fields.Char( compute='_compute_price_add_state_value' ) @api.depends('price', 'name') def _compute_price_add_state_value(self): # 添加计算字段 for order in self: order.name_and_price = '{} {}'.format(order.name, order.price) @api.onchange('price', 'name') def change_price_add_state_value(self): # 修改时,自动更新计算字段 self.update(dict( name_and_price='{} {}'.format(self.name, self.price) )) ... ```
修改addons/example/views/views.xml文件
```xml ... 列表显示字段 example.example ... ... ```
重复步骤4 14. 关系字段 > 将当前字段绑定到某个字段, 值随之变化, 可选择存或不存到数据库 修改addons/example/models/models.py
```python ... # store: 是否把关联字段存到数据库 my_related1 = fields.Integer(related='em1_mo.fuck_id', string='关联到em1_mo.fuck_id', store=True) my_related2 = fields.Date(related='my_date', string='关联到my_date', store=True) ... ```
修改addons/example/views/views.xml文件
```python ... 列表显示字段 example.example ... ... ```
重复步骤4 15. 查看模型, 菜单等 - 点击设置 - 点击技术 16. 项目地址:https://github.com/dhgdhg/odoo-tutorial