External dependencies in odoo

There is some non-obvious issue with external dependency in modules, which I want to say.

Case

Let you need to use some python lib, which is not available in system by default and should be installed manually.

So you just make import:

from pandas import DataFrame

And maybe you mention required dependence in module's description. Very simple, right? But…

Issue

The case could crash odoo completely.

This is scenario when it could happen:

  • User is not going to use your module with external dependency, but your module available in Local Modules. E.g. if have a lot of modules in your git repository and user just makes git clone and add path to repo to addons-path.
  • User's system doesn't have required dependency
  • Your module has static/ folder, e.g. to place icon.png file.

Then right after restarting server, user would get error:

2015-07-05 04:57:15,078 5760 ERROR ? werkzeug: Error on request:
Traceback (most recent call last):
  File "/usr/share/pyshared/werkzeug/serving.py", line 159, in run_wsgi
    execute(app)
  File "/usr/share/pyshared/werkzeug/serving.py", line 146, in execute
    application_iter = app(environ, start_response)
  File "/mnt/files/src/odoo/openerp/service/server.py", line 281, in app
    return self.app(e, s)
  File "/mnt/files/src/odoo/openerp/service/wsgi_server.py", line 216, in application
    return application_unproxied(environ, start_response)
  File "/mnt/files/src/odoo/openerp/service/wsgi_server.py", line 202, in application_unproxied
    result = handler(environ, start_response)
  File "/mnt/files/src/odoo/openerp/http.py", line 1244, in __call__
    self.load_addons()
  File "/mnt/files/src/odoo/openerp/http.py", line 1263, in load_addons
    m = __import__('openerp.addons.' + module)
  File "/mnt/files/src/odoo/openerp/modules/module.py", line 77, in load_module
    mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
  File "/mnt/files/git/addons-yelizariev/import_custom/__init__.py", line 1, in <module>
    import wizard
  File "/mnt/files/git/addons-yelizariev/import_custom/wizard/__init__.py", line 1, in <module>
    import upload
  File "/mnt/files/git/addons-yelizariev/import_custom/wizard/upload.py", line 14, in <module>
    from pandas import DataFrame
ImportError: No module named pandas

Suggestion

Whenever you use external dependency, put your import code in try block

try:
    from pandas import DataFrame
except ImportError:
    pass

and add external_dependencies to module's manifest:

"external_dependencies": {
    'python': ['pandas']
},

It will check dependencies before installing and will not raise ImportError if user doesn't use module.

links

0 Comments

Source diving

Odoo is opensource and has module structure. So, for the question

– Is it possible to change /this/ in Odoo?

the only answer is:

– Yes, everything is possible to do in Odoo.

OK, but how to do that? Where to find an instruction of how to implement some particular feature?

Let's take an example. Imagine we need a custom link for product at website shop. By default link is generated from Name field, but you need to make it be generated from new Name SEO field.

i.e. instead of this link:

/shop/product/ipad-mini-8

there should be this one:

/shop/product/super-seo-link-for-ipad-mini-8

where product has fields:

Name: iPad Mini

Name SEO: Super SEO link for iPad Mini

How to do that? Can we find the answer in documentation? I believe, it doesn't have an answer.

So, let's try to do it ourself. We have Odoo source, so let's try to find a solution there.

In our example we can start from seaching by /shop/product/:

./addons/website_sale_options/controllers/main.py:10:    @http.route(['/shop/product/<model("product.template"):product>'], type='http', auth="public", website=True)
./addons/website_sale/models/product.py:112:            res[product.id] = "/shop/product/%s" % (product.id,)
./addons/website_sale/models/product.py:186:            res[product.id] = "/shop/product/%s" % (product.product_tmpl_id.id,)
./addons/website_sale/controllers/main.py:230:    @http.route(['/shop/product/<model("product.template"):product>'], type='http', auth="public", website=True)
./addons/website_sale/controllers/main.py:276:    @http.route(['/shop/product/comment/<int:product_template_id>'], type='http', auth="public", methods=['POST'], website=True)
./addons/website_sale/controllers/main.py:850:        return request.redirect("/shop/product/%s?enable_editor=1" % slug(product.product_tmpl_id))

These lines from addons/website_sale/controllers/main.py looks interesting:

@http.route(['/shop/product/<model("product.template"):product>'], type='http', auth="public", website=True)
    def product(self, product, category='', search='', **kwargs):
What about <model("product.template"):product>? What is it? How it works? There should be a way for you to find the answer in source. Maybe you know about werkzeug library, maybe you know about slug function, maybe you know about ir.http model. So there should be a keyword to search. At the worst you can search by "model" keyword OR look through whole website module.

OK, I found the answer in addons/website/models/ir_http.py. Function to_url is what we need.

