Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion edi_core_oca/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@
"edi_core_oca/static/tests/**/*",
],
},
"demo": ["demo/edi_backend_demo.xml"],
"demo": [
"demo/edi_backend_demo.xml",
"demo/edi_configuration_demo.xml",
],
"installable": True,
}
46 changes: 46 additions & 0 deletions edi_core_oca/demo/edi_configuration_demo.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="edi_conf_trigger_cron_hourly" model="edi.configuration.trigger">
<field name="name">On hourly schedule</field>
<field name="code">on_cron_hourly</field>
<field
name="description"
>Trigger when the hourly EDI scheduled action runs</field>
</record>
<record id="edi_conf_trigger_cron_daily" model="edi.configuration.trigger">
<field name="name">On daily schedule</field>
<field name="code">on_cron_daily</field>
<field
name="description"
>Trigger when the daily EDI scheduled action runs</field>
</record>

<record id="cron_edi_configuration_hourly" model="ir.cron">
<field name="name">EDI configuration hourly trigger</field>
<field name="active" eval="True" />
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">hours</field>
<field
name="nextcall"
eval="(DateTime.now() + timedelta(hours=1)).strftime('%Y-%m-%d %H:00:00')"
/>
<field name="model_id" ref="edi_core_oca.model_edi_configuration" />
<field name="state">code</field>
<field name="code">model._cron_run_by_trigger("on_cron_hourly")</field>
</record>
<record id="cron_edi_configuration_daily" model="ir.cron">
<field name="name">EDI configuration daily trigger</field>
<field name="active" eval="True" />
<field name="user_id" ref="base.user_root" />
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field
name="nextcall"
eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d 00:00:00')"
/>
<field name="model_id" ref="edi_core_oca.model_edi_configuration" />
<field name="state">code</field>
<field name="code">model._cron_run_by_trigger("on_cron_daily")</field>
</record>
</odoo>
63 changes: 50 additions & 13 deletions edi_core_oca/models/edi_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

import datetime
import logging

import pytz
from psycopg2.extensions import AsIs

from odoo import api, exceptions, fields, models
from odoo.fields import Domain
from odoo.tools import DotDict, safe_eval

_logger = logging.getLogger(__name__)


def date_to_datetime(dt):
"""Convert date to datetime."""
Expand Down Expand Up @@ -239,22 +242,56 @@ def edi_get_conf_global(self, exchange_record, trigger):
]
return self.search(domain)

