odoo 笔记

22 篇文章 6 订阅

排序

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>

所有颜色

img

给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:

  1. 在__manifest__.py文件中通过post_init_hook键来注册这个钩子:

‘post_init_hook’: ‘add_book_hook’

  1. 在__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]

img

form中添加记录追踪

效果

img

实现方法: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)

结果:

img

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/%’;

重启后,静态文件会重新产生

自定义筛选去掉部分字段

img

img

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值