排序
class M(models.Model):
_name = 'm'
_order = 'date_release desc, name'
重写内置方法
重写name_serach
@api.model
def _name_search(self, name='', args=None, operator='ilike',
limit=100, name_get_uid=None):
args = [] if args is None else args.copy()
if not(name == '' and operator == 'ilike'):
args += ['|', '|',
('name', operator, name),
('isbn', operator, name),
('author_ids.name', operator, name)
]
return super(LibraryBook, self)._name_search(
name=name, args=args, operator=operator,
limit=limit, name_get_uid=name_get_uid)
重写read_group
@api.model
def _get_average_cost(self):
grouped_result = self.read_group(
[('cost_price', "!=", False)], # Domain
['category_id', 'cost_price:avg'], # Fields to access
['category_id'] # group_by
)
return grouped_result
read_group()方法的内部使用SQL的group by及累加函数来获取数据。传递给read_group() 方法的最常用参数如下:
- domain:用于为分组过滤记录。
- fields:它传递你希望获取分组数据的字段名称。该参数的值可能如下:
- 字段名:你可以向fields参数传递字段名,但如果使用这一选项,还应将该字段名同时传递给groupby参数,否则会产生错误
- field_name:agg:你可以传递带有聚合函数的字段名。例如,在cost_price:avg中,avg是一个SQL聚合函数。PostgreSQL中的聚合函数请参见https://www.postgresql.org/docs/current/functions-aggregate.html。
- name:agg(field_name):它与前面一个相同,但使用这种语句,你可以给数据列一个别名,例如average_price:avg(cost_price)。
- groupby:这个参数接收一个字段描述列表。记录将根据这些字段分组。对于date和datetime列,你可以传递groupby_function来根据不同的时长应用日期分组,如 date_release:month。这会根据月来应用分组。
read_group()还支持一些可选参数,如下:
- offset:表示可以跳过可选记录数量
- limit:表示可选的返回记录最大数量
- orderby:如果传递了该选项,结果会根据给定字段进行排序
- lazy:它接收布尔值,并且默认值为True。如果传递了True,结果仅通过第一个groupby进行分组,剩余的groupby会被放到__context键中。若为False,所有的groupby在一次调用中完成。
xml数据标色和隐藏
<!-- views.xml -->
<field name="arch" type="xml">
<tree string="Todo" decoration-danger="is_expired">
<field name="name"/>
<field name="deadline"/>
<field name="is_done"/>
<field name="is_expired" invisible="True"/>
</tree>
</field>
所有颜色
给action下拉选项中添加按钮
<record id="model_sale_contract_action_server" model="ir.actions.server">
<field name="name">批量复制</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="model_sale_contract"/> <!--在哪里model中添加, .换成_ -->
<!--<field name="binding_model_id" ref="waste.model_sale_contract"/>-->
<field name="state">code</field>
<field name="code">
action = model.contract_copy(env.context.get("active_ids"))
</field>
</record>
search
compute字段没有store=True时, 数据库不保存字段,在其他地方domian的时候无效,这时候可以写个search
is_expired = fields.Boolean(compute='_compute_is_expired', search='_search_is_expired', string="是否过期")
def _search_is_expired(self, operator, value):
if operator != '=':
raise ValidationError('只接受“=”符号')
else:
if value == False:
return [('status', '!=', 'no_verified'), '|', ('record_year', '=', fields.date.today().year),
('validity_time', '>', fields.date.today())]
else:
return ['!', '&', ('status', '!=', 'no_verified'), '|', ('record_year', '=', fields.date.today().year),
('validity_time', '>', fields.date.today())]
# 以下domain好像直接取反就行,有待观察
# ['|',('status', '=', 'no_verified'), '&', ('record_year', '!=', fields.date.today().year),
# ('validity_time', '<=', fields.date.today())]
# 调用
(is_expired', '=', False)
sorted
records = self.sorted(key=lambda x: (-x.id, x.name))
序列
@api.model
def create(self, vals):
vals['yhf'] = self.env['ir.sequence'].next_by_code('模块的_name')
return super(模块的类名,self).create(vals)
<record id="xxx" model="ir.sequence">
<field name="name">name of this sequence</field>
<field name="code">zerone.book</field>
<field name="prefix">PPA%(year)s%(month)s%(day)s</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
name 序列规则得名称,可自定义code 序列规则得编码,要求最好唯一,使用模块.表名来命确保唯一
prefix 序号编码的前缀
- 年份:%(year)s
- 月份:%(month)s
- 日: %(day)s
padding 填充数据的位数
日期
短日期
fields.Date.today()
长日期
fields.Datetime.now()
时间与字符串转换
日期转字符串
fields.Date.to_string(date)
fields.Datetime.to_string(date)
字符串转日期
fields.Date.to_date(string)
fields.Datetime.to_date(string)
格式化日期
year = fields.Date.from_string(date).strftime('%Y')
时间运算
1、往后加8个小时
datetime.datetime.now() + timedelta(hours=8)
fields.datetime.today() + timedelta(hours=8)
• 1
• 2
2、往后加8天
datetime.datetime.now() + timedelta(days=8)
fields.date.today() + timedelta(days=8)
3、往后加8年
datetime.datetime.now() + relativedelta(years=8)
fields.date.today() + relativedelta(years=8)
4、往后追8月
datetime.datetime.now() + relativedelta(months=8)
fields.date.today() + relativedelta(months=8)
关于月时间的运算
1、取出当前月的第一天和最后一天
import calendar
import datetime
#current_time临时变量,取出当前时间
current_time = datetime.datetime.now()
#调用monthrange(年份,月份),返回一个元组,例如(2,30)
#第一个元素,表示此月第一天周几,周末到周六(0-6)
#第二个元素,表示此月一共有多少天
monthRange = calendar.monthrange(current_time.year, current_time.month)
#取出当前月的第一天
date_from = datetime.date(current_time.year,current_time.month,day=1)
#取出当前月的最后一天
date_to = datetime.date(current_time.year,current_time.month,day=monthRange[1])
打印文件保存
self.env["py3o.report"].create({"ir_actions_report_id": rec.template_id.report_id.id}).save_file(rec.id)
def save_file(self, res_ids):
model_instances = self.env[self.ir_actions_report_id.model].browse(res_ids)
reports_path = []
existing_reports_attachment = self.ir_actions_report_id._get_attachments(
res_ids
)
for model_instance in model_instances:
reports_path.append(
self._get_or_create_single_report(
model_instance, {}, existing_reports_attachment
)
)
result_path, filetype = self._merge_results(reports_path)
reports_path.append(result_path)
with open(result_path, "r+b") as fd:
res = fd.read()
self._cleanup_tempfiles(set(reports_path))
# base64.b64encode(res) 很重要!!!没有这个不是二进制文件
self.env['ir.attachment'].create({'name': f'{model_instances.name}.{filetype}', 'datas': base64.b64encode(res)}).id
修改context
# current context is {'key1': True}
r2 = records.with_context({}, key2=True)
# -> r2._context is {'key2': True}
r2 = records.with_context(key2=True)
# -> r2._context is {'key1': True, 'key2': True}
模型的继承
继承中用的最多的是经典继承和代理继承,原型继承则基本不会使用到
经典继承
声明方式:_inherit = 'res.partner'
在模型类通过_inherit属性进行定义时,它向所继承模型添加了修改,而没有进行替换。意味着在继承类中中定义的字段会在父级模型中新增或修改。在数据库层,ORM对同一张数据表添加字段。ℹ️如果该字段在父类中已存在,仅修改在继承类中声明的属性,其它的保持原有父类中的内容不变。
在继承类中定义的方法替换父类中的方法。如果你不通过super调用触发父级方法,那么父级版本的方法则不会被调用,我们也就不拥有该项功能。因此,当你通过继承在已有方法中添加新逻辑时,应包含一个带有super的语句来调用其父类中的方法。
ℹ️ 还应说明带有inherit的传统继承会将功能拷贝到新模型中,虽然效率并不高。
代理继承
代理继承的一般声明方式
声明方式:
_name = 'library.member'
_inherits = {'res.partner': 'partner_id'}
ℹ️注意: 是_inherits属性(注意多一个 s),它的值是一个键值对字典,键是被继承的模型,而值是在模型中定义的Many2one字段名。
但是有一些情况下,我们不想修改已有模型,而是基于已有模型新建一个模型来使用其已有的功能。这借由Odoo的代理继承实现。
新模型创建新记录时,在原模型中也会被创建并使用many-to-one 字段关联。它仅是一个 Many2one关联,但代理机制注入了一些魔力来让他起来就好像属于新模型一样。看新模型可以看到所有原模型和新模型中的字段,但在后台两个模型分别处理各自的数据。
传统继承与面向对象编程的概念有很大不同。代理继承则与其相似,其中可创建一个新的模型来包含父级模型中的功能。它还支持多态继承,同时从两个或多个其它的模型中进行继承。
💎 何时使用代理继承?举例:向Partner模型添加的一些字段,如果这些字段其他Partner无需使用到。则使用代理继承比较好。
ℹ️需要注意代理继承仅用于字段,而不能用于方法。因此,如果原模型有一个do_something()方法,成员模型不会自动继承它。
💎 关于代理继承一个值得注意的用例是用户模型 res.users。它继承自成员(res.partner)。这表示其中在User中可见的一些字段实际存储Partner模型中(尤其是name字段)。在新用户创建时,我们还获取了一个新的自动创建的Partner。
代理继承的精简写法
使用在Many2one字段定义中使用delegate=True属性。这和_inherits完全一样。其主要优点是更为简洁。
举例:partner_id = fields.Many2one('res.partner', ondelete='cascade', delegate=True)
类似代理继承的可选方案
💎 代理继承可通过如下组合来进行替代:
父模型中的一个 many-to-one 字段
重载 create()方法自动创建并设置父级记录
父字段中希望暴露的特定字段的关联字段,有时这比完整的代理继承更为合适。例如res.company并没有继承res.partner,但使用到了其中好几个字段。
ℹ️ 但是,如果write也需要重载,最好还是用原来的代理继承。
原型继承(几乎不用)
通过添加一个在带有不同标识符的_name类属性来实现。以下是一个示例:
_inherit = 'res.partner'
_name = 'library.member'
新模型有其自己的数据表,包含完全独立于res.partner父模型的自身数据。因其仍继承Partner模型,此后的任意修改也会影响到新模型。原型继承在实践中鲜有使用,原因在于代理继承通常可以更高效的方式满足了这一需求,也无需复制数据结构。
实现init钩子
我们了解了如何通过XML或CSV文件添加、更新及删除记录。但有时,业务用例非常复杂,无法通过使用数据文件来进行解决。这些情况下,可以在声明文件中使用init钩子来执行所需要的操作。
按照如下步骤来添加post_init_hook:
- 在__manifest__.py文件中通过post_init_hook键来注册这个钩子:
'post_init_hook': 'add_book_hook'
- 在__init__.py文件中添加add_book_hook()方法:
def add_book_hook(cr, registry):
env = api.Environment(cr, SUPERUSER_ID, {})
book_data1 = {'name': 'Book 1', 'date_release': fields.Date.today()}
book_data2 = {'name': 'Book 2', 'date_release': fields.Date.today()}
env['library.book'].create([book_data1, book_data2])
在第一步中,我们在声明文件文件中通过add_book_hook值注册了post_init_hook。这表示在模块安装之后,Odoo会在__init__.py中查找add_book_hook方法。如果找到,它会使用数据库游标和 registry调用该方法。
第2步中,我们声明了add_book_hook()方法,在模块安装后会被调用。我们通过该方法创建了两条记录。在实际情况中,可以在本息编写复杂的业务逻辑。
Odoo还支持另外两种钩子:
- pre_init_hook:这个钩子会在开始安装模块时触发。它与post_init_hook正好相反,会在当前模块安装前触发。
- uninstall_hook:这个钩子会在你卸载该模块时触发。它多用于模块需要垃圾回收机制时。
读写conf文件
import configparser
cf = configparser.RawConfigParser()
cf.read(r"D:\sah_pro\GOdoo13_SAN\bin\odoo.conf")
# 写配置文件,‘options’标签下,name=kong
cf.set('options', 'name', 'kong')
cf.write(open(r"D:\sah_pro\GOdoo13_SAN\bin\odoo.conf", "r+", encoding="utf-8")) # r+模式
cf.get('options', 'name')
‘r’:只读。该文件必须已存在。
‘r+’:可读可写。该文件必须已存在,写为追加在文件内容末尾。
‘rb’:表示以二进制方式读取文件。该文件必须已存在。
‘w’:只写。打开即默认创建一个新文件,如果文件已存在,则覆盖写(即文件内原始数据会被新写入的数据清空覆盖)。
‘w+’:写读。打开创建新文件并写入数据,如果文件已存在,则覆盖写。
‘wb’:表示以二进制写方式打开,只能写文件, 如果文件不存在,创建该文件;如果文件已存在,则覆盖写。
‘a’:追加写。若打开的是已有文件则直接对已有文件操作,若打开文件不存在则创建新文件,只能执行写(追加在后面),不能读。
‘a+’:追加读写。打开文件方式与写入方式和'a'一样,但是可以读。需注意的是你若刚用‘a+’打开一个文件,一般不能直接读取,因为此时光标已经是文件末尾,除非你把光标移动到初始位置或任意非末尾的位置。(可使用seek() 方法解决这个问题)
action
分组
<field name="context" eval="{'group_by': 'create_uid'}"/>
过滤
<field name="domain">[('suapply_status', '=', '2')]</field>
context
<field name="context">{'is_contract': True}</field>
相同model不同action区分
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('sample_reserve_manage_view_tree')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('sample_reserve_manage_view_form')}),
]"/>
附件预览
http://localhost:8069/web/content/附件ID
获取selection字段的value
type= fields.Selection([('a', '吃'), ('b', '喝')], string='类型')
• 1
我们都知道self.type输出的是“a”或者“b”,但是在很多时候我们要获取到“吃”或者“喝”,比如在Report里面输出type的值,此时该怎么办呢?
type= dict(self.fields_get(allfields=['type'])['type']['selection'])[self.type]
form中添加记录追踪
效果
实现方法:tracking=True 或 tracking=1
_inherit = ['mail.thread']
is_free_freight = fields.Boolean(string='是否免运费', tracking=True)
<div class="oe_chatter">
<field name="message_ids" widget="mail_thread" options="{'post_refresh': 'recipients'}"/>
</div>
model中的方法
toggle_active
反转active
action_archive
active=False
action_unarchive
active=True
批量获取字典的数据
data = {
'model': "aaa",
'fields': "bbb",
"ids": "ccc",
"domain": "ddd"
}
model, fields, ids, domain = operator.itemgetter('model', 'fields', 'ids', 'domain')(data)
print(model, fields, ids, domain)
结果:
o2m中写整个视图(有tree和form)
默认情况下,只会有自定义的tree, form是表中所有字段
解决方式
1,添加form_view_ref, 引用自定义的form(同理有tree_view_ref)
<field name="comment_ids" context="{'tree_view_ref': 'your_app.tree_view_xml_id', 'form_view_ref': 'your_app.form_view_xml_id'}"/>
2,嵌套视图
<field name="comment_ids">
<tree>
<field name = 'comment_id'/>
<field name = 'comment'/>
</tree>
<form>
<div class="form-group">
<label name="comment">Comment:</label>
<textarea class="form-control" rows="5" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</field>
xml中context添加默认值
数据量大查询太慢
解决办法:
- 如果有active, 要在active上加上索引(index=True)
- 常用作查询的字段加上index=True,
原因:表中有active:search_read()的时候走web_search_read(), web_search_read中有search_count()方法调用active=True,导致查询过慢
注册env
db_registry = registry(db)
with api.Environment.manage(), db_registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
页面资源404
页面变为空白,以及刷新,重启等方式都无法解决,是由于静态css文件是存储在数据库之外的,数据库备份 静态文件和css文件不会一起备份,所以 当还原数据库后,页面出现空白是由于静态文件路径是指定的,但是本地又没有,找不到导致的
解决方式:
对还原下来的数据库执行一下操作
DELETE FROM ir_attachment WHERE url LIKE '/web/content/%';
重启后,静态文件会重新产生