Odoo代码规范

作为全球第一开源ERP,开源让您根据本企业业务特性,灵活定制,实现动态IT系统,可随业务变化优化ERP,进而优化管理运营。

对中大型企业,odoo必须开发才可落地,良好开发规范,有助于团队协助,敏捷开发上线。

odoo开发规范

本文介绍 Odoo 编码指南。其目的是提高 Odoo 应用程序代码的质量。事实上,正确的代码提高了可读性、简化维护、有助于调试、降低复杂性并提高可靠性。这些准则应该应用于每个新模块的开发和所有新开发。

警告

在stable(稳定)版本中修改现有文件时,原始文件样式将严格取代任何其他样式准则。换句话说,请不要修改odoo正式发布的已有文件或代码,以应用这些准则。它避免了中断代码行的修订历史记录。差异应保持在最小。有关详细信息,请参阅odoo官方的 pull request guide指南。

警告

修改master(主开发)版本中的现有文件时,仅可针对 revision(正在修订时)版本中的代码或大多数文件,将这些准则应用于现有代码。换句话说,仅当现有文件结构发生重大更改时,才修改它。在这种情况下,首先执行 move commit,然后才应用与其相关的更改。

模块结构说明

  • data/:演示数据
  • models/:模型
  • controllers/:控制器(含HTTP路由)
  • views/:视图和模板
  • security/:权限文件
  • static/:web资源,css/, js/, img/, lib/, ...
  • wizard/:向导及视图
  • report/:报表
  • tests/:单元测试
  • __manifest__.py:清单文件

命名规范

业务model放一个文件,如模块只含一个model,与模块名一致。如:

  • models/<main_model>.py
  • models/<inherited_main_model>.py
  • views/<main_model>_templates.xml
  • views/<main_model>_views.xml
  • data/<main_model>_demo.xml
  • data/<main_model>_data.xml

对于数据文件命名,按用途进行命名:demo或者data。例如:data/sale_order_demo.xmldata/sale_order_data.xml
每个模块的控制器都放在一个文件中,命名为main.py。如果是从另一个模块继承的,则将其命名为<module_name>.py

与模型不同,每个控制器类应包含在一个分离的文件中。例如,采购模块在  controllers/portal.py  中实现其客户门户的一部分,并在 controllers/purchase.py上实现其自身的典型路由。

对于静态文件,由于资源可以在不同的上下文中使用(前端、后端),因此它们将仅包含在一个捆绑包中。因此,CSS/Less、JavaScript 和 XML 文件应该捆绑一个其类型的后缀名称。

即:im_chat_common.css,用于"asset_common"捆绑的 im_chat_common.js,以及用于"asset_backend"捆绑的 im_chat_backend.js。如果模块只拥有一个文件,则约定将<module_name>.ext(如:project.js)。不要链接odoo外部资源数据(图像、库):不要使用图像的 URL,而应将其复制到我们的代码库中。

{
    'name': 'My Module',
    'version': '1.0',
    'depends': ['base'],
    'assets': {
        'web.assets_backend': [
            'my_backend.js',
            'my_backend.css',
        ],
        'web.assets_frontend': [
            'my_module_frontend.js',
            'my_module_frontend.css',
        ],
    },
    # ... 其他字段 ...
}

关于数据,按用途拆分它们:数据或演示。文件名将是主模型名称,后缀为 _data.xml 或 _demo.xml。

关于向导和瞬态模型,命名约定是 :

  • <main_transient>.py
  • <main_transient>_views.xml

其中<main_transient>是占主导地位的瞬态模型的名称,就像模型一样。<main_transient>.py 可以包含模型"transient_model.action"和"transient_model.action.line"。

统计报表命名:

  • <report_name_A>_report.py
  • <report_name_A>_report_views.py (一般提供 pivot 和 graph 视图)

可打印报表:

  • <print_report_name>_reports.py (报表  actions, 格式定义, …)
  • <print_report_name>_templates.xml (xml 报表模板)