def action_view_partners(self):
# TODO: add tests
partner_model = self.env["res.partner"]
partner_ids = set()
# Find partners linked to this conf no matter which field
query = "SELECT DISTINCT(partner_id) FROM %(table)s WHERE conf_id=%(conf_id)s"
for field in partner_model._fields.values():
if field.type == "many2many" and field.comodel_name == self._name:
self.env.cr.execute(
query, {"table": AsIs(field.relation), "conf_id": self.id}
@api.model
def _cron_run_by_trigger(self, trigger_code):
"""Execute every configuration listening to a scheduled trigger.

Scheduled triggers carry no originating record: the scheduled action
knows the trigger code and nothing else. A global configuration is
therefore executed once, bound to nothing; any other one is executed
against each of the records subscribed to it.
"""
for conf in self.search([("trigger", "=", trigger_code)]):
if not conf.model_name:
# Event triggers fall back on the record that fired them, a
# scheduled one has none. Without a model there is not even an
# empty recordset to hand over to the snippet.
_logger.warning(
"Scheduled EDI configuration %s has no model: skipped.",
conf.display_name,
)
partner_ids.update([r[0] for r in self.env.cr.fetchall()])
continue
if conf.is_global:
# A global configuration is bound to no relation, so there is
# nothing to resolve: the snippet runs once and selects the
# records it works on by itself.
conf.edi_exec_snippet_do(self.env[conf.model_name])
continue
for record in conf._get_records(self.env[conf.model_name]):
conf.edi_exec_snippet_do(record)

def _get_records(self, model):
"""Return the records of ``model`` subscribed to this configuration.

A record subscribes to a configuration through a many2many declared on
its own model -- there may be several such relations, one per EDI flow,
and a model holding none yields no record.
"""
self.ensure_one()
return model.search(
Domain.OR(
Domain(fname, "in", self.ids)
for fname, field in model._fields.items()
if field.type == "many2many" and field.comodel_name == self._name
)
)

def action_view_partners(self):
partners = self._get_records(self.env["res.partner"])
return {
"type": "ir.actions.act_window",
"name": self.env._("Partners"),
"res_model": "res.partner",
"view_mode": "list,form",
"domain": [("id", "in", list(partner_ids))],
"domain": [("id", "in", partners.ids)],
}
10 changes: 10 additions & 0 deletions edi_core_oca/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,16 @@ The snippet receives at least two variables in its evaluation context:
Plus the standard `edi_exec_snippet_do` extras (`operation`,
`edi_action`, `old_value`, `vals`, ...).

Triggers can also be fired by a scheduled action instead of by an
exchange. Add an `edi.configuration.trigger` with a code of your own,
then a scheduled action on `edi.configuration` running
`model._cron_run_by_trigger("your_code")` on whatever period you need.
No such trigger ships as data, since the periods a database needs are
its own; the demo data has an hourly and a daily one to copy from.

A scheduled trigger has no originating record, so its configurations run
on the records linked to them, or once when they are global.

Two complementary lookup modes are available, and they can be combined
freely on the same flow.

Expand Down
177 changes: 177 additions & 0 deletions edi_core_oca/tests/test_edi_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
import os
import unittest

from odoo import Command
from odoo.orm.model_classes import add_to_registry
from odoo.tests.common import RecordCapturer
from odoo.tools import mute_logger
from odoo.tools.convert import convert_file

from .common import EDIBackendCommonTestCase

Expand Down Expand Up @@ -61,6 +65,21 @@ def _setup_records(cls): # pylint:disable=missing-return
cls.exchange_type_out.send_model_id = cls.model
cls.exchange_type_out.exchange_filename_pattern = "{record.id}"
cls.edi_configuration = cls.env["edi.configuration"]
# The scheduled triggers are demo data, so load that file rather than
# declare a near-copy of it here -- which also tells us when the demo
# data itself stops loading.
convert_file(
cls.env,
"edi_core_oca",
"demo/edi_configuration_demo.xml",
None,
mode="init",
noupdate=True,
)
cls.cron_hourly_trigger = cls.env.ref(
"edi_core_oca.edi_conf_trigger_cron_hourly"
)
cls.cron_daily_trigger = cls.env.ref("edi_core_oca.edi_conf_trigger_cron_daily")
cls.create_trigger = cls.env.ref("edi_core_oca.edi_conf_trigger_record_create")
cls.write_trigger = cls.env.ref("edi_core_oca.edi_conf_trigger_record_write")
cls.create_config = cls.edi_configuration.create(
Expand All @@ -86,6 +105,164 @@ def _setup_records(cls): # pylint:disable=missing-return
}
)

def test_cron_run_by_trigger(self):
"""A scheduled configuration runs on the records linked to it.

Scenario:
1. Add a configuration listening to the hourly schedule.
2. Link it to one consumer record, and leave another one alone.
3. Run the hourly scheduled action.
Expected:
- Only the linked record is processed.
"""
conf = self.edi_configuration.create(
{
"name": "Hourly Config",
"trigger_id": self.cron_hourly_trigger.id,
"model_id": self.env["ir.model"]._get_id("edi.exchange.consumer.test"),
"snippet_do": 'record.write({"ref": "processed"})',
}
)
consumer_model = self.env["edi.exchange.consumer.test"]
linked = consumer_model.create(
{"name": "Linked", "edi_config_ids": [Command.link(conf.id)]}
)
unlinked = consumer_model.create({"name": "Unlinked"})

self.edi_configuration._cron_run_by_trigger("on_cron_hourly")

self.assertEqual(linked.ref, "processed")
self.assertFalse(unlinked.ref)