from openerp.addons.website.models.website import slug, url_for, _UNSLUG_RE

#..

class ir_http(orm.AbstractModel):
    _inherit = 'ir.http'

    rerouting_limit = 10
    geo_ip_resolver = None

    def _get_converters(self):
        return dict(
            super(ir_http, self)._get_converters(),
            model=ModelConverter,
            page=PageConverter,
        )


#..

class ModelConverter(ir.ir_http.ModelConverter):
    def __init__(self, url_map, model=False, domain='[]'):
        super(ModelConverter, self).__init__(url_map, model)
        self.domain = domain
        self.regex = _UNSLUG_RE.pattern

    def to_url(self, value):
        return slug(value)

#...
where slug function is from addons/website/models/website.py
def slug(value):
    if isinstance(value, orm.browse_record):
        # [(id, name)] = value.name_get()
        id, name = value.id, value.display_name
    else:
        # assume name_search result tuple
        id, name = value
    slugname = slugify(name or '').strip().strip('-')
    if not slugname:
        return str(id)
    return "%s-%d" % (slugname, id)
So, that is. To make a custom link, we have to inherit ModelConverter and modify to_url function. Something like that:

from openerp.addons.website.models.website import slugify
from openerp.addons.website.models.ir_http import ModelConverter

class ModelConverterCustom(ModelConverter):
    def to_url(self, value):
        if isinstance(value, orm.browse_record) and hasattr(value, 'name_seo'):
            id, name = value.id, value.name_seo
            slugname = slugify(name or '').strip().strip('-')
            if slugname:
                return "%s-%d" % (slugname, id)
        return super(ModelConverterCustom, self).to_url(value)

class ir_http(orm.AbstractModel):
    _inherit = 'ir.http'

    def _get_converters(self):
        res = super(ir_http, self)._get_converters()
        res['model'] = ModelConverterCustom
        return res

This is the way I use in odoo development:

  • Dive into source
  • Figure out how it works
  • Create module to make changes you need
0 Comments

Mail relocation

Odoo has a good integration with emails. It allows to have a discussion with a customer attached to some record (task, lead, invoice etc.).

In the main, your customer don't know about this and can send emails (e.g. about a task) to your personal alias (e.g. admin@yourcompany). It could not be a problem, if you work on task alone. But if you need to share mail to colleagues, it's better to move (attach) mail to task form. I've made module mail_move_message to do it.

Technically, the module update fields res_id and model of mail.message table. It also keeps original values to allow to move everything back.

A usage guide with screenshots can be found here: https://apps.odoo.com/apps/modules/8.0/mail\_move\_message/

It's not mentioned in module's description, but if you move a message, then all messages in a thread below the message would also be moved to a new location.

Repository: https://github.com/yelizariev/addons-yelizariev/tree/8.0/mail\_move\_message

Uninstalling

Please note, that after uninstalling the module, all infromation about original location of moved messages will be lost.

0 Comments

Skype field in partner form

0 Comments

Templates for user's email signature

If you have a lot of users and you want to control their email signature, then signature template is what you need. With my module res_users_signature you can create signature template and apply one for your users.

A user can choose his signature template himself at Preference window (which is opened via top right hand menu).

Technically the module make minimal changes at user model. It just put new value to signature field at res.users model.

The module updates signature if there are changes at basic users models (res.users, res.partner, hr.employee). E.g. if a user change his phone number, then his signature would be updated. Also, you can update all signatures manually.

Images

You can use base64-encoded images at signature template:

<img src="data:image/png;base64,${user.company_id.logo_web}"/>
<img src="data:image/png;base64,ABCDE....12345="/>

I've found, that some email clients doesn't show such images. In order to fix it, the module overwrite build_email function at ir.mail_server model. The module make attachments from base64 images at email body.

class ir_mail_server(models.Model):
    _inherit = "ir.mail_server"

    def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
               attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None,
               body_alternative=None, subtype_alternative='plain'):
        """ copy-pasted from openerp/addons/base/ir/ir_mail_server.py::build_email """

        ftemplate = '__image-%s__'
        fcounter = 0
        attachments = attachments or []

        pattern = re.compile(r'"data:image/png;base64,[^"]*"')
        pos = 0
        new_body = ''
        while True:
            match = pattern.search(body, pos)
            if not match:
                break
            s = match.start()
            e = match.end()
            data = body[s+len('"data:image/png;base64,'):e-1]
            new_body += body[pos:s]

            fname = ftemplate % fcounter
            fcounter += 1
            attachments.append( (fname, base64.b64decode(data)) )

            new_body += '"cid:%s"' % fname
            pos = e

        new_body += body[pos:]
        body = new_body

        //...

Repository: https://github.com/yelizariev/addons-yelizariev/tree/8.0/res\_users\_signature

0 Comments