addons/<module_name>/
|-- __init__.py
|-- __manifest__.py
|-- controllers/
|   |-- __init__.py
|   |-- <inherited_module_name>.py
|   `-- main.py
|-- data/
|   |-- <main_model>_data.xml
|   `-- <inherited_main_model>_demo.xml
|-- models/
|   |-- __init__.py
|   |-- <main_model>.py
|   `-- <inherited_main_model>.py
|-- report/
|   |-- __init__.py
|   |-- <main_stat_report_model>.py
|   |-- <main_stat_report_model>_views.xml
|   |-- <main_print_report>_reports.xml
|   `-- <main_print_report>_templates.xml
|-- security/
|   |-- ir.model.access.csv
|   `-- <main_model>_security.xml
|-- static/
|   |-- img/
|   |   |-- my_little_kitten.png
|   |   `-- troll.jpg
|   |-- lib/
|   |   `-- external_lib/
|   `-- src/
|       |-- js/
|       |   `-- <my_module_name>.js
|       |-- css/
|       |   `-- <my_module_name>.css
|       |-- scss/
|       |   `-- <my_module_name>.scss
|       `-- xml/
|           `-- <my_module_name>.xml
|-- views/
|   |-- <main_model>_templates.xml
|   |-- <main_model>_views.xml
|   |-- <inherited_main_model>_templates.xml
|   `-- <inherited_main_model>_views.xml
`-- wizard/
    |-- <main_transient_A>.py
    |-- <main_transient_A>_views.xml
    |-- <main_transient_B>.py
    `-- <main_transient_B>_views.xml

XML文件编写

定义记录xml时,需<record>标记:

  • id属性放在model属性前
  • 字段(field)定义中,name属性放在第一个。然后,将值放在<field>标签内,或者放在eval属性中。最后将其他属性(widget、options...)按重要性排序。
  • 尝试按模型对记录进行分组。如果操作/菜单/视图之间存在依赖关系,则此约定可能不适用。
  • 标签<data>仅用于设置不可更新的数据noupdate=1,如果整个xml文件都是不可更新数据,则noupdate=1属性可以设置在<odoo>标签上,而不需要<data>标签。
  • priority 优先级 可通过计算调整不同场景下的视图显示。
<record id="view_id" model="ir.ui.view">
    <field name="name">view.name</field>
    <field name="model">object_name</field>
    <field name="priority" eval="16"/>
    <field name="arch" type="xml">
        <tree>
            <field name="my_field_1"/>
            <field name="my_field_2" string="My Label" widget="statusbar" statusbar_visible="draft,sent,progress,done" />
        </tree>
    </field>
</record>

Odoo定义标签为快捷方式:

  • menuitem:为ir.ui.menu快捷方式(即菜单项)
  • template: 表示只需arch视图部分的QWeb视图
  • report: 定义报表action
  • act_window:当record用不了时用它

xml_id命名

权限(Security)、视图(View)和动作(Action)使用的命名规则:

  • 菜单(menu): <model_name>_menu, 或 子菜单 <model_name>_menu_do_stuff .
  • 视图(view): <model_name>_view_<view_type>,view_type可能的取值有:kanbanformtreesearch
  • 动作(action): 主动作命名为<model_name>_action,其他的动作命名为<model_name>_action_<detail>,其中<detail>使用小写字母简单描述动作,适用于一个模型有多个动作
  • 权限组(group): <model_name>_group_<group_name>,group_name为组名,可能的取值包括:‘user’, ‘manager’, …
  • 记录规则(rule): <model_name>_rule_<concerned_group>,concerned_group 是相关权限组的缩写,如 (‘user’ for the ‘model_name_group_user’, ‘public’ for public user 公共用户, ‘company’ for multi-company rules 多公司规则, …).
  • 所有的菜单项统一放在menu.xml文件中
  • 每个菜单项都需要配置权限组
  • 菜单项要设置sequence,sequence的间隔预留大一些,如10、20、30...
  • 每个模型都要配置search视图
  • 动作窗口要指定视图
<!-- views and menus -->
<record id="model_name_view_form" model="ir.ui.view">
    ...
</record>

<record id="model_name_view_kanban" model="ir.ui.view">
    ...
</record>

<menuitem
    id="model_name_menu_root"
    name="Main Menu"
    sequence="5"
/>
<menuitem
    id="model_name_menu_action"
    name="Sub Menu 1"
    parent="module_name.module_name_menu_root"
    action="model_name_action"
    sequence="10"
/>

<!-- actions -->
<record id="model_name_action" model="ir.actions.act_window">
    ...