def test_cron_run_by_trigger_ignores_other_schedules(self):
"""Each schedule only runs the configurations listening to it.

Scenario:
1. Add a configuration listening to the hourly schedule.
2. Link a consumer record to it.
3. Run the daily scheduled action.
Expected:
- The record is left untouched.
"""
conf = self.edi_configuration.create(
{
"name": "Hourly Config",
"trigger_id": self.cron_hourly_trigger.id,
"model_id": self.env["ir.model"]._get_id("edi.exchange.consumer.test"),
"snippet_do": 'record.write({"ref": "processed"})',
}
)
record = self.env["edi.exchange.consumer.test"].create(
{"name": "Linked", "edi_config_ids": [Command.link(conf.id)]}
)

self.edi_configuration._cron_run_by_trigger("on_cron_daily")

self.assertFalse(record.ref)

@mute_logger("odoo.addons.edi_core_oca.models.edi_configuration")
def test_cron_run_by_trigger_skips_config_without_model(self):
"""A scheduled configuration with no model does not stop the others.

Scenario:
1. Add a configuration listening to the hourly schedule without
telling it which model it applies to.
2. Add a second, complete one and link a consumer record to it.
3. Run the hourly scheduled action.
Expected:
- The incomplete configuration is skipped.
- The linked record is still processed.
"""
hourly_trigger = self.cron_hourly_trigger
self.edi_configuration.create(
{
"name": "Hourly Config No Model",
"trigger_id": hourly_trigger.id,
"snippet_do": 'record.write({"ref": "processed"})',
}
)
conf = self.edi_configuration.create(
{
"name": "Hourly Config",
"trigger_id": hourly_trigger.id,
"model_id": self.env["ir.model"]._get_id("edi.exchange.consumer.test"),
"snippet_do": 'record.write({"ref": "processed"})',
}
)
record = self.env["edi.exchange.consumer.test"].create(
{"name": "Linked", "edi_config_ids": [Command.link(conf.id)]}
)

self.edi_configuration._cron_run_by_trigger("on_cron_hourly")

self.assertEqual(record.ref, "processed")

def test_get_records_model_without_relation(self):
"""A model that cannot be linked to a configuration yields no record.

Scenario:
1. Ask a configuration for the records of a model carrying no
relation to EDI configurations.
Expected:
- It resolves to no record at all, rather than to every record of
that model.
"""
conf = self.edi_configuration.create({"name": "Config On Countries"})
self.assertFalse(conf._get_records(self.env["res.country"]))

def test_cron_run_by_trigger_global_config(self):
"""A global scheduled configuration runs once, bound to no record.

Scenario:
1. Add a global configuration listening to the hourly schedule,
with nothing subscribed to it, whose snippet picks the records
to work on by itself.
2. Run the hourly scheduled action.
Expected:
- The snippet runs, even though no record is linked to the
configuration.
- It runs exactly once, not once per record of the model.
"""
consumer_model = self.env["edi.exchange.consumer.test"]
consumer_model.create({"name": "Target"})
consumer_model.create({"name": "Another target"})
self.edi_configuration.create(
{
"name": "Hourly Global Config",
"trigger_id": self.cron_hourly_trigger.id,
"model_id": self.env["ir.model"]._get_id("edi.exchange.consumer.test"),
"is_global": True,
"snippet_do": (
'env["edi.exchange.consumer.test"].create('
'{"name": "By global conf"})'
),
}
)

with RecordCapturer(consumer_model, []) as capture:
self.edi_configuration._cron_run_by_trigger("on_cron_hourly")

self.assertEqual(len(capture.records), 1)
self.assertEqual(capture.records.name, "By global conf")

def test_action_view_partners(self):
"""The button opens the partner list on the configuration's customers.

No relation from ``res.partner`` ships here, so there is nothing to
find and only the action itself can be asserted.
"""
conf = self.edi_configuration.create(
{
"name": "Partner Config",
"model_id": self.env["ir.model"]._get_id("res.partner"),
}
)

action = conf.action_view_partners()

self.assertEqual(action["res_model"], "res.partner")
self.assertEqual(action["domain"], [("id", "in", [])])

def test_edi_send_via_edi_config(self):
# Check configuration on create
self.consumer_record.invalidate_recordset()
Expand Down
Loading