How to add new emails models to email notifications

Hi All,

One of our customers requested to send email notifications to the email defined in the company of the records. In order to achieve it I implemented the following code:

from trytond.pool import Pool, PoolMeta


class EmailTemplate(metaclass=PoolMeta):
    __name__ = 'ir.email.template'

    @classmethod
    def email_models(cls):
        return super().email_models() + ['company.company']

    @classmethod
    def _get_address(cls, record):
        pool = Pool()
        Company = pool.get('company.company')
        address = super()._get_address(record)
        if isinstance(record, Company):
            address = cls._get_address(record.party)
        return address

    @classmethod
    def _get_language(cls, record):
        pool = Pool()
        Company = pool.get('company.company')
        language = super()._get_language(record)
        if isinstance(record, Company):
            language = cls._get_language(record.party)
        return language

And registered the class on the pool in a custom module.
On the same module I defined an XML record which defines the notification emails and uses the company field of the invoice class.

When activating the module I get the following error:

----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/convert.py", line 464, in parse_xmlstream
    self.sax_parser.parse(source)
  File "/usr/lib/python3.12/xml/sax/expatreader.py", line 105, in parse
    xmlreader.IncrementalParser.parse(self, source)
  File "/usr/lib/python3.12/xml/sax/xmlreader.py", line 124, in parse
    self.feed(buffer)
  File "/usr/lib/python3.12/xml/sax/expatreader.py", line 211, in feed
    self._parser.Parse(data, isFinal)
  File "./Modules/pyexpat.c", line 475, in EndElement
  File "/usr/lib/python3.12/xml/sax/expatreader.py", line 344, in end_element
    self._cont_handler.endElement(name)
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/convert.py", line 519, in endElement
    self.taghandler = self.taghandler.endElement(name)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/convert.py", line 314, in endElement
    self.mh.import_record(
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/convert.py", line 695, in import_record
    self.create_records(model, [values], [fs_id])
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/convert.py", line 701, in create_records
    records = Model.create(vlist)
              ^^^^^^^^^^^^^^^^^^^
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/model/modelsql.py", line 262, in wrapper
    return func(cls, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/model/modelsql.py", line 964, in create
    cls._validate(sub_records)
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/transaction.py", line 50, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/model/modelstorage.py", line 1373, in _validate
    validate_domain(field)
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/model/modelstorage.py", line 1274, in validate_domain
    validate_relation_domain(
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/model/modelstorage.py", line 1355, in validate_relation_domain
    raise DomainValidationError(
trytond.model.modelstorage.DomainValidationError: The value "Company (company)" for field "Recipients" in "New Invoice Notification" of "Email Notification" is not valid according to its domain. -

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/tests/test_tryton.py", line 294, in setUpClass
    activate_module(modules, lang=cls.language)
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/tests/test_tryton.py", line 114, in activate_module
    ActivateUpgrade(instance_id).transition_upgrade()
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/ir/module.py", line 505, in transition_upgrade
    pool.init(update=update, lang=lang)
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/pool.py", line 167, in init
    restart = not load_modules(
                  ^^^^^^^^^^^^^
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/modules/__init__.py", line 394, in load_modules
    _load_modules(update)
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/modules/__init__.py", line 357, in _load_modules
    load_module_graph(graph, pool, update, lang)
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/modules/__init__.py", line 208, in load_module_graph
    tryton_parser.parse_xmlstream(fp)
  File "/home/pokoli/projectes/xxxx/tryton/trytond/trytond/convert.py", line 466, in parse_xmlstream
    raise ParsingError("in %s" % self.current_state()) from e
trytond.convert.ParsingError: in record 'custom_module.posted_invoice_notification'

What I’m doing wrong? How can I extend the recipients of the notification email to include also the company?

P.S: If you thin interesting to add the company as a recipient of the emails, just let me know and I will be happy to contribute the needed code to add it as part of standard modules.

Hi,

Could you share the xml record you are trying to insert here?

Can you manually call email_models from the trytond-console to make sure 'company.company' is actually there?

Of course, here you have:

       <record model="ir.action.report" id="report_posted_invoice_notification">
            <field name="name">New Invoice Notification</field>
            <field name="model">account.invoice</field>
            <field name="report_name">account.invoice.posted_notification</field>
            <field name="report">custom_module/invoice_notification.html</field>
            <field name="template_extension">html</field>
        </record>

        <record model="notification.email" id="posted_invoice_notification">
            <field name="subject">Invoice per email</field>
            <field name="content" ref="report_posted_invoice_notification"/>
            <field name="recipients"
                search="[('model.model', '=', 'account.invoice'), ('name', '=', 'company')]"/>
            <field name="contact_mechanism">invoice</field>
        </record>

        <record model="notification.email.attachment" id="posted_invoice_notification_report_invoice">
            <field name="notification" ref="posted_invoice_notification"/>
            <field name="report" ref="report_invoice2"/>
        </record>

        <record model="ir.trigger" id="posted_invoice_trigger">
            <field name="name">Posted Invoice Notification</field>
            <field name="notification_email" ref="posted_invoice_notification"/>
            <field name="model" search="[('model', '=', 'account.invoice')]"/>
            <field name="on_create" eval="False"/>
            <field name="on_write" eval="True"/>
            <field name="on_delete" eval="False"/>
            <field name="on_time" eval="False"/>
            <field name="action">notification.email|trigger</field>
            <field name="limit_number" eval="1"/>
            <field name="condition" eval="And(Eval('self', {}).get('type', '') == 'out', Eval('self', {}).get('state', 'draft').in_(['posted', 'paid']))" pyson="1"/>
        </record>

Yes, it is there:

>>> EmailTemplate = pool.get('ir.email.template')
>>> EmailTemplate.email_models()
['res.user', 'party.party', 'party.contact_mechanism', 'company.employee', 'company.company']

Got it.

The __setup__ of notification.email uses the pool while it’s being initialized. The field update should probably happen in the __post_setup__.

Edit: What I mean is at this point, pool.get('ir.email.template') does not include the overrides declared in your module, because it is not loaded yet

I can confirm the overrides are not included.

So you think this is a bug on the notification email module?
I updated its code to define the domain on __post_setup__ instead of __setup__ but the error persisted.

So you think this is a bug on the notification email module?

I am almost sure of it.

The domain of the fields is updated based on the contents of the ir.email.template model in its current state when the notification.email model is registered.

Declaring an empty override of notification.email in your module and registering it after your ir.email.template override should fix this, though the real problem IMO is the __setup__.

I got something similar implemented, but I didn’t use xml to define the notification.email record, I entered it in the client. (and it seems to work)

I tried with:

class Notification(metaclass=PoolMeta):
    __name__ = 'notification.email'

    @classmethod
    def __setup__(cls):
        super().__setup__()
        for field in [
                'recipients',
                'recipients_secondary',
                'recipients_hidden',
                ]:
            field = getattr(cls, field)
            print(field.domain)

But this is the domain defined on the fields:

[('model.model', '=', Eval('model', '')), ['OR', ('relation', 'in', ['res.user', 'party.party', 'party.contact_mechanism', 'company.employee']), [('model.model', 'in', ['res.user', 'party.party', 'party.contact_mechanism', 'company.employee']), ('name', '=', 'id')]], ['OR', ('relation', 'in', ['res.user', 'party.party', 'party.contact_mechanism', 'company.employee', 'company.company']), [('model.model', 'in', ['res.user', 'party.party', 'party.contact_mechanism', 'company.employee', 'company.company']), ('name', '=', 'id')]]]

As you see the relation models are included multiple times, one of them is missing the custom model (company.company) thus the code fails because the domain is applied.

I’ve just filled It is not possible to include custom models in email notifications (#13410) · Issues · Tryton / Tryton · GitLab

The __setup__ appends in the domain, so the override adds a new constraint rather than replacing the existing one…

Renaming to __post_setup__ and keeping your override should work (though still a workaround).

I have the following code as a workarround:

class Notification(metaclass=PoolMeta):
    __name__ = 'notification.email'

    @classmethod
    def __setup__(cls):
        super().__setup__()
        for field in [
                'recipients',
                'recipients_secondary',
                'recipients_hidden',
                ]:
            field = getattr(cls, field)
            for clause in field.domain:
                if clause[0] == 'OR':
                    for models in [clause[1][2], clause[2][0][2]]:
                        if 'company.company' not in models:
                            models.append('company.company')