</record>

<record id="model_name_action_child_list" model="ir.actions.act_window">
    ...
</record>

<!-- security -->
<record id="module_name_group_user" model="res.groups">
    ...
</record>

<record id="model_name_rule_public" model="ir.rule">
    ...
</record>

<record id="model_name_rule_company" model="ir.rule">
    ...
</record>

注意

视图名称使用 . 连接关键语,my.model.view_type 或 my.model.view_type.inherit ,来标记ad of “This is the form view of My Model”.

继承XML的命名

继承视图的命名规则为

  • #. Extension 扩展模式:使用与要扩展的原始视图相同的 xml id,并使用 _inherit 后缀 :如 视图view project.project_view_form 被扩展 为 project_forecast.project_view_form_inherit.
  • #. Primary 主模式:保持原有 xml id。
<record id="module2.model_view_form_inherit" model="ir.ui.view">
    <field name="inherit_id" ref="module1.model_view_form"/>
    ...
</record>

<record id="module2.model_view_form" model="ir.ui.view">
    <field name="inherit_id" ref="module1.model_view_form"/>
    <field name="mode">primary</field>
    ...
</record>

权限文件编写

ir.model.access.csv文件:文件名固定

id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_hr_resource_manager,hr.employee.resource.manager,resource.model_resource_resource,hr.group_hr_manager,1,1,1,1
access_hr_resource_calendar_user,hr.employee.resource.calendar.user,resource.model_resource_calendar,hr.group_hr_user,1,1,1,1
access_hr_resource_calendar_attendance_user,hr.employee.resource.calendar.attendance.user,resource.model_resource_calendar_attendance,hr.group_hr_user,1,1,1,1
access_hr_contract_manager,hr.contract.manager,model_hr_contract,hr_contract.group_hr_contract_manager,1,1,1,1
  1. id:权限的ID  access_hr_contract_manager(access_+模型名+角色名)
  2. name:权限的name hr.contract.manager(模型+角色名)
  3. model_id:id;模型的ID model_hr_contract(model_+模型名)
  4. group_id:id;群组的ID hr_contract.group_hr_contract_manager(模型名+group+用户名)
  5. perm_read:查看权限
  6. perm_write:修改权限
  7. perm_create:增加数据权限
  8. perm_unlink:删除权限

清单文件编写

name: 模块名,以tx_开头

author:作者,固定为 广东同欣智能科技有限公司

website:网址,固定为 Home | 精益美业 (beauty-lean.com)

version:版本,固定为 当前odoo开发版本

summary:模块简述,尽可能简洁

description:模块描述,尽可能全面描述模块功能

maintainer:运维者,开发者名称

category:分类

Python

PEP8 选项

Odoo源代码基本准守Python标准PEP8,但是忽略其中一些规则:

  • E501:行太长了
  • E301:预计有1个空行,找到0
  • E302:预计有2个空行,找到1
  • E126:延长线过度缩进以用于悬挂缩进
  • E123:关闭支架与开口支架线的压痕不匹配
  • E127:延伸线过度缩进以进行视觉缩进
  • E128:用于视觉缩进的缩进的延续线
  • E265:屏蔽代码应以'#'开头

import 顺序

  • 外部库
  • 导入odoo
  • 导入odoo模块

在每组中的导入按字母顺序排序

# 按需导入,无用的删除
# 1 : 导入python库
from base64 import b64encode
import re
from time import Time
from datetime import datetime
# 2 :  imports of odoo
from odoo import api, fields, models # alphabetically ordered
from odoo.tools.safe_eval import safe_eval as eval
from odoo.tools.translate import _
# 3 :  imports from odoo modules
from odoo.addons.website.models.website import slug
from odoo.addons.web.controllers.main import login_redirect

