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
2 changes: 2 additions & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
odoo-addon-webhook_outgoing @ git+https://github.com/OCA/webhook@refs/pull/15/head#subdirectory=webhook_outgoing
odoo-addon-queue_job @ git+https://github.com/OCA/queue@17.0#subdirectory=queue_job
86 changes: 86 additions & 0 deletions webhook_incoming/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
================
Incoming Webhook
================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:801eb9069d1b38681a4da1eec8219ce59055f9cea46af867d49a7be05b955dc4
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwebhook-lightgray.png?logo=github
:target: https://github.com/OCA/webhook/tree/17.0/webhook_incoming
:alt: OCA/webhook
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/webhook-17-0/webhook-17-0-webhook_incoming
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/webhook&target_branch=17.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module allow creating an automation that send webhook/requests to
another systems via HTTP.

To create a new automation to send webhook requests, go to Settings >
Automated Actions:

- When add an automation, choose Custom Webhook as action to perform.
- Config Endpoint, Headers and Body Template accordingly.

This webhook action use Jinja and rendering engine, you can draft body
template using Jinja syntax.

**Table of contents**

.. contents::
:local:

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/webhook/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/webhook/issues/new?body=module:%20webhook_incoming%0Aversion:%2017.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Hoang Tran

Contributors
------------

- Hoang Tran <thhoang.tr@gmail.com>

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/webhook <https://github.com/OCA/webhook/tree/17.0/webhook_incoming>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions webhook_incoming/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
16 changes: 16 additions & 0 deletions webhook_incoming/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2024 Hoang Tran <thhoang.tr@gmail.com>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

{
"name": "Incoming Webhook",
"summary": "Receive incoming webhook requests as trigger to execute tasks.",
"version": "17.0.0.0.1",
"author": "Hoang Tran,Odoo Community Association (OCA)",
"license": "LGPL-3",
"website": "https://github.com/OCA/webhook",
"depends": ["base_automation", "webhook_outgoing", "queue_job"],
"data": [
"views/base_automation_views.xml",
],
"auto_install": True,
}
2 changes: 2 additions & 0 deletions webhook_incoming/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import base_automation
from . import ir_actions_server
224 changes: 224 additions & 0 deletions webhook_incoming/models/base_automation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# Copyright 2024 Hoang Tran <thhoang.tr@gmail.com>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import base64
import json
import logging
import traceback
from contextlib import closing

from pytz import timezone

from odoo import (
SUPERUSER_ID,
Command,
_,
api,
exceptions,
fields,
models,
registry,
tools,
)
from odoo.tools import ustr
from odoo.tools.float_utils import float_compare
from odoo.tools.safe_eval import safe_eval

_logger = logging.getLogger(__name__)


class InheritedBaseAutomation(models.Model):
_inherit = "base.automation"

allow_creation = fields.Boolean(
string="Allow creation?",
help="Allow executing webhook to maybe create record if a record is not "
"found using record getter",
)
create_record_code = fields.Text(
"Record Creation Code",
default="""# Available variables:
# - env: Odoo Environment on which the action is triggered
# - model: Odoo Model of the record on which the action is triggered;
# is a void recordset
# - record: record on which the action is triggered; may be void
# - records: recordset of all records on which the action is triggered
# in multi-mode; may be void
# - payload: input payload from webhook request
# - time, datetime, dateutil, timezone: useful Python libraries
# - float_compare: Odoo function to compare floats based on specific precisions
# - log: log(message, level='info'): logging function to record debug information
# in ir.logging table
# - UserError: Warning Exception to use with raise
# - Command: x2Many commands namespace
# You must return the created record by assign it to `record` variable:
# - record = res.partner(1,)
""",
help="Create record if Record Getter couldn't find a matching one.",
)
delay_execution = fields.Boolean(
help="Queue actions to perform to delay execution."
)

def _get_eval_context(self, payload=None):
"""
Override to add payload to context
"""
eval_context = super()._get_eval_context()
eval_context["model"] = self.env[self.model_name]
eval_context["payload"] = payload if payload is not None else {}
return eval_context

def _execute_webhook(self, payload):
"""Execute the webhook for the given payload.
The payload is a dictionnary that can be used by the `record_getter` to
identify the record on which the automation should be run.
"""
self.ensure_one()

# info logging is done by the ir.http logger
msg = "Webhook #%s triggered with payload %s"
msg_args = (self.id, payload)
_logger.debug(msg, *msg_args)

record = self.env[self.model_name]
eval_context = self._get_eval_context(payload=payload)

