diff --git a/ai_oca_bridge_sale/README.rst b/ai_oca_bridge_sale/README.rst
new file mode 100644
index 0000000..e4dcb28
--- /dev/null
+++ b/ai_oca_bridge_sale/README.rst
@@ -0,0 +1,121 @@
+==================
+AI OCA Bridge Sale
+==================
+
+..
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! This file is generated by oca-gen-addon-readme !!
+ !! changes will be overwritten. !!
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ !! source digest: sha256:7db759bf0f80da243772d180303cb0e39243db662bf554ee4adbe340d61c2d92
+ !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+.. |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-AGPL--3-blue.png
+ :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
+ :alt: License: AGPL-3
+.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fai-lightgray.png?logo=github
+ :target: https://github.com/OCA/ai/tree/16.0/ai_oca_bridge_sale
+ :alt: OCA/ai
+.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
+ :target: https://translation.odoo-community.org/projects/ai-16-0/ai-16-0-ai_oca_bridge_sale
+ :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/ai&target_branch=16.0
+ :alt: Try me on Runboat
+
+|badge1| |badge2| |badge3| |badge4| |badge5|
+
+This module is intended to allow external AI systems to perform actions
+with the correct context, based on triggers related to Sales management.
+
+**Table of contents**
+
+.. contents::
+ :local:
+
+Configuration
+=============
+
+- **Bridge with ``Usage = "AI Thread Create"``** Processes new sale
+ orders and order lines in the external system for AI-enhanced actions
+ like sales analysis, customer insights, or automated sales processes.
+
+- **Bridge with ``Usage = "AI Thread Write"``** Updates sale order and
+ order line information in the external system when they are modified
+ in Odoo.
+
+- **Bridge with ``Usage = "AI Thread Unlink"``** Removes sale order and
+ order line data from the external system when they are deleted from
+ Odoo.
+
+For creating those bridges, apart from the usage of the bridge, the user
+must define:
+
+- Payload Type: it depends on the endpoint configuration, normally
+ "Record" would work.
+- Result Type: depending on your use case.
+- Model: select the "Sale Order" or "Sale Order Line" model
+- Field: add at least the fields the endpoint is expecting (e.g., name,
+ partner, product, quantity, state, etc.).
+- Filter: add a domain for using the bridge only with the sale
+ orders/order lines intended to trigger automatic actions
+
+Usage
+=====
+
+Depending on the bridges you have created, create, update or delete a
+sale order or order line record matching the bridge domain. You should
+see the execution on the AI Bridge Execution menu.
+
+Bug Tracker
+===========
+
+Bugs are tracked on `GitHub 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 `_.
+
+Do not contact contributors directly about support or help with technical issues.
+
+Credits
+=======
+
+Authors
+-------
+
+* Escodoo
+
+Contributors
+------------
+
+- `Escodoo `__:
+
+ - Marcel Savegnago
+
+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.
+
+.. |maintainer-marcelsavegnago| image:: https://github.com/marcelsavegnago.png?size=40px
+ :target: https://github.com/marcelsavegnago
+ :alt: marcelsavegnago
+
+Current `maintainer `__:
+
+|maintainer-marcelsavegnago|
+
+This module is part of the `OCA/ai `_ project on GitHub.
+
+You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/ai_oca_bridge_sale/__init__.py b/ai_oca_bridge_sale/__init__.py
new file mode 100644
index 0000000..0650744
--- /dev/null
+++ b/ai_oca_bridge_sale/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/ai_oca_bridge_sale/__manifest__.py b/ai_oca_bridge_sale/__manifest__.py
new file mode 100644
index 0000000..02d493d
--- /dev/null
+++ b/ai_oca_bridge_sale/__manifest__.py
@@ -0,0 +1,21 @@
+# Copyright 2025 Escodoo
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+{
+ "name": "AI OCA Bridge Sale",
+ "summary": """Adds Sale triggers for AI Bridges""",
+ "version": "16.0.1.0.0",
+ "license": "AGPL-3",
+ "author": "Escodoo,Odoo Community Association (OCA)",
+ "maintainers": ["marcelsavegnago"],
+ "category": "AI",
+ "website": "https://github.com/OCA/ai",
+ "depends": [
+ "ai_oca_bridge",
+ "sale",
+ ],
+ "data": [],
+ "demo": [
+ "demo/ai_bridge_demo.xml",
+ ],
+}
diff --git a/ai_oca_bridge_sale/demo/ai_bridge_demo.xml b/ai_oca_bridge_sale/demo/ai_bridge_demo.xml
new file mode 100644
index 0000000..8a150c8
--- /dev/null
+++ b/ai_oca_bridge_sale/demo/ai_bridge_demo.xml
@@ -0,0 +1,116 @@
+
+
+
+
+ Sale Order AI Bridge - Create
+
+ This AI bridge is triggered when a new sale order is created.
+ ]]>
+
+
+ ai_thread_create
+ https://api.example.com/ai/sale_order/create
+ none
+ record
+ none
+ immediate
+
+
+
+
+ Sale Order AI Bridge - Update
+
+ This AI bridge is triggered when a sale order is updated.
+ ]]>
+
+
+ ai_thread_write
+ https://api.example.com/ai/sale_order/update
+ none
+ record
+ none
+ immediate
+
+
+
+
+ Sale Order AI Bridge - Delete
+
+ This AI bridge is triggered when a sale order is deleted.
+ ]]>
+
+
+ ai_thread_unlink
+ https://api.example.com/ai/sale_order/delete
+ none
+ none
+ none
+ immediate
+
+
+
+
+ Sale Order Line AI Bridge - Create
+
+ This AI bridge is triggered when a new sale order line is created.
+ ]]>
+
+
+ ai_thread_create
+ https://api.example.com/ai/sale_order_line/create
+ none
+ record
+ none
+ immediate
+
+
+
+
+ Sale Order Line AI Bridge - Update
+
+ This AI bridge is triggered when a sale order line is updated.
+ ]]>
+
+
+ ai_thread_write
+ https://api.example.com/ai/sale_order_line/update
+ none
+ record
+ none
+ immediate
+
+
+
+
+ Sale Order Line AI Bridge - Delete
+
+ This AI bridge is triggered when a sale order line is deleted.
+ ]]>
+
+
+ ai_thread_unlink
+ https://api.example.com/ai/sale_order_line/delete
+ none
+ none
+ none
+ immediate
+
+
diff --git a/ai_oca_bridge_sale/models/__init__.py b/ai_oca_bridge_sale/models/__init__.py
new file mode 100644
index 0000000..2d7ee6c
--- /dev/null
+++ b/ai_oca_bridge_sale/models/__init__.py
@@ -0,0 +1,2 @@
+from . import sale_order
+from . import sale_order_line
diff --git a/ai_oca_bridge_sale/models/sale_order.py b/ai_oca_bridge_sale/models/sale_order.py
new file mode 100644
index 0000000..98851a4
--- /dev/null
+++ b/ai_oca_bridge_sale/models/sale_order.py
@@ -0,0 +1,6 @@
+from odoo import models
+
+
+class SaleOrder(models.Model):
+ _inherit = ["sale.order", "ai.bridge.thread"]
+ _name = "sale.order"
diff --git a/ai_oca_bridge_sale/models/sale_order_line.py b/ai_oca_bridge_sale/models/sale_order_line.py
new file mode 100644
index 0000000..0ef5cd6
--- /dev/null
+++ b/ai_oca_bridge_sale/models/sale_order_line.py
@@ -0,0 +1,6 @@
+from odoo import models
+
+
+class SaleOrderLine(models.Model):
+ _inherit = ["sale.order.line", "ai.bridge.thread"]
+ _name = "sale.order.line"
diff --git a/ai_oca_bridge_sale/readme/CONFIGURE.md b/ai_oca_bridge_sale/readme/CONFIGURE.md
new file mode 100644
index 0000000..1d75d68
--- /dev/null
+++ b/ai_oca_bridge_sale/readme/CONFIGURE.md
@@ -0,0 +1,15 @@
+- **Bridge with `Usage = "AI Thread Create"`**
+ Processes new sale orders and order lines in the external system for AI-enhanced actions like sales analysis, customer insights, or automated sales processes.
+
+- **Bridge with `Usage = "AI Thread Write"`**
+ Updates sale order and order line information in the external system when they are modified in Odoo.
+
+- **Bridge with `Usage = "AI Thread Unlink"`**
+ Removes sale order and order line data from the external system when they are deleted from Odoo.
+
+For creating those bridges, apart from the usage of the bridge, the user must define:
+- Payload Type: it depends on the endpoint configuration, normally "Record" would work.
+- Result Type: depending on your use case.
+- Model: select the "Sale Order" or "Sale Order Line" model
+- Field: add at least the fields the endpoint is expecting (e.g., name, partner, product, quantity, state, etc.).
+- Filter: add a domain for using the bridge only with the sale orders/order lines intended to trigger automatic actions
diff --git a/ai_oca_bridge_sale/readme/CONTRIBUTORS.md b/ai_oca_bridge_sale/readme/CONTRIBUTORS.md
new file mode 100644
index 0000000..f618905
--- /dev/null
+++ b/ai_oca_bridge_sale/readme/CONTRIBUTORS.md
@@ -0,0 +1,2 @@
+- [Escodoo](https://www.escodoo.com.br):
+ - Marcel Savegnago \
diff --git a/ai_oca_bridge_sale/readme/DESCRIPTION.md b/ai_oca_bridge_sale/readme/DESCRIPTION.md
new file mode 100644
index 0000000..e58472a
--- /dev/null
+++ b/ai_oca_bridge_sale/readme/DESCRIPTION.md
@@ -0,0 +1 @@
+This module is intended to allow external AI systems to perform actions with the correct context, based on triggers related to Sales management.
diff --git a/ai_oca_bridge_sale/readme/USAGE.md b/ai_oca_bridge_sale/readme/USAGE.md
new file mode 100644
index 0000000..2ec9bf5
--- /dev/null
+++ b/ai_oca_bridge_sale/readme/USAGE.md
@@ -0,0 +1 @@
+Depending on the bridges you have created, create, update or delete a sale order or order line record matching the bridge domain. You should see the execution on the AI Bridge Execution menu.
diff --git a/ai_oca_bridge_sale/static/description/icon.png b/ai_oca_bridge_sale/static/description/icon.png
new file mode 100644
index 0000000..4adee3c
Binary files /dev/null and b/ai_oca_bridge_sale/static/description/icon.png differ
diff --git a/ai_oca_bridge_sale/static/description/index.html b/ai_oca_bridge_sale/static/description/index.html
new file mode 100644
index 0000000..a138632
--- /dev/null
+++ b/ai_oca_bridge_sale/static/description/index.html
@@ -0,0 +1,463 @@
+
+
+
+
+
+AI OCA Bridge Sale
+
+
+
+
+
AI OCA Bridge Sale
+
+
+

+
This module is intended to allow external AI systems to perform actions
+with the correct context, based on triggers related to Sales management.
+
Table of contents
+
+
+
+
+- Bridge with ``Usage = “AI Thread Create”`` Processes new sale
+orders and order lines in the external system for AI-enhanced actions
+like sales analysis, customer insights, or automated sales processes.
+- Bridge with ``Usage = “AI Thread Write”`` Updates sale order and
+order line information in the external system when they are modified
+in Odoo.
+- Bridge with ``Usage = “AI Thread Unlink”`` Removes sale order and
+order line data from the external system when they are deleted from
+Odoo.
+
+
For creating those bridges, apart from the usage of the bridge, the user
+must define:
+
+- Payload Type: it depends on the endpoint configuration, normally
+“Record” would work.
+- Result Type: depending on your use case.
+- Model: select the “Sale Order” or “Sale Order Line” model
+- Field: add at least the fields the endpoint is expecting (e.g., name,
+partner, product, quantity, state, etc.).
+- Filter: add a domain for using the bridge only with the sale
+orders/order lines intended to trigger automatic actions
+
+
+
+
+
Depending on the bridges you have created, create, update or delete a
+sale order or order line record matching the bridge domain. You should
+see the execution on the AI Bridge Execution menu.
+
+
+
+
Bugs are tracked on GitHub 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.
+
Do not contact contributors directly about support or help with technical issues.
+
+
+
+
+
+
+
+
This module is maintained by the OCA.
+
+
+
+
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.
+
Current maintainer:
+

+
This module is part of the OCA/ai project on GitHub.
+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+
+
+
+
+
diff --git a/ai_oca_bridge_sale/tests/__init__.py b/ai_oca_bridge_sale/tests/__init__.py
new file mode 100644
index 0000000..43e04a9
--- /dev/null
+++ b/ai_oca_bridge_sale/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_sale_ai_bridge
diff --git a/ai_oca_bridge_sale/tests/test_sale_ai_bridge.py b/ai_oca_bridge_sale/tests/test_sale_ai_bridge.py
new file mode 100644
index 0000000..d300a59
--- /dev/null
+++ b/ai_oca_bridge_sale/tests/test_sale_ai_bridge.py
@@ -0,0 +1,523 @@
+# Copyright 2025 Escodoo
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
+
+from unittest import mock
+
+from odoo.tests.common import TransactionCase
+
+
+class TestSaleAiBridge(TransactionCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+
+ # Create necessary Sale data
+ cls.partner = cls.env["res.partner"].create(
+ {
+ "name": "Test Customer",
+ "email": "test@example.com",
+ }
+ )
+
+ cls.product = cls.env["product.product"].create(
+ {
+ "name": "Test Product",
+ "detailed_type": "consu",
+ "list_price": 100.0,
+ }
+ )
+
+ # Create bridges for sale orders
+ cls.bridge_sale_order_create = cls.env["ai.bridge"].create(
+ {
+ "name": "Sale Order AI Bridge - Create",
+ "description": "Test bridge for sale order creation
",
+ "model_id": cls.env.ref("sale.model_sale_order").id,
+ "usage": "ai_thread_create",
+ "url": "https://api.example.com/ai/sale_order/create",
+ "auth_type": "none",
+ "payload_type": "record",
+ "result_type": "none",
+ "result_kind": "immediate",
+ "field_ids": [
+ (
+ 6,
+ 0,
+ [
+ cls.env.ref("sale.field_sale_order__name").id,
+ cls.env.ref("sale.field_sale_order__partner_id").id,
+ cls.env.ref("sale.field_sale_order__state").id,
+ ],
+ )
+ ],
+ }
+ )
+
+ cls.bridge_sale_order_write = cls.env["ai.bridge"].create(
+ {
+ "name": "Sale Order AI Bridge - Update",
+ "description": "Test bridge for sale order updates
",
+ "model_id": cls.env.ref("sale.model_sale_order").id,
+ "usage": "ai_thread_write",
+ "url": "https://api.example.com/ai/sale_order/update",
+ "auth_type": "none",
+ "payload_type": "record",
+ "result_type": "none",
+ "result_kind": "immediate",
+ "field_ids": [
+ (
+ 6,
+ 0,
+ [
+ cls.env.ref("sale.field_sale_order__name").id,
+ cls.env.ref("sale.field_sale_order__partner_id").id,
+ cls.env.ref("sale.field_sale_order__state").id,
+ ],
+ )
+ ],
+ }
+ )
+
+ cls.bridge_sale_order_unlink = cls.env["ai.bridge"].create(
+ {
+ "name": "Sale Order AI Bridge - Delete",
+ "description": "Test bridge for sale order deletion
",
+ "model_id": cls.env.ref("sale.model_sale_order").id,
+ "usage": "ai_thread_unlink",
+ "url": "https://api.example.com/ai/sale_order/delete",
+ "auth_type": "none",
+ "payload_type": "none",
+ "result_type": "none",
+ "result_kind": "immediate",
+ }
+ )
+
+ # Create bridges for sale order lines
+ cls.bridge_sale_order_line_create = cls.env["ai.bridge"].create(
+ {
+ "name": "Sale Order Line AI Bridge - Create",
+ "description": "Test bridge for sale order line creation
",
+ "model_id": cls.env.ref("sale.model_sale_order_line").id,
+ "usage": "ai_thread_create",
+ "url": "https://api.example.com/ai/sale_order_line/create",
+ "auth_type": "none",
+ "payload_type": "record",
+ "result_type": "none",
+ "result_kind": "immediate",
+ "field_ids": [
+ (
+ 6,
+ 0,
+ [
+ cls.env.ref("sale.field_sale_order_line__name").id,
+ cls.env.ref("sale.field_sale_order_line__product_id").id,
+ cls.env.ref(
+ "sale.field_sale_order_line__product_uom_qty"
+ ).id,
+ ],
+ )
+ ],
+ }
+ )
+
+ cls.bridge_sale_order_line_write = cls.env["ai.bridge"].create(
+ {
+ "name": "Sale Order Line AI Bridge - Update",
+ "description": "Test bridge for sale order line updates
",
+ "model_id": cls.env.ref("sale.model_sale_order_line").id,
+ "usage": "ai_thread_write",
+ "url": "https://api.example.com/ai/sale_order_line/update",
+ "auth_type": "none",
+ "payload_type": "record",
+ "result_type": "none",
+ "result_kind": "immediate",
+ "field_ids": [
+ (
+ 6,
+ 0,
+ [
+ cls.env.ref("sale.field_sale_order_line__name").id,
+ cls.env.ref("sale.field_sale_order_line__product_id").id,
+ cls.env.ref(
+ "sale.field_sale_order_line__product_uom_qty"
+ ).id,
+ ],
+ )
+ ],
+ }
+ )
+
+ cls.bridge_sale_order_line_unlink = cls.env["ai.bridge"].create(
+ {
+ "name": "Sale Order Line AI Bridge - Delete",
+ "description": "Test bridge for sale order line deletion
",
+ "model_id": cls.env.ref("sale.model_sale_order_line").id,
+ "usage": "ai_thread_unlink",
+ "url": "https://api.example.com/ai/sale_order_line/delete",
+ "auth_type": "none",
+ "payload_type": "none",
+ "result_type": "none",
+ "result_kind": "immediate",
+ }
+ )
+
+ def test_sale_order_create_bridge(self):
+ with mock.patch("requests.post") as mock_post:
+ mock_post.return_value.status_code = 200
+ mock_post.return_value.json.return_value = {"message": "Sale order created"}
+ self.assertEqual(
+ 0,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_create.id)]
+ ),
+ )
+ sale_order = self.env["sale.order"].create(
+ {
+ "partner_id": self.partner.id,
+ "order_line": [
+ (
+ 0,
+ 0,
+ {
+ "product_id": self.product.id,
+ "product_uom_qty": 1,
+ "price_unit": 100.0,
+ },
+ )
+ ],
+ }
+ )
+ executions = self.env["ai.bridge.execution"].search(
+ [("ai_bridge_id", "=", self.bridge_sale_order_create.id)]
+ )
+ self.assertEqual(len(executions), 1)
+ args, kwargs = mock_post.call_args
+ self.assertEqual(args[0], "https://api.example.com/ai/sale_order/create")
+ record = kwargs["json"].get("record", {})
+ self.assertEqual(record.get("id"), sale_order.id)
+ partner_id = record.get("partner_id")
+ if isinstance(partner_id, list):
+ partner_id = partner_id[0]
+ self.assertEqual(partner_id, self.partner.id)
+
+ def test_sale_order_write_bridge(self):
+ self.bridge_sale_order_create.active = False
+ sale_order = self.env["sale.order"].create(
+ {
+ "partner_id": self.partner.id,
+ "order_line": [
+ (
+ 0,
+ 0,
+ {
+ "product_id": self.product.id,
+ "product_uom_qty": 1,
+ "price_unit": 100.0,
+ },
+ )
+ ],
+ }
+ )
+ self.bridge_sale_order_create.active = True
+ with mock.patch("requests.post") as mock_post:
+ mock_post.return_value.status_code = 200
+ mock_post.return_value.json.return_value = {"message": "Sale order updated"}
+ self.assertEqual(
+ 0,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_write.id)]
+ ),
+ )
+ sale_order.write(
+ {
+ "state": "sale",
+ }
+ )
+ executions = self.env["ai.bridge.execution"].search(
+ [("ai_bridge_id", "=", self.bridge_sale_order_write.id)]
+ )
+ self.assertEqual(len(executions), 1)
+ args, kwargs = mock_post.call_args
+ self.assertEqual(args[0], "https://api.example.com/ai/sale_order/update")
+ record = kwargs["json"].get("record", {})
+ self.assertEqual(record.get("id"), sale_order.id)
+ self.assertEqual(record.get("state"), "sale")
+
+ def test_sale_order_unlink_bridge(self):
+ self.bridge_sale_order_create.active = False
+ sale_order = self.env["sale.order"].create(
+ {
+ "partner_id": self.partner.id,
+ "order_line": [
+ (
+ 0,
+ 0,
+ {
+ "product_id": self.product.id,
+ "product_uom_qty": 1,
+ "price_unit": 100.0,
+ },
+ )
+ ],
+ }
+ )
+ self.bridge_sale_order_create.active = True
+ sale_order_id = sale_order.id
+ with mock.patch("requests.post") as mock_post:
+ mock_post.return_value.status_code = 200
+ mock_post.return_value.json.return_value = {"message": "Sale order deleted"}
+ self.assertEqual(
+ 0,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_unlink.id)]
+ ),
+ )
+ sale_order.unlink()
+ executions = self.env["ai.bridge.execution"].search(
+ [("ai_bridge_id", "=", self.bridge_sale_order_unlink.id)]
+ )
+ self.assertEqual(len(executions), 1)
+ args, kwargs = mock_post.call_args
+ self.assertEqual(args[0], "https://api.example.com/ai/sale_order/delete")
+ self.assertEqual(kwargs["json"].get("_id", False), sale_order_id)
+
+ def test_sale_order_line_create_bridge(self):
+ sale_order = self.env["sale.order"].create(
+ {
+ "partner_id": self.partner.id,
+ }
+ )
+ with mock.patch("requests.post") as mock_post:
+ mock_post.return_value.status_code = 200
+ mock_post.return_value.json.return_value = {
+ "message": "Sale order line created"
+ }
+ self.assertEqual(
+ 0,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_line_create.id)]
+ ),
+ )
+ order_line = self.env["sale.order.line"].create(
+ {
+ "order_id": sale_order.id,
+ "product_id": self.product.id,
+ "product_uom_qty": 2,
+ "price_unit": 100.0,
+ }
+ )
+ executions = self.env["ai.bridge.execution"].search(
+ [("ai_bridge_id", "=", self.bridge_sale_order_line_create.id)]
+ )
+ self.assertEqual(len(executions), 1)
+ args, kwargs = mock_post.call_args
+ self.assertEqual(
+ args[0], "https://api.example.com/ai/sale_order_line/create"
+ )
+ record = kwargs["json"].get("record", {})
+ self.assertEqual(record.get("id"), order_line.id)
+ product_id = record.get("product_id")
+ if isinstance(product_id, list):
+ product_id = product_id[0]
+ self.assertEqual(product_id, self.product.id)
+
+ def test_sale_order_line_write_bridge(self):
+ sale_order = self.env["sale.order"].create(
+ {
+ "partner_id": self.partner.id,
+ }
+ )
+ self.bridge_sale_order_line_create.active = False
+ order_line = self.env["sale.order.line"].create(
+ {
+ "order_id": sale_order.id,
+ "product_id": self.product.id,
+ "product_uom_qty": 1,
+ "price_unit": 100.0,
+ }
+ )
+ self.bridge_sale_order_line_create.active = True
+ with mock.patch("requests.post") as mock_post:
+ mock_post.return_value.status_code = 200
+ mock_post.return_value.json.return_value = {
+ "message": "Sale order line updated"
+ }
+ self.assertEqual(
+ 0,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_line_write.id)]
+ ),
+ )
+ order_line.write(
+ {
+ "product_uom_qty": 3,
+ }
+ )
+ executions = self.env["ai.bridge.execution"].search(
+ [("ai_bridge_id", "=", self.bridge_sale_order_line_write.id)]
+ )
+ self.assertEqual(len(executions), 1)
+ args, kwargs = mock_post.call_args
+ self.assertEqual(
+ args[0], "https://api.example.com/ai/sale_order_line/update"
+ )
+ record = kwargs["json"].get("record", {})
+ self.assertEqual(record.get("id"), order_line.id)
+ self.assertEqual(record.get("product_uom_qty"), 3)
+
+ def test_sale_order_line_unlink_bridge(self):
+ sale_order = self.env["sale.order"].create(
+ {
+ "partner_id": self.partner.id,
+ }
+ )
+ self.bridge_sale_order_line_create.active = False
+ order_line = self.env["sale.order.line"].create(
+ {
+ "order_id": sale_order.id,
+ "product_id": self.product.id,
+ "product_uom_qty": 1,
+ "price_unit": 100.0,
+ }
+ )
+ self.bridge_sale_order_line_create.active = True
+ order_line_id = order_line.id
+ with mock.patch("requests.post") as mock_post:
+ mock_post.return_value.status_code = 200
+ mock_post.return_value.json.return_value = {
+ "message": "Sale order line deleted"
+ }
+ self.assertEqual(
+ 0,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_line_unlink.id)]
+ ),
+ )
+ order_line.unlink()
+ executions = self.env["ai.bridge.execution"].search(
+ [("ai_bridge_id", "=", self.bridge_sale_order_line_unlink.id)]
+ )
+ self.assertEqual(len(executions), 1)
+ args, kwargs = mock_post.call_args
+ self.assertEqual(
+ args[0], "https://api.example.com/ai/sale_order_line/delete"
+ )
+ self.assertEqual(kwargs["json"].get("_id", False), order_line_id)
+
+ def test_all_sale_order_bridges_together(self):
+ with mock.patch("requests.post") as mock_post:
+ mock_post.return_value.status_code = 200
+ mock_post.return_value.json.return_value = {"message": "Success"}
+ self.assertEqual(
+ 0,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_create.id)]
+ ),
+ )
+ self.assertEqual(
+ 0,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_write.id)]
+ ),
+ )
+ self.assertEqual(
+ 0,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_unlink.id)]
+ ),
+ )
+ sale_order = self.env["sale.order"].create(
+ {
+ "partner_id": self.partner.id,
+ "order_line": [
+ (
+ 0,
+ 0,
+ {
+ "product_id": self.product.id,
+ "product_uom_qty": 1,
+ "price_unit": 100.0,
+ },
+ )
+ ],
+ }
+ )
+ sale_order.write(
+ {
+ "state": "sale",
+ }
+ )
+
+ self.assertEqual(
+ 1,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_create.id)]
+ ),
+ )
+ self.assertEqual(
+ 1,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_write.id)]
+ ),
+ )
+
+ def test_all_sale_order_line_bridges_together(self):
+ sale_order = self.env["sale.order"].create(
+ {
+ "partner_id": self.partner.id,
+ }
+ )
+ with mock.patch("requests.post") as mock_post:
+ mock_post.return_value.status_code = 200
+ mock_post.return_value.json.return_value = {"message": "Success"}
+ self.assertEqual(
+ 0,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_line_create.id)]
+ ),
+ )
+ self.assertEqual(
+ 0,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_line_write.id)]
+ ),
+ )
+ self.assertEqual(
+ 0,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_line_unlink.id)]
+ ),
+ )
+ order_line = self.env["sale.order.line"].create(
+ {
+ "order_id": sale_order.id,
+ "product_id": self.product.id,
+ "product_uom_qty": 1,
+ "price_unit": 100.0,
+ }
+ )
+ order_line.write(
+ {
+ "product_uom_qty": 2,
+ }
+ )
+ order_line.unlink()
+
+ self.assertEqual(
+ 1,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_line_create.id)]
+ ),
+ )
+ self.assertEqual(
+ 1,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_line_write.id)]
+ ),
+ )
+ self.assertEqual(
+ 1,
+ self.env["ai.bridge.execution"].search_count(
+ [("ai_bridge_id", "=", self.bridge_sale_order_line_unlink.id)]
+ ),
+ )
diff --git a/setup/ai_oca_bridge_sale/odoo/addons/ai_oca_bridge_sale b/setup/ai_oca_bridge_sale/odoo/addons/ai_oca_bridge_sale
new file mode 120000
index 0000000..88437de
--- /dev/null
+++ b/setup/ai_oca_bridge_sale/odoo/addons/ai_oca_bridge_sale
@@ -0,0 +1 @@
+../../../../ai_oca_bridge_sale
\ No newline at end of file
diff --git a/setup/ai_oca_bridge_sale/setup.py b/setup/ai_oca_bridge_sale/setup.py
new file mode 100644
index 0000000..28c57bb
--- /dev/null
+++ b/setup/ai_oca_bridge_sale/setup.py
@@ -0,0 +1,6 @@
+import setuptools
+
+setuptools.setup(
+ setup_requires=['setuptools-odoo'],
+ odoo_addon=True,
+)