编程习惯

  • 每个python文件都应该以 # -*- coding: utf-8 -*- 作为第一行。
  • 使用"""""给方法加注释及使用#给具体代码加注释
  • 简单易读的代码
  • 不要使用.clone()
# bad
new_dict = my_dict.clone()
new_list = old_list.clone()
# good
new_dict = dict(my_dict)
new_list = list(old_list)

Odoo中编程

  • 避免创建生成器和装饰器:仅使用Odoo API已有的
  • 使用filteredmappedsorted, … 方法来提升代码可读性和性能。
  • 使用更易理解的方法名
方法批量处理数据

当添加一个函数时,确保它可以处理多条数据,典型用法是通过 api.model 装饰器,可以在self上进行循环处理

@api.model
def my_method(self)
    for record in self:
        record.do_cool_stuff()

为了更好的性能,比如当定义一个状态按钮时,不在api.model 循环里用search和search_count方法,而用read_group一次计算

def _compute_equipment_count(self):
""" Count the number of equipement per category """
    equipment_data = self.env['hr.equipment'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id'])
    mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in equipment_data])
    for category in self:
        category.equipment_count = mapped_data.get(category.id, 0)

Odoo 11是最后一个使用`api.multi()`装饰器的版本。从Odoo 12开始,`api.multi()`被弃用,并推荐使用`api.model`装饰器来代替。
`api.model`装饰器用于定义模型方法,这些方法可以在记录集上调用,而不是单个记录。在Odoo 12及以后的版本中,`api.model`成为处理多记录操作的标准方式。
在Odoo 12之前,`api.one()`和`api.multi()`被用来区分单记录和多记录操作。然而,从Odoo 12开始,`api.one()`也被弃用,并且`api.model`成为了处理模型方法的唯一方式,无论它们是单记录还是多记录操作。现在,方法是否作为单记录或多记录操作取决于它是如何被调用的,而不是它的装饰器。
因此,为了保持代码的现代性和兼容性,建议在Odoo 12及更高版本中使用`api.model`装饰器,并避免使用`api.multi()`和`api.one()`。

上下文环境

新API中,context变量不能修改。通过with_context用新运行环境调用方法。

records.with_context(new_context).do_stuff() # all the context is replaced
records.with_context(**additionnal_context).do_other_stuff() # additionnal_context values override native context ones
尽量用ORM

当ORM可以实现的时候尽量使用ORM而不要直接写sql,因为它可能会绕过orm的一些规则如权限、事务等,还会让代码变得难读且不安全。

# 错误的写法,注入风险,代码效率低
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in (' + ','.join(map(str, ids))+') AND state=%s AND obj_price > 0', ('draft',))
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]

# 不会被注入,但仍然是错误的写法
self.env.cr.execute('SELECT id FROM auction_lots WHERE auction_id in %s '\
           'AND state=%s AND obj_price > 0', (tuple(ids), 'draft',))
auction_lots_ids = [x[0] for x in self.env.cr.fetchall()]

# 推荐的写法
auction_lots_ids = self.search([('auction_id','in',ids), ('state','=','draft'), ('obj_price','>',0)])
防止注入

不要用python的+号连接符、%解释符来拼sql

# 错误的写法
self.env.cr.execute('SELECT distinct child_id FROM account_account_consol_rel ' +
           'WHERE parent_id IN ('+','.join(map(str, ids))+')')

# 推荐的写法
self.env.cr.execute('SELECT DISTINCT child_id '\
           'FROM account_account_consol_rel '\
           'WHERE parent_id IN %s',
           (tuple(ids),))
扩展的思考

函数和方法不应包含太多的逻辑:使用大量小而简单的方法比使用少量大型和复杂的方法更可取。一条好的经验法则是,只要方法具有多个责任,就将其拆分(参考 http://en.wikipedia.org/wiki/Single_responsibility_principle)

应避免在方法中硬编码业务逻辑,因为它可以防止子模块轻松扩展。

# do not do this
# modifying the domain or criteria implies overriding whole method
def action(self):
    ...  # long method
    partners = self.env['res.partner'].search(complex_domain)
    emails = partners.filtered(lambda r: arbitrary_criteria).mapped('email')

# better but do not do this either
# modifying the logic forces to duplicate some parts of the code
def action(self):
    ...
    partners = self._get_partners()
    emails = partners._get_emails()

# better
# minimum override
def action(self):
    ...
    partners = self.env['res.partner'].search(self._get_partner_domain())
    emails = partners.filtered(lambda r: r._filter_partners()).mapped('email')

为了示例,上述代码是可扩展的,但必须考虑可读性,并且必须做出权衡。

此外,相应地命名函数:小且正确命名的函数是可读/可维护代码和更严格的文档的起点。

此建议还与类、文件、模块和包相关。

不要手动提交事务

Odoo 框架负责为所有 RPC 调用提供事务上下文。原则是,在每个 RPC 调用开始时,打开一个新的数据库游标,并在调用return返回时commit,即在commit前先将结果返回至 RPC 客户端,大致如下所示:

def execute(self, db_name, uid, obj, method, *args, **kw):
    db, pool = pooler.get_db_and_pool(db_name)
    # create transaction cursor
    cr = db.cursor()
    try:
        res = pool.execute_cr(cr, uid, obj, method, *args, **kw)
        cr.commit() # all good, we commit
    except Exception:
        cr.rollback() # error, rollback everything atomically
        raise
    finally:
        cr.close() # always close cursor opened manually
    return res

如果在执行 RPC 调用期间发生任何错误,事务将以原方式回滚,从而保留系统的状态。

同样,系统在执行测试代码期间还提供专用事务,因此可以回滚或不依赖于服务器启动选项。

因此,如果您在任何地方代码调用 cr.commit()  ,您很有可能会以各种方式破坏系统,因为您将导致部分提交,从而导致部分和不干净的回滚,从而导致问题:

  1. 业务数据不一致,通常是数据丢失
  2. 工作流取消同步,文档永久卡住
  3. 无法干净地回滚的测试,并且将开始污染数据库并触发错误(即使事务期间未发生错误也是如此)

所以请遵照以下非常简单的规则:

绝对不要自己调用 cr.commit()除非 您已显式创建自己的数据库游标!或者你有特殊的原因必须这样做。

如果您确实创建了自己的游标,则需要处理错误情况和适当的回滚,并在完成该游标后正确关闭游标。

与普遍的看法相反,在以下情况下,您甚至不需要调用cr.commit() :

  • 在模型的 _auto_init() 方法中。模型对象:这是由加载项初始化方法,或由 ORM 事务在创建自定义模型时处理
  •  在报表中: commit()  也由框架处理,因此您甚至可以在报表内更新数据库
  •  在模型中。瞬态方法:这些方法与常规模型完全一样。模型,在事务中,并在末尾使用相应的 cr.commit()/rollback()
  • 其它情况(如果您有疑问,请参阅上面的一般规则!

所以,服务器框架之外的所有 cr.commit()  调用都必须有一个明确的注释,解释它们为什么是绝对必要的,为什么它们确实是正确的,以及为什么它们不中断事务。否则,他们应该被删除!

正确的使用翻译方法

odoo用一个下划线方法来表明某字段需要翻译,该方法通过from odoo.tools.translate import _导入。一般情况下该方法只能被用在代码里的规定字符串的翻译,不能用于动态字符串的翻译,翻译方法的调用只能是_('literal string'),里面不能加其他的。

# 好的方式,简洁
error = _('This record is locked!')

# 好的方式,包含格式的字符串
error = _('Record %s cannot be modified!') % record

# 好的方式,多行文字的字符串
error = _("""This is a bad multiline example
             about record %s!""") % record
error = _('Record %s cannot be modified' \
          'after being validated!') % record

# 错误的方式:试图在字符串格式化后进行翻译
# 这样没有作用,而只会把翻译搞乱
error = _('Record %s cannot be modified!' % record)

# 错误:动态字符串,不能这样翻译
error = _("'" + que_rec['question'] + "' \n")

# 错误:字段值将会被系统字段翻译,而不会获取预期结果
error = _("Product %s is out of stock!") % _(product.name)
# 错误的方式:试图在字符串格式化后进行翻译
error = _("Product %s is out of stock!" % product.name)
符号和习惯
  • 模型名-使用.分隔,模块名做前缀
    • 定义odoo模型时,使用单数形式的名字如res.user,res.partner
    • 定义wizard时,命名格式为<related_base_model>.<action>related_base_model是关联模型名称,action是功能简称,如account.invoice.make
    • 定义报表模型时,使用<related_base_model>.report.<action>,和wizard一样
  • python类-使用驼峰命名方式
class AccountInvoice(models.Model):
    ...

class account_invoice(osv.osv):
    ...

变量名

  • 模型变量使用驼峰命名方式
  • 普通变量用下划线+小写字母
  • 如果变量包含记录 id 或 id 列表则将变量名称后缀为 _id 或 _ids。不要使用 partner_id 来包含一条 res.partner 的记录(_id 只记录id这个数字,而非对象)
ResPartner = self.env['res.partner']
partners = ResPartner.browse(ids)
partner_id = partners[0].id

One2Many, Many2Many字段一般以ids作为后缀如:sale_order_line_ids
Many2One 一般以_id为后缀如:partner_iduser_id

  • 方法命名

    • 计算字段 - 计算方法一般是_compute_<field_name>
    • 搜索方法 - _search_<field_name>
    • 默认方法 - _default_<field_name>
    • onchange方法 - _onchange_<field_name>
    • 约束方法 - _check_<constraint_name>
    • action方法 - 一个对象的动作方法一般以action_开头,它的装饰器是@api.model
  • 模型中属性顺序

    • 私有属性:_name, _description, _inherit, ...
    • 默认方法和_default_get
    • 字段声明
    • 计算和搜索方法和字段声明顺序一致
    • 约束方法(@api.constrains)和onchange方法(@api.onchange)
    • CRUD方法
    • action方法
    • 其他业务方法
class Event(models.Model):
    # 私有属性
    _name = 'event.event'
    _description = 'Event'

    # 默认方法
    def _default_name(self):
        ...

    # 字段声明
    name = fields.Char(string='Name', default=_default_name)
    seats_reserved = fields.Integer(oldname='register_current', string='Reserved Seats',
        store=True, readonly=True, compute='_compute_seats')
    seats_available = fields.Integer(oldname='register_avail', string='Available Seats',
        store=True, readonly=True, compute='_compute_seats')
    price = fields.Integer(string='Price')

    # 计算和搜索方法,与字段申明顺序一致
    @api.depends('seats_max', 'registration_ids.state', 'registration_ids.nb_register')
    def _compute_seats(self):
        ...

    # 约束方法和onchange方法
    @api.constrains('seats_max', 'seats_available')
    def _check_seats_limit(self):
        ...

    @api.onchange('date_begin')
    def _onchange_date_begin(self):
        ...

    # CRUD方法    @api.model
    def create(self, values):
        ...

    # action方法
    def action_validate(self):
        self.ensure_one()
        ...

    # 其他业务方法
    def mail_user_confirm(self):
        ...
接口编写
@http.route('/tx_map/map_base_info/test', methods=['GET'], type='http', auth='public')
    def test(self):
        origin_url = request.httprequest.base_url
        base_url = origin_url[:origin_url.index(urlparse(origin_url).path)]
        return request.make_response(json.dumps({'url': base_url}), headers={'Content-Type': 'application/json'})
  • 路由定义:/模块名/模型名/具体方法

  • methods:指定请求类型,如GET、POST、PUT、DELETE...

  • type:路由类型

  • auth:认证方式,user为已登录的普通用户,public为公开无需登录,admin为管理员用户

  • 方法名与具体方法相同

Javascript和CSS

请按如下约定结构组织你的代码:

  • static: 所有一般静态资源文件
  • static/lib: 库文件,如: jquery  库可放在 addons/web/static/lib/jquery
  • static/src: 源文件
  • static/src/css
  • static/src/fonts
  • static/src/img
  • static/src/js
  • static/src/scss
  • static/src/xml: 所有qweb templates 文件
  • static/tests: 所有测试相关文件

Javascript 编码规范

  • 在所有javascript文件中使用use strict;
  • 不添加压缩javascript库
  • 类名使用驼峰命名
/** @odoo-module **/
import {someFunction} from './file_b';

export function otherFunction(val){
    return someFunction(val+3);
}
odoo.define('@web/file_a',function(require){
'use strict';
let __exports={};
const { someFunction }=require("@web/file_b");
__exports.otherFunction =function otherFunction(val){
    return someFunction(val+3):
}
return __exports;
)};

CSS 编码规范

  • 将所有的class命名为o_<module_name>,如o_form_view
  • 避免使用id
  • 使用bootstrap的class
  • 用下划线+小写来命名
.o_form_view{
    .o_iban {
        display: inline-flex;
        margin-left: -15px;
        margin-top: 5px;
    }
    .o_iban_fail {
        cursor: help;
    }
    .o_iban_input_with_validator {
        padding-right: 20px !important;
    }
}

  • 11
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值