if self.record_getter:
try:
record = safe_eval(self.record_getter, eval_context)
except Exception as e: # noqa: BLE001
msg = (
"Webhook #%s couldn't be triggered because record_getter failed:"
"\n%s"
)
msg_args = (self.id, traceback.format_exc())
_logger.warning(msg, *msg_args)
self._webhook_logging(payload, self._add_postmortem(e))
raise e

if not record.exists() and self.allow_creation:
try:
create_eval_context = self._get_create_eval_context(payload=payload)
safe_eval(
self.create_record_code,
create_eval_context,
mode="exec",
nocopy=True,
) # nocopy allows to return 'action'
record = create_eval_context.get("record", self.model_id.browse())
except Exception as e: # noqa: BLE001
msg = "Webhook #%s failed with error:\n%s"
msg_args = (self.id, traceback.format_exc())
_logger.warning(msg, *msg_args)
self._webhook_logging(payload, self._add_postmortem(e))

elif not record.exists():
msg = (
"Webhook #%s could not be triggered because "
"no record to run it on was found."
)
msg_args = (self.id,)
_logger.warning(msg, *msg_args)
self._webhook_logging(payload, msg)
raise exceptions.ValidationError(
_("No record to run the automation on was found.")
)

try:
res = self._process(record)
self._webhook_logging(payload, None)
return res
except Exception as e: # noqa: BLE001
msg = "Webhook #%s failed with error:\n%s"
msg_args = (self.id, traceback.format_exc())
_logger.warning(msg, *msg_args)
self._webhook_logging(payload, self._add_postmortem(e))
raise e

def _get_create_eval_context(self, payload=None):
def log(message, level="info"):
with self.pool.cursor() as cr:
cr.execute(
"""
INSERT INTO ir_logging(
create_date, create_uid, type, dbname, name,
level, message, path, line, func
)
VALUES (
NOW() at time zone 'UTC', %s,
%s, %s, %s, %s,
%s, %s, %s, %s,
)
""",
(
self.env.uid,
"server",
self._cr.dbname,
__name__,
level,
message,
"action",
self.id,
self.name,
),
)

eval_context = dict(self.env.context)
model_name = self.model_id.sudo().model
model = self.env[model_name]
eval_context.update(
{
"uid": self._uid,
"user": self.env.user,
"time": tools.safe_eval.time,
"datetime": tools.safe_eval.datetime,
"dateutil": tools.safe_eval.dateutil,
"timezone": timezone,
"float_compare": float_compare,
"b64encode": base64.b64encode,
"b64decode": base64.b64decode,
"Command": Command,
"env": self.env,
"model": model,
"log": log,
"payload": payload,
}
)
return eval_context

def _webhook_logging(self, body, response):
if self.log_webhook_calls:
with closing(registry(self.env.cr.dbname).cursor()) as cr:
env = api.Environment(cr, SUPERUSER_ID, {})

def create_log(env, response):
vals = {
"webhook_type": "incoming",
"webhook": f"{self.name} ({self})",
"endpoint": f"/web/hook/{self.webhook_uuid}",
"headers": {},
"request": json.dumps(ustr(body), indent=4),
"body": json.dumps(ustr(body), indent=4),
"response": ustr(response),
"status": getattr(response, "status_code", None),
}
env["webhook.logging"].create(vals)
env.cr.commit()

create_log(env, response)

def _process(self, records, domain_post=None):
"""
Override to allow delay execution
"""
to_delay = self.filtered(lambda a: a.delay_execution)
execute_now = self - to_delay

super(
InheritedBaseAutomation,
to_delay.with_context(delay_execution=True),
)._process(records, domain_post=domain_post)

return super(InheritedBaseAutomation, execute_now)._process(
records, domain_post=domain_post
)
21 changes: 21 additions & 0 deletions webhook_incoming/models/ir_actions_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2024 Hoang Tran <thhoang.tr@gmail.com>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, models


class IrActionsServer(models.Model):
_inherit = "ir.actions.server"

def run(self):
if self.env.context.get("delay_execution"):
return self.with_delay().run()
return super().run()

@api.model
def _job_prepare_context_before_enqueue_keys(self):
return (
"active_model",
"active_ids",
"active_id",
"domain_post",
)
3 changes: 3 additions & 0 deletions webhook_incoming/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
1 change: 1 addition & 0 deletions webhook_incoming/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Hoang Tran \<<thhoang.tr@gmail.com>\>
11 changes: 11 additions & 0 deletions webhook_incoming/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
This module allow creating an automation that send webhook/requests to
another systems via HTTP.

To create a new automation to send webhook requests, go to Settings \>
Automated Actions:

- When add an automation, choose Custom Webhook as action to perform.
- Config Endpoint, Headers and Body Template accordingly.

This webhook action use Jinja and rendering engine, you can draft body
template using Jinja syntax.
Loading