From 7af6ed82bf1f0f22a4ccc9c500b290105dd5b749 Mon Sep 17 00:00:00 2001 From: "robin.keunen" Date: Tue, 2 Jun 2020 15:59:46 +0200 Subject: [PATCH 01/21] [ADD] easy_my_coop_connector --- easy_my_coop_api/models/external_id_mixin.py | 30 +- easy_my_coop_connector/README.rst | 65 +++ easy_my_coop_connector/__init__.py | 1 + easy_my_coop_connector/__manifest__.py | 26 ++ easy_my_coop_connector/demo/demo.xml | 24 + easy_my_coop_connector/models/__init__.py | 3 + easy_my_coop_connector/models/emc_backend.py | 84 ++++ easy_my_coop_connector/models/emc_bindings.py | 48 ++ .../models/subscription_request.py | 73 +++ .../models/subscription_request_adapter.py | 92 ++++ .../readme/CONTRIBUTORS.rst | 2 + easy_my_coop_connector/readme/DESCRIPTION.rst | 1 + easy_my_coop_connector/readme/ROADMAP.rst | 0 easy_my_coop_connector/readme/USAGE.rst | 22 + .../security/ir.model.access.csv | 4 + .../static/description/index.html | 423 ++++++++++++++++++ easy_my_coop_connector/tests/__init__.py | 1 + .../tests/test_subscription_request.py | 101 +++++ easy_my_coop_connector/views/actions.xml | 25 ++ easy_my_coop_connector/views/emc_backend.xml | 47 ++ easy_my_coop_connector/views/emc_bindings.xml | 62 +++ easy_my_coop_connector/views/menus.xml | 35 ++ 22 files changed, 1165 insertions(+), 4 deletions(-) create mode 100644 easy_my_coop_connector/README.rst create mode 100644 easy_my_coop_connector/__init__.py create mode 100644 easy_my_coop_connector/__manifest__.py create mode 100644 easy_my_coop_connector/demo/demo.xml create mode 100644 easy_my_coop_connector/models/__init__.py create mode 100644 easy_my_coop_connector/models/emc_backend.py create mode 100644 easy_my_coop_connector/models/emc_bindings.py create mode 100644 easy_my_coop_connector/models/subscription_request.py create mode 100644 easy_my_coop_connector/models/subscription_request_adapter.py create mode 100644 easy_my_coop_connector/readme/CONTRIBUTORS.rst create mode 100644 easy_my_coop_connector/readme/DESCRIPTION.rst create mode 100644 easy_my_coop_connector/readme/ROADMAP.rst create mode 100644 easy_my_coop_connector/readme/USAGE.rst create mode 100644 easy_my_coop_connector/security/ir.model.access.csv create mode 100644 easy_my_coop_connector/static/description/index.html create mode 100644 easy_my_coop_connector/tests/__init__.py create mode 100644 easy_my_coop_connector/tests/test_subscription_request.py create mode 100644 easy_my_coop_connector/views/actions.xml create mode 100644 easy_my_coop_connector/views/emc_backend.xml create mode 100644 easy_my_coop_connector/views/emc_bindings.xml create mode 100644 easy_my_coop_connector/views/menus.xml diff --git a/easy_my_coop_api/models/external_id_mixin.py b/easy_my_coop_api/models/external_id_mixin.py index cd9193e..91e3b9b 100644 --- a/easy_my_coop_api/models/external_id_mixin.py +++ b/easy_my_coop_api/models/external_id_mixin.py @@ -2,6 +2,8 @@ # Robin Keunen # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from psycopg2 import IntegrityError + from odoo import api, fields, models @@ -9,6 +11,14 @@ class ExternalIdMixin(models.AbstractModel): _name = "external.id.mixin" _description = "External ID Mixin" + _sql_constraints = [ + ( + "_api_external_id_uniq", + "unique(_api_external_id)", + "API External ID must be unique!", + ) + ] + # do not access directly, always use get_api_external_id method _api_external_id = fields.Integer( string="External ID", index=True, required=False @@ -28,7 +38,7 @@ class ExternalIdMixin(models.AbstractModel): sequence = Sequence.search([("code", "=", code)]) if not sequence: sequence = Sequence.sudo().create( - {"name": code, "code": code, "number_next": 1} + {"name": code, "code": code, "number_next": 100} ) self.sudo().write({"external_id_sequence_id": sequence.id}) @@ -40,9 +50,21 @@ class ExternalIdMixin(models.AbstractModel): if not self.external_id_sequence_id: self.set_external_sequence() if not self._api_external_id: - self.sudo().write( - {"_api_external_id": self.external_id_sequence_id._next()} - ) + # pass already allocated ids + n = 100 + while True: + try: + self.sudo().write( + { + "_api_external_id": self.external_id_sequence_id._next() + } + ) + break + except IntegrityError as e: + if n > 0: + continue + else: + raise e return self._api_external_id diff --git a/easy_my_coop_connector/README.rst b/easy_my_coop_connector/README.rst new file mode 100644 index 0000000..d13f5ed --- /dev/null +++ b/easy_my_coop_connector/README.rst @@ -0,0 +1,65 @@ +====================== +Easy My Coop Connector +====================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-coopiteasy%2Fvertical--cooperative-lightgray.png?logo=github + :target: https://github.com/coopiteasy/vertical-cooperative/tree/12.0/easy_my_coop_connector + :alt: coopiteasy/vertical-cooperative + +|badge1| |badge2| |badge3| + + Connect to Easy My Coop RESTful API. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Get an API token from the Eeasy My Copp API you wish to connect to +and setup a backend at +- Settings > Technical (debug mode) > EMC Backend + +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 smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Coop IT Easy SCRLfs + +Contributors +~~~~~~~~~~~~ + +* Coop IT Easy SCRLfs +* Robin Keunen + +Maintainers +~~~~~~~~~~~ + +This module is part of the `coopiteasy/vertical-cooperative `_ project on GitHub. + +You are welcome to contribute. diff --git a/easy_my_coop_connector/__init__.py b/easy_my_coop_connector/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/easy_my_coop_connector/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/easy_my_coop_connector/__manifest__.py b/easy_my_coop_connector/__manifest__.py new file mode 100644 index 0000000..e023e33 --- /dev/null +++ b/easy_my_coop_connector/__manifest__.py @@ -0,0 +1,26 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +{ + "name": "Easy My Coop Connector", + "version": "12.0.0.0.1", + "depends": ["easy_my_coop"], + "author": "Coop IT Easy SCRLfs", + "category": "Connector", + "website": "www.coopiteasy.be", + "license": "AGPL-3", + "summary": """ + Connect to Easy My Coop RESTful API. + """, + "data": [ + "security/ir.model.access.csv", + "views/emc_backend.xml", + "views/emc_bindings.xml", + "views/actions.xml", + "views/menus.xml", + ], + "demo": ["demo/demo.xml"], + "installable": True, + "application": False, +} diff --git a/easy_my_coop_connector/demo/demo.xml b/easy_my_coop_connector/demo/demo.xml new file mode 100644 index 0000000..cea2a26 --- /dev/null +++ b/easy_my_coop_connector/demo/demo.xml @@ -0,0 +1,24 @@ + + + + + IWP backend + http://localhost:9876 + cbd07f57-c903-43b4-b668-436b3bec5f15 + + + + + + 37 + + + + + + 38 + + diff --git a/easy_my_coop_connector/models/__init__.py b/easy_my_coop_connector/models/__init__.py new file mode 100644 index 0000000..f5c72d9 --- /dev/null +++ b/easy_my_coop_connector/models/__init__.py @@ -0,0 +1,3 @@ +from . import emc_backend +from . import emc_bindings +from . import subscription_request diff --git a/easy_my_coop_connector/models/emc_backend.py b/easy_my_coop_connector/models/emc_backend.py new file mode 100644 index 0000000..2bf1f4d --- /dev/null +++ b/easy_my_coop_connector/models/emc_backend.py @@ -0,0 +1,84 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import json +import logging + +import requests +from werkzeug.exceptions import NotFound + +from odoo import _, api, fields, models + +_logger = logging.getLogger(__name__) + + +class EMCBackend(models.Model): + _name = "emc.backend" + _description = "EMC Backend" + + name = fields.Char(string="Name", required=True) + location = fields.Char(string="Location") + api_key = fields.Char(string="API Key") + description = fields.Text(string="Description", required=False) + active = fields.Boolean(string="active", default=True) + + @api.multi + def http_get(self, url, params=None, headers=None): + self.ensure_one() + headers = self._add_api_key(headers) + if url.startswith("/"): + url = self.location + url + + return requests.get(url, params=params, headers=headers) + + @api.multi + def http_get_content(self, url, params=None, headers=None): + self.ensure_one() + response = self.http_get(url, params=params, headers=headers) + + if response.status_code == 200: + content = response.content.decode("utf-8") + return json.loads(content) + else: + # todo handle different codes (at least 404, 403, 500) + raise NotFound( + _("request returned status code %s" % response.status_code) + ) + + @api.multi + def http_post(self, url, data, headers=None): + self.ensure_one() + headers = self._add_api_key(headers) + if url.startswith("/"): + url = self.location + url + + return requests.post(url, json=data, headers=headers) + + @api.multi + def _add_api_key(self, headers): + self.ensure_one() + key_dict = {"API-KEY": self.api_key} + if headers: + headers.update(key_dict) + else: + headers = key_dict + return headers + + @api.multi + def action_ping(self): + self.ensure_one() + url = self.location + "/api/ping/test" + try: + response = requests.get(url) + except Exception as e: + _logger.error(e) + raise Warning(_("Failed to connect to backend: %s" % str(e))) + + if response.status_code == 200: + content = json.loads(response.content.decode("utf-8")) + raise Warning(_("Success: %s") % content["message"]) + else: + raise Warning( + _("Failed to connect to backend: %s" % str(response.content)) + ) diff --git a/easy_my_coop_connector/models/emc_bindings.py b/easy_my_coop_connector/models/emc_bindings.py new file mode 100644 index 0000000..c1e9981 --- /dev/null +++ b/easy_my_coop_connector/models/emc_bindings.py @@ -0,0 +1,48 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class EMCBinding(models.AbstractModel): + _name = "emc.binding" + _description = "EMC Binding (abstract)" + + backend_id = fields.Many2one( + comodel_name="emc.backend", string="EMC Backend", ondelete="restrict" + ) + external_id = fields.Integer(string="ID in Platform", index=True) + # odoo_id = fields.Many2one # implement in concrete class + + @api.model + def search_binding(self, backend, external_id): + return self.search( + [ + ("backend_id", "=", backend.id), + ("external_id", "=", external_id), + ] + ) + + +class SubscriptionRequestBinding(models.Model): + _name = "emc.binding.subscription.request" + _inherit = "emc.binding" + + internal_id = fields.Many2one( + comodel_name="subscription.request", + string="Internal ID", + required=True, + ) + + +class ProductTemplateBinding(models.Model): + _name = "emc.binding.product.template" + _inherit = "emc.binding" + + internal_id = fields.Many2one( + comodel_name="product.template", + string="Internal ID", + domain="[('is_share', '=', True)]", + required=True, + ) diff --git a/easy_my_coop_connector/models/subscription_request.py b/easy_my_coop_connector/models/subscription_request.py new file mode 100644 index 0000000..0d63953 --- /dev/null +++ b/easy_my_coop_connector/models/subscription_request.py @@ -0,0 +1,73 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + +from .subscription_request_adapter import SubscriptionRequestAdapter + + +class SubscriptionRequest(models.Model): + _inherit = "subscription.request" + + source = fields.Selection(selection_add=[("emc_api", "Easy My Coop API")]) + binding_id = fields.One2many( + comodel_name="emc.binding.subscription.request", + inverse_name="internal_id", + string="Binding ID", + required=False, + ) + + @api.model + def fetch_subscription_requests(self, date_from=None, date_to=None): + SRBinding = self.env["emc.binding.subscription.request"] + + backend = self.env["emc.backend"].search([("active", "=", True)]) + backend.ensure_one() + + adapter = SubscriptionRequestAdapter(backend=backend) + requests_dict = adapter.search(date_from=date_from, date_to=date_to) + for request_dict in requests_dict["rows"]: + external_id = request_dict["id"] + request_values = adapter.to_write_values(request_dict) + sr_binding = SRBinding.search_binding(backend, external_id) + if sr_binding: # update request + sr_binding.internal_id.write(request_values) + else: + srequest = self.env["subscription.request"].create( + request_values + ) + SRBinding.create( + { + "backend_id": backend.id, + "external_id": external_id, + "internal_id": srequest.id, + } + ) + + @api.model + def backend_read(self, external_id): + SRBinding = self.env["emc.binding.subscription.request"] + + backend = self.env["emc.backend"].search([("active", "=", True)]) + backend.ensure_one() + + adapter = SubscriptionRequestAdapter(backend) + sr_data = adapter.read(external_id) + # todo 404 + + request_values = adapter.to_write_values(sr_data) + sr_binding = SRBinding.search_binding(backend, external_id) + + if sr_binding: # update request + srequest = sr_binding.internal_id.write(request_values) + else: + srequest = self.env["subscription.request"].create(request_values) + SRBinding.create( + { + "backend_id": backend.id, + "external_id": external_id, + "internal_id": srequest.id, + } + ) + return srequest diff --git a/easy_my_coop_connector/models/subscription_request_adapter.py b/easy_my_coop_connector/models/subscription_request_adapter.py new file mode 100644 index 0000000..2987f5a --- /dev/null +++ b/easy_my_coop_connector/models/subscription_request_adapter.py @@ -0,0 +1,92 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from os.path import join + +from odoo import _ +from odoo.exceptions import UserError +from odoo.fields import Date + + +class SubscriptionRequestAdapter: + _model = "subscription.request" + _root = "api" + _service = "subscription-request" + + def __init__(self, backend): + self.backend = backend + + def get_url(self, args): + """args is a list of path elements + :return the complete route to the service + """ + return join("/", self._root, self._service, *args) + + def create(self): + # pylint: disable=method-required-super + raise NotImplementedError + + def search(self, date_from=None, date_to=None): + url = self.get_url([]) + params = {} + if date_from: + params.update({"date_from": Date.to_string(date_from)}) + if date_to: + params.update({"date_to": Date.to_string(date_to)}) + + sr_list = self.backend.http_get_content(url, params=params) + return sr_list + + def read(self, id_): + # pylint: disable=method-required-super + url = self.get_url([str(id_)]) + sr = self.backend.http_get_content(url) + return sr + + def update(self): + raise NotImplementedError + + def delete(self): + raise NotImplementedError + + def to_write_values(self, request): + """ + :return a writable dictionary of values from the dictionary + received from the api + """ + address = request["address"] + + Country = self.backend.env["res.country"] + country = Country.search([("code", "=", address["country"])]) + + ProductTemplateBinding = self.backend.env[ + "emc.binding.product.template" + ] + external_product_id = request["share_product"]["id"] + share_product_binding = ProductTemplateBinding.search_binding( + self.backend, external_product_id + ) + if not share_product_binding: + raise UserError( + _( + "No binding exists for share product %s. Please contact " + "system administrator " + ) + % request["share_product"]["name"] + ) + + return { + "email": request["email"], + "name": request["name"], + "date": request["date"], + "state": request["state"], + "lang": request["lang"], + "ordered_parts": request["ordered_parts"], + "address": address["street"], + "zip_code": address["zip_code"], + "city": address["city"], + "country_id": country.id, + "share_product_id": share_product_binding.internal_id.id, + "source": "emc_api", + } diff --git a/easy_my_coop_connector/readme/CONTRIBUTORS.rst b/easy_my_coop_connector/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..f8672e6 --- /dev/null +++ b/easy_my_coop_connector/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Coop IT Easy SCRLfs +* Robin Keunen diff --git a/easy_my_coop_connector/readme/DESCRIPTION.rst b/easy_my_coop_connector/readme/DESCRIPTION.rst new file mode 100644 index 0000000..f462c6d --- /dev/null +++ b/easy_my_coop_connector/readme/DESCRIPTION.rst @@ -0,0 +1 @@ + Connect to Easy My Coop RESTful API. diff --git a/easy_my_coop_connector/readme/ROADMAP.rst b/easy_my_coop_connector/readme/ROADMAP.rst new file mode 100644 index 0000000..e69de29 diff --git a/easy_my_coop_connector/readme/USAGE.rst b/easy_my_coop_connector/readme/USAGE.rst new file mode 100644 index 0000000..82cc370 --- /dev/null +++ b/easy_my_coop_connector/readme/USAGE.rst @@ -0,0 +1,22 @@ +## Configuration + +Get an API token from the Easy My Coop API you wish to connect to +and setup a backend at +- Settings > Technical (debug mode) > EMC Backend + +## Concepts + +The models are heavily influenced by the odoo connector: + +- emc_backend + - connects to the API, sends HTTP requests, converts json to dictionaries +- _adapter + - uses the backend to send query for model it's responsible for. + - adapts the objects to api format dictionaries + - adapts the result dictionaries to writable dictionaries +- _binding + - links an internal record with an external record for each backend + +The is responsible for the orchestration of these components. + +In the current implementation, only one backend is allowed. diff --git a/easy_my_coop_connector/security/ir.model.access.csv b/easy_my_coop_connector/security/ir.model.access.csv new file mode 100644 index 0000000..5dc68a2 --- /dev/null +++ b/easy_my_coop_connector/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_emc_backend_administrator,access_emc_backend_administrator,model_emc_backend,base.group_system,1,1,1,1 +access_emc_binding_subscription_request_administrator,access_emc_binding_subscription_request_administrator,model_emc_binding_subscription_request,base.group_system,1,1,1,1 +access_emc_binding_product_template_administrator,access_emc_binding_product_template_administrator,model_emc_binding_product_template,base.group_system,1,1,1,1 diff --git a/easy_my_coop_connector/static/description/index.html b/easy_my_coop_connector/static/description/index.html new file mode 100644 index 0000000..3f77927 --- /dev/null +++ b/easy_my_coop_connector/static/description/index.html @@ -0,0 +1,423 @@ + + + + + + +Easy My Coop Connector + + + +
+

Easy My Coop Connector

+ + +

Beta License: AGPL-3 coopiteasy/vertical-cooperative

+
+Connect to Easy My Coop RESTful API.
+

Table of contents

+ +
+

Usage

+

Get an API token from the Eeasy My Copp API you wish to connect to +and setup a backend at +- Settings > Technical (debug mode) > EMC Backend

+
+
+

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 smashing it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Coop IT Easy SCRLfs
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the coopiteasy/vertical-cooperative project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/easy_my_coop_connector/tests/__init__.py b/easy_my_coop_connector/tests/__init__.py new file mode 100644 index 0000000..4346b2f --- /dev/null +++ b/easy_my_coop_connector/tests/__init__.py @@ -0,0 +1 @@ +from . import test_subscription_request diff --git a/easy_my_coop_connector/tests/test_subscription_request.py b/easy_my_coop_connector/tests/test_subscription_request.py new file mode 100644 index 0000000..c61a196 --- /dev/null +++ b/easy_my_coop_connector/tests/test_subscription_request.py @@ -0,0 +1,101 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +import datetime +import json +from unittest.mock import Mock, patch + +import requests + +from odoo.tests.common import TransactionCase + +NOT_FOUND_ERROR = {"name": "Not Found", "code": 404} +FORBIDDEN_ERROR = {"name": "Forbidden", "code": 403} +SERVER_ERROR = {"name": "Server Error", "code": 500} +NO_RESULT = {"count": 0, "rows": []} +SEARCH_RESULT = { + "count": 1, + "rows": [ + { + "id": 1, + "date": "2020-05-14", + "email": "manuel@demo.net", + "address": { + "city": "Brussels", + "street": "schaerbeekstraat", + "zip_code": "1111", + "country": "BE", + }, + "lang": "en_US", + "ordered_parts": 3, + "name": "Manuel Dublues", + "share_product": {"name": "Part B - Worker", "id": 38}, + "state": "draft", + } + ], +} +GET_RESULT = { + "id": 1, + "name": "Robin Des Bois", + "date": "2020-05-14", + "email": "manuel@demo.net", + "address": { + "city": "Brussels", + "street": "schaerbeekstraat", + "zip_code": "1111", + "country": "BE", + }, + "lang": "en_US", + "ordered_parts": 3, + "share_product": {"name": "Part B - Worker", "id": 38}, + "state": "draft", +} + + +def dict_to_dump(content): + return json.dumps(content).encode("utf-8") + + +class TestCase(TransactionCase): + def setUp(self): + super().setUp() + self.backend = self.browse_ref( + "easy_my_coop_connector.emc_backend_demo" + ) + + def test_search_requests(self): + SubscriptionRequest = self.env["subscription.request"] + SRBinding = self.env["emc.binding.subscription.request"] + + date_to = datetime.date.today() + date_from = date_to - datetime.timedelta(days=1) + + with patch.object(requests, "get") as mock_get: + mock_get.return_value = mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = dict_to_dump(SEARCH_RESULT) + + SubscriptionRequest.fetch_subscription_requests( + date_from=date_from, date_to=date_to + ) + + external_id = 1 + binding = SRBinding.search_binding(self.backend, external_id) + self.assertTrue( + bool(binding), + "no binding created when searching subscription requests", + ) + + srequest = binding.internal_id + self.assertEquals(srequest.name, "Manuel Dublues") + + with patch.object(requests, "get") as mock_get: + mock_get.return_value = mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = dict_to_dump(GET_RESULT) + + SubscriptionRequest.backend_read(external_id) + + self.assertEquals(srequest.name, "Robin Des Bois") diff --git a/easy_my_coop_connector/views/actions.xml b/easy_my_coop_connector/views/actions.xml new file mode 100644 index 0000000..97e2390 --- /dev/null +++ b/easy_my_coop_connector/views/actions.xml @@ -0,0 +1,25 @@ + + + + + + EMC Backend + emc.backend + tree,form + + + + Subscription Request Bindings + emc.binding.subscription.request + tree,form + + + + Share Product Bindings + emc.binding.product.template + tree,form + + diff --git a/easy_my_coop_connector/views/emc_backend.xml b/easy_my_coop_connector/views/emc_backend.xml new file mode 100644 index 0000000..27e52d8 --- /dev/null +++ b/easy_my_coop_connector/views/emc_backend.xml @@ -0,0 +1,47 @@ + + + + + emc_backend_view_form + emc.backend + +
+ +
+ +
+ + + + + +
+
+
+
+
+
+ + + emc_backend_view_tree + emc.backend + + + + + + + + +
diff --git a/easy_my_coop_connector/views/emc_bindings.xml b/easy_my_coop_connector/views/emc_bindings.xml new file mode 100644 index 0000000..7d92357 --- /dev/null +++ b/easy_my_coop_connector/views/emc_bindings.xml @@ -0,0 +1,62 @@ + + + + + emc_binding_subscription_request_view_form + emc.binding.subscription.request + +
+ + + + + + + +
+
+
+ + + emc_binding_subscription_request_view_tree + emc.binding.subscription.request + + + + + + + + + + + emc_binding_product_template_view_form + emc.binding.product.template + +
+ + + + + + + +
+
+
+ + + emc_binding_product_template_view_tree + emc.binding.product.template + + + + + + + + +
diff --git a/easy_my_coop_connector/views/menus.xml b/easy_my_coop_connector/views/menus.xml new file mode 100644 index 0000000..2c15069 --- /dev/null +++ b/easy_my_coop_connector/views/menus.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + From feda2385156491381f9aa8a28f9eebb46cbd8c31 Mon Sep 17 00:00:00 2001 From: "robin.keunen" Date: Tue, 2 Jun 2020 19:07:37 +0200 Subject: [PATCH 02/21] [IMP] emcc: catch 403, 404, 500 return code --- easy_my_coop_connector/demo/demo.xml | 2 +- easy_my_coop_connector/models/emc_backend.py | 21 +++++++++++++++---- .../models/subscription_request.py | 4 ++-- .../tests/test_subscription_request.py | 4 ++-- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/easy_my_coop_connector/demo/demo.xml b/easy_my_coop_connector/demo/demo.xml index cea2a26..5a3a1c7 100644 --- a/easy_my_coop_connector/demo/demo.xml +++ b/easy_my_coop_connector/demo/demo.xml @@ -19,6 +19,6 @@ - 38 + 31 diff --git a/easy_my_coop_connector/models/emc_backend.py b/easy_my_coop_connector/models/emc_backend.py index 2bf1f4d..3079df6 100644 --- a/easy_my_coop_connector/models/emc_backend.py +++ b/easy_my_coop_connector/models/emc_backend.py @@ -6,9 +6,15 @@ import json import logging import requests -from werkzeug.exceptions import NotFound +from werkzeug.exceptions import ( + InternalServerError, + NotFound, +) from odoo import _, api, fields, models +from odoo.exceptions import ( + AccessDenied, +) _logger = logging.getLogger(__name__) @@ -40,10 +46,17 @@ class EMCBackend(models.Model): if response.status_code == 200: content = response.content.decode("utf-8") return json.loads(content) - else: - # todo handle different codes (at least 404, 403, 500) + elif response.status_code == 403: + raise AccessDenied(_("You are not allowed to access this resource")) + elif response.status_code == 404: raise NotFound( - _("request returned status code %s" % response.status_code) + _("Resource not found %s on server" % response.status_code) + ) + else: # 500 et al. + content = response.content.decode("utf-8") + raise InternalServerError( + _("request returned status code %s with message %s" % ( + response.status_code, content)) ) @api.multi diff --git a/easy_my_coop_connector/models/subscription_request.py b/easy_my_coop_connector/models/subscription_request.py index 0d63953..c19281f 100644 --- a/easy_my_coop_connector/models/subscription_request.py +++ b/easy_my_coop_connector/models/subscription_request.py @@ -54,13 +54,13 @@ class SubscriptionRequest(models.Model): adapter = SubscriptionRequestAdapter(backend) sr_data = adapter.read(external_id) - # todo 404 request_values = adapter.to_write_values(sr_data) sr_binding = SRBinding.search_binding(backend, external_id) if sr_binding: # update request - srequest = sr_binding.internal_id.write(request_values) + srequest = sr_binding.internal_id + srequest.write(request_values) else: srequest = self.env["subscription.request"].create(request_values) SRBinding.create( diff --git a/easy_my_coop_connector/tests/test_subscription_request.py b/easy_my_coop_connector/tests/test_subscription_request.py index c61a196..a2b25f5 100644 --- a/easy_my_coop_connector/tests/test_subscription_request.py +++ b/easy_my_coop_connector/tests/test_subscription_request.py @@ -31,7 +31,7 @@ SEARCH_RESULT = { "lang": "en_US", "ordered_parts": 3, "name": "Manuel Dublues", - "share_product": {"name": "Part B - Worker", "id": 38}, + "share_product": {"name": "Part B - Worker", "id": 31}, "state": "draft", } ], @@ -49,7 +49,7 @@ GET_RESULT = { }, "lang": "en_US", "ordered_parts": 3, - "share_product": {"name": "Part B - Worker", "id": 38}, + "share_product": {"name": "Part B - Worker", "id": 31}, "state": "draft", } From 09921c08525ac8648b16603ade4364f541d967b7 Mon Sep 17 00:00:00 2001 From: "robin.keunen" Date: Thu, 4 Jun 2020 11:43:00 +0200 Subject: [PATCH 03/21] [ADD] emcc: manual import wizard --- easy_my_coop_connector/__init__.py | 1 + easy_my_coop_connector/__manifest__.py | 1 + .../models/subscription_request.py | 8 ++++ .../models/subscription_request_adapter.py | 11 ++--- .../tests/test_subscription_request.py | 10 +++++ easy_my_coop_connector/views/menus.xml | 13 ++++-- easy_my_coop_connector/wizards/__init__.py | 1 + .../wizards/emc_history_import_sr.py | 23 ++++++++++ .../wizards/emc_history_import_sr.xml | 42 +++++++++++++++++++ 9 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 easy_my_coop_connector/wizards/__init__.py create mode 100644 easy_my_coop_connector/wizards/emc_history_import_sr.py create mode 100644 easy_my_coop_connector/wizards/emc_history_import_sr.xml diff --git a/easy_my_coop_connector/__init__.py b/easy_my_coop_connector/__init__.py index 0650744..aee8895 100644 --- a/easy_my_coop_connector/__init__.py +++ b/easy_my_coop_connector/__init__.py @@ -1 +1,2 @@ from . import models +from . import wizards diff --git a/easy_my_coop_connector/__manifest__.py b/easy_my_coop_connector/__manifest__.py index e023e33..1518676 100644 --- a/easy_my_coop_connector/__manifest__.py +++ b/easy_my_coop_connector/__manifest__.py @@ -17,6 +17,7 @@ "security/ir.model.access.csv", "views/emc_backend.xml", "views/emc_bindings.xml", + "wizards/emc_history_import_sr.xml", "views/actions.xml", "views/menus.xml", ], diff --git a/easy_my_coop_connector/models/subscription_request.py b/easy_my_coop_connector/models/subscription_request.py index c19281f..f82b7dc 100644 --- a/easy_my_coop_connector/models/subscription_request.py +++ b/easy_my_coop_connector/models/subscription_request.py @@ -44,6 +44,14 @@ class SubscriptionRequest(models.Model): "internal_id": srequest.id, } ) + external_ids = [row["id"] for row in requests_dict["rows"]] + srequests = SRBinding.search( + [ + ("backend_id", "=", backend.id), + ("external_id", "in", external_ids), + ] + ).mapped("internal_id") + return srequests @api.model def backend_read(self, external_id): diff --git a/easy_my_coop_connector/models/subscription_request_adapter.py b/easy_my_coop_connector/models/subscription_request_adapter.py index 2987f5a..d28a182 100644 --- a/easy_my_coop_connector/models/subscription_request_adapter.py +++ b/easy_my_coop_connector/models/subscription_request_adapter.py @@ -55,14 +55,14 @@ class SubscriptionRequestAdapter: :return a writable dictionary of values from the dictionary received from the api """ - address = request["address"] - Country = self.backend.env["res.country"] - country = Country.search([("code", "=", address["country"])]) - ProductTemplateBinding = self.backend.env[ "emc.binding.product.template" ] + address = request["address"] + + country = Country.search([("code", "=", address["country"])]) + external_product_id = request["share_product"]["id"] share_product_binding = ProductTemplateBinding.search_binding( self.backend, external_product_id @@ -75,6 +75,7 @@ class SubscriptionRequestAdapter: ) % request["share_product"]["name"] ) + product_product = share_product_binding.internal_id.product_variant_id return { "email": request["email"], @@ -87,6 +88,6 @@ class SubscriptionRequestAdapter: "zip_code": address["zip_code"], "city": address["city"], "country_id": country.id, - "share_product_id": share_product_binding.internal_id.id, + "share_product_id": product_product.id, "source": "emc_api", } diff --git a/easy_my_coop_connector/tests/test_subscription_request.py b/easy_my_coop_connector/tests/test_subscription_request.py index a2b25f5..65f4931 100644 --- a/easy_my_coop_connector/tests/test_subscription_request.py +++ b/easy_my_coop_connector/tests/test_subscription_request.py @@ -64,6 +64,10 @@ class TestCase(TransactionCase): self.backend = self.browse_ref( "easy_my_coop_connector.emc_backend_demo" ) + self.share_type_B_pt = self.browse_ref( + "easy_my_coop.product_template_share_type_2_demo" + ) + self.share_type_B_pp = self.share_type_B_pt.product_variant_id def test_search_requests(self): SubscriptionRequest = self.env["subscription.request"] @@ -90,6 +94,12 @@ class TestCase(TransactionCase): srequest = binding.internal_id self.assertEquals(srequest.name, "Manuel Dublues") + self.assertEquals( + srequest.share_product_id.id, self.share_type_B_pp.id + ) + self.assertEquals( + srequest.subscription_amount, self.share_type_B_pt.list_price * 3 + ) with patch.object(requests, "get") as mock_get: mock_get.return_value = mock_response = Mock() diff --git a/easy_my_coop_connector/views/menus.xml b/easy_my_coop_connector/views/menus.xml index 2c15069..47c5a00 100644 --- a/easy_my_coop_connector/views/menus.xml +++ b/easy_my_coop_connector/views/menus.xml @@ -17,19 +17,26 @@ parent="emc_connector_menu_menu" action="emc_backend_action" groups="base.group_user" - sequence="1000"/> + sequence="1010"/> + sequence="1020"/> + sequence="1030"/> + + diff --git a/easy_my_coop_connector/wizards/__init__.py b/easy_my_coop_connector/wizards/__init__.py new file mode 100644 index 0000000..f22bdbc --- /dev/null +++ b/easy_my_coop_connector/wizards/__init__.py @@ -0,0 +1 @@ +from . import emc_history_import_sr diff --git a/easy_my_coop_connector/wizards/emc_history_import_sr.py b/easy_my_coop_connector/wizards/emc_history_import_sr.py new file mode 100644 index 0000000..70018f7 --- /dev/null +++ b/easy_my_coop_connector/wizards/emc_history_import_sr.py @@ -0,0 +1,23 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class EMCHistoryImportSR(models.TransientModel): + _name = "emc.history.import.sr" + _description = "emc.history.import.sr" + + name = fields.Char("Name", default="Import History") + date_from = fields.Date(string="Date From", required=True) + date_to = fields.Date(string="Date To", required=True) + + @api.multi + def import_subscription_button(self): + self.env["subscription.request"].fetch_subscription_requests( + date_from=self.date_from, date_to=self.date_to + ) + + action = self.env.ref("easy_my_coop.subscription_request_action") + return action.read()[0] diff --git a/easy_my_coop_connector/wizards/emc_history_import_sr.xml b/easy_my_coop_connector/wizards/emc_history_import_sr.xml new file mode 100644 index 0000000..8035124 --- /dev/null +++ b/easy_my_coop_connector/wizards/emc_history_import_sr.xml @@ -0,0 +1,42 @@ + + + + + emc_history_import_sr_view_form + emc.history.import.sr + +
+ + + + + +
+
+
+
+
+
+ + + Import Subscription Request History + ir.actions.act_window + emc.history.import.sr + form + + + + + + + + + +
+
From 3bf12d7bf4c4b7721105cadc905eb6733f190885 Mon Sep 17 00:00:00 2001 From: "robin.keunen" Date: Thu, 4 Jun 2020 11:52:11 +0200 Subject: [PATCH 04/21] [ADD] emcc: cron to fetch requests --- easy_my_coop_connector/__manifest__.py | 1 + easy_my_coop_connector/data/cron.xml | 19 +++++++++++++ .../models/subscription_request.py | 28 +++++++++++++++++++ 3 files changed, 48 insertions(+) create mode 100644 easy_my_coop_connector/data/cron.xml diff --git a/easy_my_coop_connector/__manifest__.py b/easy_my_coop_connector/__manifest__.py index 1518676..fbd1e70 100644 --- a/easy_my_coop_connector/__manifest__.py +++ b/easy_my_coop_connector/__manifest__.py @@ -20,6 +20,7 @@ "wizards/emc_history_import_sr.xml", "views/actions.xml", "views/menus.xml", + "data/cron.xml", ], "demo": ["demo/demo.xml"], "installable": True, diff --git a/easy_my_coop_connector/data/cron.xml b/easy_my_coop_connector/data/cron.xml new file mode 100644 index 0000000..f598445 --- /dev/null +++ b/easy_my_coop_connector/data/cron.xml @@ -0,0 +1,19 @@ + + + + + Fetch Subscription Requests + 1 + days + -1 + + + + code + model.fetch_subscription_requests_cron() + 50 + + diff --git a/easy_my_coop_connector/models/subscription_request.py b/easy_my_coop_connector/models/subscription_request.py index f82b7dc..b09c22d 100644 --- a/easy_my_coop_connector/models/subscription_request.py +++ b/easy_my_coop_connector/models/subscription_request.py @@ -2,10 +2,15 @@ # Robin Keunen # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import logging +from datetime import date, timedelta + from odoo import api, fields, models from .subscription_request_adapter import SubscriptionRequestAdapter +_logger = logging.getLogger(__name__) + class SubscriptionRequest(models.Model): _inherit = "subscription.request" @@ -79,3 +84,26 @@ class SubscriptionRequest(models.Model): } ) return srequest + + @api.model + def fetch_subscription_requests_cron(self): + backend = self.env["emc.backend"].search([("active", "=", True)]) + try: + backend.ensure_one() + except ValueError as e: + _logger.error( + "One and only one backend is allowed for the Easy My Coop " + "connector " + ) + raise e + + date_to = date.today() + date_from = date_to - timedelta(days=1) + _logger.info( + "fetching subscription requests at {backend} from {date_from} to " + "{date_to}.".format( + backend=backend.name, date_from=date_from, date_to=date_to + ) + ) + self.fetch_subscription_requests(date_from=date_from, date_to=date_to) + _logger.info("fetch done.") From dc8b2276d95cfbd3699acb0ca80d62542613e88b Mon Sep 17 00:00:00 2001 From: "robin.keunen" Date: Wed, 12 Aug 2020 17:41:35 +0200 Subject: [PATCH 05/21] [IMP] emca: sr_service.validate returns the new invoice --- .isort.cfg | 2 +- easy_my_coop_api/demo/demo.xml | 9 +++++++++ easy_my_coop_api/models/external_id_mixin.py | 3 ++- .../services/account_invoice_service.py | 8 ++++---- easy_my_coop_api/services/schemas.py | 2 +- .../services/subscription_request_service.py | 7 ++++--- easy_my_coop_api/tests/test_account_invoice.py | 12 ++++++++---- .../tests/test_external_id_mixin.py | 17 +++++++++++++++++ .../tests/test_subscription_requests.py | 2 +- 9 files changed, 47 insertions(+), 15 deletions(-) diff --git a/.isort.cfg b/.isort.cfg index 1517121..22988e0 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -9,4 +9,4 @@ line_length=79 known_odoo=odoo known_odoo_addons=odoo.addons sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER -known_third_party=addons,cStringIO,lxml,openerp,requests,setuptools,werkzeug,xlsxwriter +known_third_party=addons,cStringIO,lxml,openerp,psycopg2,requests,setuptools,werkzeug,xlsxwriter diff --git a/easy_my_coop_api/demo/demo.xml b/easy_my_coop_api/demo/demo.xml index 72d2102..2e15bb0 100644 --- a/easy_my_coop_api/demo/demo.xml +++ b/easy_my_coop_api/demo/demo.xml @@ -16,4 +16,13 @@ 2 + + + 1 + + + + 2 + + diff --git a/easy_my_coop_api/models/external_id_mixin.py b/easy_my_coop_api/models/external_id_mixin.py index 91e3b9b..1e699fa 100644 --- a/easy_my_coop_api/models/external_id_mixin.py +++ b/easy_my_coop_api/models/external_id_mixin.py @@ -54,9 +54,10 @@ class ExternalIdMixin(models.AbstractModel): n = 100 while True: try: + next_id = self.external_id_sequence_id._next() self.sudo().write( { - "_api_external_id": self.external_id_sequence_id._next() + "_api_external_id": next_id } ) break diff --git a/easy_my_coop_api/services/account_invoice_service.py b/easy_my_coop_api/services/account_invoice_service.py index 29216e5..6a39ecf 100644 --- a/easy_my_coop_api/services/account_invoice_service.py +++ b/easy_my_coop_api/services/account_invoice_service.py @@ -27,11 +27,11 @@ class AccountInvoiceService(Component): """ def get(self, _id): - sr = self.env["account.invoice"].search( + ai = self.env["account.invoice"].search( [("_api_external_id", "=", _id)] ) - if sr: - return self._to_dict(sr) + if ai: + return self._to_dict(ai) else: raise wrapJsonException( NotFound(_("No invoice found for id %s") % _id) @@ -42,7 +42,7 @@ class AccountInvoiceService(Component): data = { "id": invoice.get_api_external_id(), - "name": invoice.name, + "number": invoice.number, "state": invoice.state, "type": invoice.type, "date": Date.to_string(invoice.date), diff --git a/easy_my_coop_api/services/schemas.py b/easy_my_coop_api/services/schemas.py index 542c9f0..9f6166d 100644 --- a/easy_my_coop_api/services/schemas.py +++ b/easy_my_coop_api/services/schemas.py @@ -124,7 +124,7 @@ S_INVOICE_LINE_RETURN_GET = { S_INVOICE_RETURN_GET = { "id": {"type": "integer", "required": True}, - "name": {"type": "string", "required": True, "empty": False}, + "number": {"type": "string", "required": True, "empty": False}, "state": {"type": "string", "required": True, "empty": False}, "type": {"type": "string", "required": True, "empty": False}, "date": {"type": "string", "required": True, "empty": False}, diff --git a/easy_my_coop_api/services/subscription_request_service.py b/easy_my_coop_api/services/subscription_request_service.py index 66e3a2e..ed05258 100644 --- a/easy_my_coop_api/services/subscription_request_service.py +++ b/easy_my_coop_api/services/subscription_request_service.py @@ -88,8 +88,9 @@ class SubscriptionRequestService(Component): _("Subscription request %s is not in draft state") % _id ) ) - sr.validate_subscription_request() - return self._to_dict(sr) + invoice = sr.validate_subscription_request() + invoice_service = self.work.component(usage="invoice") + return invoice_service.get(invoice.get_api_external_id()) def _to_dict(self, sr): sr.ensure_one() @@ -219,4 +220,4 @@ class SubscriptionRequestService(Component): return schemas.S_SUBSCRIPTION_REQUEST_VALIDATE def _validator_return_validate(self): - return schemas.S_SUBSCRIPTION_REQUEST_RETURN_GET + return schemas.S_INVOICE_RETURN_GET diff --git a/easy_my_coop_api/tests/test_account_invoice.py b/easy_my_coop_api/tests/test_account_invoice.py index 2eaf902..4919ef5 100644 --- a/easy_my_coop_api/tests/test_account_invoice.py +++ b/easy_my_coop_api/tests/test_account_invoice.py @@ -31,7 +31,7 @@ class TestAccountInvoiceController(BaseEMCRestCase): today = Date.to_string(Date.today()) self.demo_invoice_dict = { "id": 1, - "name": "Capital Release Example", + "number": "xxx", # can't guess it "partner": {"id": 1, "name": "Catherine des Champs"}, "account": {"id": 1, "name": "Cooperators"}, "journal": {"id": 1, "name": "Subscription Journal"}, @@ -79,7 +79,7 @@ class TestAccountInvoiceController(BaseEMCRestCase): self.capital_release = self.env["account.invoice"].create( { - "name": "Capital Release Example", + "number": "Capital Release Example", "partner_id": self.coop_candidate.id, "type": "out_invoice", "invoice_line_ids": capital_release_line, @@ -92,10 +92,14 @@ class TestAccountInvoiceController(BaseEMCRestCase): def test_service_get(self): external_id = self.capital_release.get_api_external_id() result = self.ai_service.get(external_id) - self.assertEquals(self.demo_invoice_dict, result) + expected = self.demo_invoice_dict.copy() + expected["number"] = result["number"] + self.assertEquals(expected, result) def test_route_get(self): external_id = self.capital_release.get_api_external_id() route = "/api/invoice/%s" % external_id content = self.http_get_content(route) - self.assertEquals(self.demo_invoice_dict, content) + expected = self.demo_invoice_dict.copy() + expected["number"] = content["number"] + self.assertEquals(expected, content) diff --git a/easy_my_coop_api/tests/test_external_id_mixin.py b/easy_my_coop_api/tests/test_external_id_mixin.py index 0f7f0a3..242e7a3 100644 --- a/easy_my_coop_api/tests/test_external_id_mixin.py +++ b/easy_my_coop_api/tests/test_external_id_mixin.py @@ -2,6 +2,9 @@ # Robin Keunen # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from psycopg2 import IntegrityError + +import odoo from odoo.fields import Date from odoo.tests import TransactionCase @@ -81,3 +84,17 @@ class TestExternalIdMixin(TransactionCase): self.assertTrue(bool(invoice.external_id_sequence_id)) self.assertEquals(external_id, invoice.get_api_external_id()) + + @odoo.tools.mute_logger("odoo.sql_db") + def test_duplicate_api_external_id_raises(self): + invoice_1 = self.env["account.invoice"].create( + {"name": "create passes"} + ) + external_id = invoice_1.get_api_external_id() + self.assertTrue(bool(invoice_1._api_external_id)) + + invoice_2 = self.env["account.invoice"].create( + {"name": "create passes"} + ) + with self.assertRaises(IntegrityError): + invoice_2._api_external_id = external_id diff --git a/easy_my_coop_api/tests/test_subscription_requests.py b/easy_my_coop_api/tests/test_subscription_requests.py index be1102b..8abb081 100644 --- a/easy_my_coop_api/tests/test_subscription_requests.py +++ b/easy_my_coop_api/tests/test_subscription_requests.py @@ -193,7 +193,7 @@ class TestSRController(BaseEMCRestCase): content = json.loads(response.content.decode("utf-8")) state = content.get("state") - self.assertEquals(state, "done") + self.assertEquals(state, "open") def test_service_validate_draft_request(self): self.sr_service.validate(self.demo_request_1.get_api_external_id()) From b2af3a22b04f40c9a39f94fb53d0339eb9b32d66 Mon Sep 17 00:00:00 2001 From: "robin.keunen" Date: Fri, 5 Jun 2020 16:46:35 +0200 Subject: [PATCH 06/21] [ADD] emcc: send validate request to platform --- easy_my_coop_connector/demo/demo.xml | 6 +++ easy_my_coop_connector/models/__init__.py | 1 + .../models/account_invoice.py | 20 +++++++ easy_my_coop_connector/models/emc_backend.py | 44 +++++++++------ easy_my_coop_connector/models/emc_bindings.py | 11 +++- .../models/subscription_request.py | 53 ++++++++++++++----- .../models/subscription_request_adapter.py | 19 ++++++- .../security/ir.model.access.csv | 1 + .../tests/test_subscription_request.py | 48 ++++++++++++++++- easy_my_coop_connector/views/actions.xml | 6 +++ easy_my_coop_connector/views/emc_bindings.xml | 28 ++++++++++ easy_my_coop_connector/views/menus.xml | 9 +++- 12 files changed, 212 insertions(+), 34 deletions(-) create mode 100644 easy_my_coop_connector/models/account_invoice.py diff --git a/easy_my_coop_connector/demo/demo.xml b/easy_my_coop_connector/demo/demo.xml index 5a3a1c7..eae18ae 100644 --- a/easy_my_coop_connector/demo/demo.xml +++ b/easy_my_coop_connector/demo/demo.xml @@ -21,4 +21,10 @@ 31 + + + emc_api + + + diff --git a/easy_my_coop_connector/models/__init__.py b/easy_my_coop_connector/models/__init__.py index f5c72d9..e786407 100644 --- a/easy_my_coop_connector/models/__init__.py +++ b/easy_my_coop_connector/models/__init__.py @@ -1,3 +1,4 @@ from . import emc_backend from . import emc_bindings +from . import account_invoice from . import subscription_request diff --git a/easy_my_coop_connector/models/account_invoice.py b/easy_my_coop_connector/models/account_invoice.py new file mode 100644 index 0000000..91fa6e7 --- /dev/null +++ b/easy_my_coop_connector/models/account_invoice.py @@ -0,0 +1,20 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class AccountInvoice(models.Model): + _inherit = "account.invoice" + + binding_id = fields.One2many( + comodel_name="emc.binding.account.invoice", + inverse_name="internal_id", + string="Binding ID", + required=False, + ) diff --git a/easy_my_coop_connector/models/emc_backend.py b/easy_my_coop_connector/models/emc_backend.py index 3079df6..61743cb 100644 --- a/easy_my_coop_connector/models/emc_backend.py +++ b/easy_my_coop_connector/models/emc_backend.py @@ -6,15 +6,10 @@ import json import logging import requests -from werkzeug.exceptions import ( - InternalServerError, - NotFound, -) +from werkzeug.exceptions import BadRequest, InternalServerError, NotFound from odoo import _, api, fields, models -from odoo.exceptions import ( - AccessDenied, -) +from odoo.exceptions import AccessDenied _logger = logging.getLogger(__name__) @@ -38,16 +33,22 @@ class EMCBackend(models.Model): return requests.get(url, params=params, headers=headers) - @api.multi - def http_get_content(self, url, params=None, headers=None): - self.ensure_one() - response = self.http_get(url, params=params, headers=headers) - + def _process_response(self, response): if response.status_code == 200: content = response.content.decode("utf-8") return json.loads(content) + elif response.status_code == 400: + content = response.content.decode("utf-8") + raise BadRequest( + _( + "request returned status code %s with message %s" + % (response.status_code, content) + ) + ) elif response.status_code == 403: - raise AccessDenied(_("You are not allowed to access this resource")) + raise AccessDenied( + _("You are not allowed to access this resource") + ) elif response.status_code == 404: raise NotFound( _("Resource not found %s on server" % response.status_code) @@ -55,10 +56,18 @@ class EMCBackend(models.Model): else: # 500 et al. content = response.content.decode("utf-8") raise InternalServerError( - _("request returned status code %s with message %s" % ( - response.status_code, content)) + _( + "request returned status code %s with message %s" + % (response.status_code, content) + ) ) + @api.multi + def http_get_content(self, url, params=None, headers=None): + self.ensure_one() + response = self.http_get(url, params=params, headers=headers) + return self._process_response(response) + @api.multi def http_post(self, url, data, headers=None): self.ensure_one() @@ -68,6 +77,11 @@ class EMCBackend(models.Model): return requests.post(url, json=data, headers=headers) + def http_post_content(self, url, data, headers=None): + self.ensure_one() + response = self.http_post(url, data, headers=headers) + return self._process_response(response) + @api.multi def _add_api_key(self, headers): self.ensure_one() diff --git a/easy_my_coop_connector/models/emc_bindings.py b/easy_my_coop_connector/models/emc_bindings.py index c1e9981..39dec2e 100644 --- a/easy_my_coop_connector/models/emc_bindings.py +++ b/easy_my_coop_connector/models/emc_bindings.py @@ -13,7 +13,7 @@ class EMCBinding(models.AbstractModel): comodel_name="emc.backend", string="EMC Backend", ondelete="restrict" ) external_id = fields.Integer(string="ID in Platform", index=True) - # odoo_id = fields.Many2one # implement in concrete class + # internal_id = fields.Many2one # implement in concrete class @api.model def search_binding(self, backend, external_id): @@ -46,3 +46,12 @@ class ProductTemplateBinding(models.Model): domain="[('is_share', '=', True)]", required=True, ) + + +class AccountInvoiceBinding(models.Model): + _name = "emc.binding.account.invoice" + _inherit = "emc.binding" + + internal_id = fields.Many2one( + comodel_name="account.invoice", string="Internal ID", required=True + ) diff --git a/easy_my_coop_connector/models/subscription_request.py b/easy_my_coop_connector/models/subscription_request.py index b09c22d..2662b9c 100644 --- a/easy_my_coop_connector/models/subscription_request.py +++ b/easy_my_coop_connector/models/subscription_request.py @@ -23,13 +23,24 @@ class SubscriptionRequest(models.Model): required=False, ) + @api.model + def _get_backend(self): + backend = self.env["emc.backend"].search([("active", "=", True)]) + try: + backend.ensure_one() + except ValueError as e: + _logger.error( + "One and only one backend is allowed for the Easy My Coop " + "connector." + ) + raise e + return backend + @api.model def fetch_subscription_requests(self, date_from=None, date_to=None): SRBinding = self.env["emc.binding.subscription.request"] - backend = self.env["emc.backend"].search([("active", "=", True)]) - backend.ensure_one() - + backend = self._get_backend() adapter = SubscriptionRequestAdapter(backend=backend) requests_dict = adapter.search(date_from=date_from, date_to=date_to) for request_dict in requests_dict["rows"]: @@ -62,8 +73,7 @@ class SubscriptionRequest(models.Model): def backend_read(self, external_id): SRBinding = self.env["emc.binding.subscription.request"] - backend = self.env["emc.backend"].search([("active", "=", True)]) - backend.ensure_one() + backend = self._get_backend() adapter = SubscriptionRequestAdapter(backend) sr_data = adapter.read(external_id) @@ -87,15 +97,7 @@ class SubscriptionRequest(models.Model): @api.model def fetch_subscription_requests_cron(self): - backend = self.env["emc.backend"].search([("active", "=", True)]) - try: - backend.ensure_one() - except ValueError as e: - _logger.error( - "One and only one backend is allowed for the Easy My Coop " - "connector " - ) - raise e + backend = self._get_backend() date_to = date.today() date_from = date_to - timedelta(days=1) @@ -107,3 +109,26 @@ class SubscriptionRequest(models.Model): ) self.fetch_subscription_requests(date_from=date_from, date_to=date_to) _logger.info("fetch done.") + + @api.multi + def validate_subscription_request(self): + self.ensure_one() + invoice = super( + SubscriptionRequest, self + ).validate_subscription_request() + + if self.source == "emc_api": + backend = self._get_backend() + sr_adapter = SubscriptionRequestAdapter(backend=backend) + invoice_dict = sr_adapter.validate(self.binding_id.external_id) + + InvoiceBinding = self.env["emc.binding.account.invoice"] + InvoiceBinding.create( + { + "backend_id": backend.id, + "external_id": invoice_dict["id"], + "internal_id": invoice.id, + } + ) + + return invoice diff --git a/easy_my_coop_connector/models/subscription_request_adapter.py b/easy_my_coop_connector/models/subscription_request_adapter.py index d28a182..d312563 100644 --- a/easy_my_coop_connector/models/subscription_request_adapter.py +++ b/easy_my_coop_connector/models/subscription_request_adapter.py @@ -4,8 +4,10 @@ from os.path import join +from werkzeug.exceptions import BadRequest + from odoo import _ -from odoo.exceptions import UserError +from odoo.exceptions import UserError, ValidationError from odoo.fields import Date @@ -50,6 +52,21 @@ class SubscriptionRequestAdapter: def delete(self): raise NotImplementedError + def validate(self, id_): + url = self.get_url([str(id_), "validate"]) + data = {} + try: + invoice_dict = self.backend.http_post_content(url, data) + except BadRequest: + raise ValidationError( + _( + "The request was already validated on the " + "platform. Please check data consistency " + "with your system administrator." + ) + ) + return invoice_dict + def to_write_values(self, request): """ :return a writable dictionary of values from the dictionary diff --git a/easy_my_coop_connector/security/ir.model.access.csv b/easy_my_coop_connector/security/ir.model.access.csv index 5dc68a2..9ce8ad3 100644 --- a/easy_my_coop_connector/security/ir.model.access.csv +++ b/easy_my_coop_connector/security/ir.model.access.csv @@ -2,3 +2,4 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_emc_backend_administrator,access_emc_backend_administrator,model_emc_backend,base.group_system,1,1,1,1 access_emc_binding_subscription_request_administrator,access_emc_binding_subscription_request_administrator,model_emc_binding_subscription_request,base.group_system,1,1,1,1 access_emc_binding_product_template_administrator,access_emc_binding_product_template_administrator,model_emc_binding_product_template,base.group_system,1,1,1,1 +access_emc_binding_account_invoice_administrator,access_emc_binding_account_invoice_administrator,model_emc_binding_account_invoice,base.group_system,1,1,1,1 diff --git a/easy_my_coop_connector/tests/test_subscription_request.py b/easy_my_coop_connector/tests/test_subscription_request.py index 65f4931..c13f788 100644 --- a/easy_my_coop_connector/tests/test_subscription_request.py +++ b/easy_my_coop_connector/tests/test_subscription_request.py @@ -9,7 +9,7 @@ from unittest.mock import Mock, patch import requests -from odoo.tests.common import TransactionCase +from odoo.addons.easy_my_coop.tests.test_base import EMCBaseCase NOT_FOUND_ERROR = {"name": "Not Found", "code": 404} FORBIDDEN_ERROR = {"name": "Forbidden", "code": 403} @@ -53,12 +53,35 @@ GET_RESULT = { "state": "draft", } +VALIDATE_RESULT = { + "id": 9999, + "number": "SUBJ/2020/001", + "date_due": "2020-08-12", + "state": "open", + "date_invoice": "2020-08-12", + "date": "2020-08-12", + "type": "out_invoice", + "subscription_request": {"name": "Manuel Dublues", "id": 1}, + "partner": {"name": "Manuel Dublues", "id": 1}, + "invoice_lines": [ + { + "price_unit": 25.0, + "quantity": 3.0, + "account": {"name": "Product Sales", "id": 2}, + "name": "Part B - Worker", + "product": {"name": "Part B - Worker", "id": 2}, + } + ], + "journal": {"name": "Subscription Journal", "id": 1}, + "account": {"name": "Cooperators", "id": 1}, +} + def dict_to_dump(content): return json.dumps(content).encode("utf-8") -class TestCase(TransactionCase): +class EMCConnectorCase(EMCBaseCase): def setUp(self): super().setUp() self.backend = self.browse_ref( @@ -109,3 +132,24 @@ class TestCase(TransactionCase): SubscriptionRequest.backend_read(external_id) self.assertEquals(srequest.name, "Robin Des Bois") + + def test_validate_request(self): + srequest = self.browse_ref("easy_my_coop.subscription_request_1_demo") + with patch.object(requests, "post") as mock_get: + mock_get.return_value = mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = dict_to_dump(VALIDATE_RESULT) + + srequest.validate_subscription_request() + + self.assertEquals(srequest.state, "done") + + # local invoice created + self.assertTrue(len(srequest.capital_release_request) > 0) + # local invoice linked to external invoice + self.assertEquals( + srequest.capital_release_request.binding_id.external_id, + VALIDATE_RESULT["id"], + ) + + # todo test 400 diff --git a/easy_my_coop_connector/views/actions.xml b/easy_my_coop_connector/views/actions.xml index 97e2390..6790a5c 100644 --- a/easy_my_coop_connector/views/actions.xml +++ b/easy_my_coop_connector/views/actions.xml @@ -22,4 +22,10 @@ emc.binding.product.template tree,form + + + Invoice Bindings + emc.binding.account.invoice + tree,form + diff --git a/easy_my_coop_connector/views/emc_bindings.xml b/easy_my_coop_connector/views/emc_bindings.xml index 7d92357..8d20980 100644 --- a/easy_my_coop_connector/views/emc_bindings.xml +++ b/easy_my_coop_connector/views/emc_bindings.xml @@ -59,4 +59,32 @@ + + + emc_binding_account_invoice_view_form + emc.binding.account.invoice + +
+ + + + + + + +
+
+
+ + + emc_binding_account_invoice_view_tree + emc.binding.account.invoice + + + + + + + + diff --git a/easy_my_coop_connector/views/menus.xml b/easy_my_coop_connector/views/menus.xml index 47c5a00..caac917 100644 --- a/easy_my_coop_connector/views/menus.xml +++ b/easy_my_coop_connector/views/menus.xml @@ -33,10 +33,17 @@ groups="base.group_user" sequence="1030"/> + + + sequence="1100"/> From 9c99e34ed1db422a891a4375c57b12072f1bfc4e Mon Sep 17 00:00:00 2001 From: "robin.keunen" Date: Thu, 13 Aug 2020 15:08:30 +0200 Subject: [PATCH 07/21] [REF] emcc: abstract adapter --- ...ion_request_adapter.py => emc_adapters.py} | 99 ++++++++++++------- .../models/subscription_request.py | 24 ++--- 2 files changed, 76 insertions(+), 47 deletions(-) rename easy_my_coop_connector/models/{subscription_request_adapter.py => emc_adapters.py} (60%) diff --git a/easy_my_coop_connector/models/subscription_request_adapter.py b/easy_my_coop_connector/models/emc_adapters.py similarity index 60% rename from easy_my_coop_connector/models/subscription_request_adapter.py rename to easy_my_coop_connector/models/emc_adapters.py index d312563..654339c 100644 --- a/easy_my_coop_connector/models/subscription_request_adapter.py +++ b/easy_my_coop_connector/models/emc_adapters.py @@ -11,26 +11,55 @@ from odoo.exceptions import UserError, ValidationError from odoo.fields import Date -class SubscriptionRequestAdapter: - _model = "subscription.request" +class AbstractEMCAdapter: + _model = "set in implementation class" _root = "api" - _service = "subscription-request" + _service = "set in implementation class" def __init__(self, backend): self.backend = backend - def get_url(self, args): + def _get_url(self, args): """args is a list of path elements :return the complete route to the service """ return join("/", self._root, self._service, *args) + def search(self, **params): + raise NotImplementedError + + def read(self, id_): + # pylint: disable=method-required-super + url = self._get_url([str(id_)]) + api_dict = self.backend.http_get_content(url) + return self.to_write_values(api_dict) + def create(self): # pylint: disable=method-required-super raise NotImplementedError + def update(self): + raise NotImplementedError + + def delete(self): + raise NotImplementedError + + def to_write_values(self, api_dict): + """ + :return a tuple with + - the external id + - a writable dictionary for _model + received from the api + """ + raise NotImplementedError + + +class SubscriptionRequestAdapter(AbstractEMCAdapter): + _model = "subscription.request" + _service = "subscription-request" + def search(self, date_from=None, date_to=None): - url = self.get_url([]) + url = self._get_url([]) params = {} if date_from: params.update({"date_from": Date.to_string(date_from)}) @@ -38,22 +67,13 @@ class SubscriptionRequestAdapter: params.update({"date_to": Date.to_string(date_to)}) sr_list = self.backend.http_get_content(url, params=params) - return sr_list - - def read(self, id_): - # pylint: disable=method-required-super - url = self.get_url([str(id_)]) - sr = self.backend.http_get_content(url) - return sr - - def update(self): - raise NotImplementedError - - def delete(self): - raise NotImplementedError + return { + "count": sr_list["count"], + "rows": [self.to_write_values(row) for row in sr_list["rows"]], + } def validate(self, id_): - url = self.get_url([str(id_), "validate"]) + url = self._get_url([str(id_), "validate"]) data = {} try: invoice_dict = self.backend.http_post_content(url, data) @@ -65,22 +85,19 @@ class SubscriptionRequestAdapter: "with your system administrator." ) ) - return invoice_dict + ai_adapter = AccountInvoiceAdapter(backend=self.backend) + return ai_adapter.to_write_values(invoice_dict) - def to_write_values(self, request): - """ - :return a writable dictionary of values from the dictionary - received from the api - """ + def to_write_values(self, api_dict): Country = self.backend.env["res.country"] ProductTemplateBinding = self.backend.env[ "emc.binding.product.template" ] - address = request["address"] + address = api_dict["address"] country = Country.search([("code", "=", address["country"])]) - external_product_id = request["share_product"]["id"] + external_product_id = api_dict["share_product"]["id"] share_product_binding = ProductTemplateBinding.search_binding( self.backend, external_product_id ) @@ -90,17 +107,18 @@ class SubscriptionRequestAdapter: "No binding exists for share product %s. Please contact " "system administrator " ) - % request["share_product"]["name"] + % api_dict["share_product"]["name"] ) product_product = share_product_binding.internal_id.product_variant_id - return { - "email": request["email"], - "name": request["name"], - "date": request["date"], - "state": request["state"], - "lang": request["lang"], - "ordered_parts": request["ordered_parts"], + external_id = api_dict["id"] + writable_dict = { + "email": api_dict["email"], + "name": api_dict["name"], + "date": api_dict["date"], + "state": api_dict["state"], + "lang": api_dict["lang"], + "ordered_parts": api_dict["ordered_parts"], "address": address["street"], "zip_code": address["zip_code"], "city": address["city"], @@ -108,3 +126,14 @@ class SubscriptionRequestAdapter: "share_product_id": product_product.id, "source": "emc_api", } + return external_id, writable_dict + + +class AccountInvoiceAdapter(AbstractEMCAdapter): + _model = "account.invoice" + _service = "invoice" + + def to_write_values(self, api_dict): + external_id = api_dict.pop("id") + writable_dict = api_dict + return external_id, writable_dict diff --git a/easy_my_coop_connector/models/subscription_request.py b/easy_my_coop_connector/models/subscription_request.py index 2662b9c..6c5c31d 100644 --- a/easy_my_coop_connector/models/subscription_request.py +++ b/easy_my_coop_connector/models/subscription_request.py @@ -7,7 +7,7 @@ from datetime import date, timedelta from odoo import api, fields, models -from .subscription_request_adapter import SubscriptionRequestAdapter +from .emc_adapters import SubscriptionRequestAdapter _logger = logging.getLogger(__name__) @@ -43,15 +43,13 @@ class SubscriptionRequest(models.Model): backend = self._get_backend() adapter = SubscriptionRequestAdapter(backend=backend) requests_dict = adapter.search(date_from=date_from, date_to=date_to) - for request_dict in requests_dict["rows"]: - external_id = request_dict["id"] - request_values = adapter.to_write_values(request_dict) + for external_id, request_dict in requests_dict["rows"]: sr_binding = SRBinding.search_binding(backend, external_id) if sr_binding: # update request - sr_binding.internal_id.write(request_values) + sr_binding.internal_id.write(request_dict) else: srequest = self.env["subscription.request"].create( - request_values + request_dict ) SRBinding.create( { @@ -60,7 +58,9 @@ class SubscriptionRequest(models.Model): "internal_id": srequest.id, } ) - external_ids = [row["id"] for row in requests_dict["rows"]] + external_ids = [ + external_id for external_id, _ in requests_dict["rows"] + ] srequests = SRBinding.search( [ ("backend_id", "=", backend.id), @@ -76,9 +76,7 @@ class SubscriptionRequest(models.Model): backend = self._get_backend() adapter = SubscriptionRequestAdapter(backend) - sr_data = adapter.read(external_id) - - request_values = adapter.to_write_values(sr_data) + _, request_values = adapter.read(external_id) sr_binding = SRBinding.search_binding(backend, external_id) if sr_binding: # update request @@ -120,13 +118,15 @@ class SubscriptionRequest(models.Model): if self.source == "emc_api": backend = self._get_backend() sr_adapter = SubscriptionRequestAdapter(backend=backend) - invoice_dict = sr_adapter.validate(self.binding_id.external_id) + external_id, invoice_dict = sr_adapter.validate( + self.binding_id.external_id + ) InvoiceBinding = self.env["emc.binding.account.invoice"] InvoiceBinding.create( { "backend_id": backend.id, - "external_id": invoice_dict["id"], + "external_id": external_id, "internal_id": invoice.id, } ) From 8f210401a6f5c5f78e1e68423f9ecfefdc727be9 Mon Sep 17 00:00:00 2001 From: "robin.keunen" Date: Thu, 13 Aug 2020 18:10:54 +0200 Subject: [PATCH 08/21] [ADD] emcc: post payment for emc-api invoice sends the payment to platform --- easy_my_coop_connector/demo/demo.xml | 10 +- easy_my_coop_connector/models/__init__.py | 2 + .../models/account_journal.py | 16 ++++ .../models/account_payment.py | 48 ++++++++++ easy_my_coop_connector/models/emc_adapters.py | 58 +++++++++++- easy_my_coop_connector/models/emc_backend.py | 13 +++ easy_my_coop_connector/models/emc_bindings.py | 18 ++++ .../models/subscription_request.py | 21 +---- .../security/ir.model.access.csv | 2 + easy_my_coop_connector/tests/__init__.py | 1 + easy_my_coop_connector/tests/test_data.py | 92 +++++++++++++++++++ easy_my_coop_connector/tests/test_payment.py | 72 +++++++++++++++ .../tests/test_subscription_request.py | 89 +++--------------- easy_my_coop_connector/views/actions.xml | 12 +++ easy_my_coop_connector/views/emc_bindings.xml | 56 +++++++++++ easy_my_coop_connector/views/menus.xml | 14 +++ 16 files changed, 422 insertions(+), 102 deletions(-) create mode 100644 easy_my_coop_connector/models/account_journal.py create mode 100644 easy_my_coop_connector/models/account_payment.py create mode 100644 easy_my_coop_connector/tests/test_data.py create mode 100644 easy_my_coop_connector/tests/test_payment.py diff --git a/easy_my_coop_connector/demo/demo.xml b/easy_my_coop_connector/demo/demo.xml index eae18ae..e240535 100644 --- a/easy_my_coop_connector/demo/demo.xml +++ b/easy_my_coop_connector/demo/demo.xml @@ -4,6 +4,11 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). --> + + + emc_api + + IWP backend http://localhost:9876 @@ -22,9 +27,4 @@ 31 - - emc_api - - - diff --git a/easy_my_coop_connector/models/__init__.py b/easy_my_coop_connector/models/__init__.py index e786407..ae6fedf 100644 --- a/easy_my_coop_connector/models/__init__.py +++ b/easy_my_coop_connector/models/__init__.py @@ -1,4 +1,6 @@ from . import emc_backend from . import emc_bindings from . import account_invoice +from . import account_journal +from . import account_payment from . import subscription_request diff --git a/easy_my_coop_connector/models/account_journal.py b/easy_my_coop_connector/models/account_journal.py new file mode 100644 index 0000000..855d16e --- /dev/null +++ b/easy_my_coop_connector/models/account_journal.py @@ -0,0 +1,16 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class AccountJournal(models.Model): + _inherit = "account.journal" + + binding_id = fields.One2many( + comodel_name="emc.binding.account.journal", + inverse_name="internal_id", + string="Binding ID", + required=False, + ) diff --git a/easy_my_coop_connector/models/account_payment.py b/easy_my_coop_connector/models/account_payment.py new file mode 100644 index 0000000..abac561 --- /dev/null +++ b/easy_my_coop_connector/models/account_payment.py @@ -0,0 +1,48 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + +from .emc_adapters import AccountPaymentAdapter + + +class AccountPayment(models.Model): + _inherit = "account.payment" + + binding_id = fields.One2many( + comodel_name="emc.binding.account.payment", + inverse_name="internal_id", + string="Binding ID", + required=False, + ) + + @api.multi + def post(self): + res = super(AccountPayment, self).post() + for payment in self: + if any(payment.invoice_ids.mapped("release_capital_request")): + invoice_id = payment.invoice_ids + if len(invoice_id) > 1: + raise ValidationError( + _( + "This version of easy my coop connector " + "can't handle several invoice per" + "payment. Please contact your " + "system administrator" + ) + ) + + backend = self.env["emc.backend"].get_backend() + adapter = AccountPaymentAdapter(backend=backend) + external_id, external_record = adapter.create(payment) + self.env["emc.binding.account.payment"].create( + { + "backend": backend.id, + "internal_id": payment.id, + "external_id": external_id, + } + ) + + return res diff --git a/easy_my_coop_connector/models/emc_adapters.py b/easy_my_coop_connector/models/emc_adapters.py index 654339c..2fb4fcc 100644 --- a/easy_my_coop_connector/models/emc_adapters.py +++ b/easy_my_coop_connector/models/emc_adapters.py @@ -19,10 +19,12 @@ class AbstractEMCAdapter: def __init__(self, backend): self.backend = backend - def _get_url(self, args): + def _get_url(self, args=None): """args is a list of path elements :return the complete route to the service """ + if args is None: + args = [] return join("/", self._root, self._service, *args) def search(self, **params): @@ -34,9 +36,13 @@ class AbstractEMCAdapter: api_dict = self.backend.http_get_content(url) return self.to_write_values(api_dict) - def create(self): + def create(self, record): # pylint: disable=method-required-super - raise NotImplementedError + url = self._get_url() + api_dict = self.to_api_dict(record) + external_record = self.backend.http_post_content(url, api_dict) + external_id, writeable_dict = self.to_write_values(external_record) + return external_id, writeable_dict def update(self): raise NotImplementedError @@ -53,13 +59,16 @@ class AbstractEMCAdapter: """ raise NotImplementedError + def to_api_dict(self, record): + raise NotImplementedError + class SubscriptionRequestAdapter(AbstractEMCAdapter): _model = "subscription.request" _service = "subscription-request" def search(self, date_from=None, date_to=None): - url = self._get_url([]) + url = self._get_url() params = {} if date_from: params.update({"date_from": Date.to_string(date_from)}) @@ -137,3 +146,44 @@ class AccountInvoiceAdapter(AbstractEMCAdapter): external_id = api_dict.pop("id") writable_dict = api_dict return external_id, writable_dict + + +class AccountPaymentAdapter(AbstractEMCAdapter): + _model = "account.payment" + _service = "payment" + + def to_write_values(self, api_dict): + api_dict = api_dict.copy() + external_id = api_dict.pop("id") + writable_dict = api_dict + return external_id, writable_dict + + def to_api_dict(self, record): + + if not record.journal_id.binding_id: + raise ValidationError( + _( + "Journal %s is not bound to a journal on the platform. " + "Please contact system administrator." + ) + % record.journal_id.name + ) + + if not record.invoice_ids.binding_id: + raise ValidationError( + _( + "Invoice %s is not bound to a journal on the platform. " + "Please contact system administrator." + ) + % record.invoice_ids.name + ) + + return { + "journal": record.journal_id.binding_id.external_id, + "invoice": record.invoice_ids.binding_id.external_id, + "payment_date": Date.to_string(record.payment_date), + "amount": record.amount, + "communication": record.communication, + "payment_type": record.payment_type, + "payment_method": record.payment_method_id.code, + } diff --git a/easy_my_coop_connector/models/emc_backend.py b/easy_my_coop_connector/models/emc_backend.py index 61743cb..ee9924d 100644 --- a/easy_my_coop_connector/models/emc_backend.py +++ b/easy_my_coop_connector/models/emc_backend.py @@ -24,6 +24,19 @@ class EMCBackend(models.Model): description = fields.Text(string="Description", required=False) active = fields.Boolean(string="active", default=True) + @api.model + def get_backend(self): + backend = self.env["emc.backend"].search([("active", "=", True)]) + try: + backend.ensure_one() + except ValueError as e: + _logger.error( + "One and only one backend is allowed for the Easy My Coop " + "connector." + ) + raise e + return backend + @api.multi def http_get(self, url, params=None, headers=None): self.ensure_one() diff --git a/easy_my_coop_connector/models/emc_bindings.py b/easy_my_coop_connector/models/emc_bindings.py index 39dec2e..0077cdc 100644 --- a/easy_my_coop_connector/models/emc_bindings.py +++ b/easy_my_coop_connector/models/emc_bindings.py @@ -55,3 +55,21 @@ class AccountInvoiceBinding(models.Model): internal_id = fields.Many2one( comodel_name="account.invoice", string="Internal ID", required=True ) + + +class AccountPaymentBinding(models.Model): + _name = "emc.binding.account.payment" + _inherit = "emc.binding" + + internal_id = fields.Many2one( + comodel_name="account.payment", string="Internal ID", required=True + ) + + +class AccountJournalBinding(models.Model): + _name = "emc.binding.account.journal" + _inherit = "emc.binding" + + internal_id = fields.Many2one( + comodel_name="account.journal", string="Internal ID", required=True + ) diff --git a/easy_my_coop_connector/models/subscription_request.py b/easy_my_coop_connector/models/subscription_request.py index 6c5c31d..214315e 100644 --- a/easy_my_coop_connector/models/subscription_request.py +++ b/easy_my_coop_connector/models/subscription_request.py @@ -23,24 +23,11 @@ class SubscriptionRequest(models.Model): required=False, ) - @api.model - def _get_backend(self): - backend = self.env["emc.backend"].search([("active", "=", True)]) - try: - backend.ensure_one() - except ValueError as e: - _logger.error( - "One and only one backend is allowed for the Easy My Coop " - "connector." - ) - raise e - return backend - @api.model def fetch_subscription_requests(self, date_from=None, date_to=None): SRBinding = self.env["emc.binding.subscription.request"] - backend = self._get_backend() + backend = self.env["emc.backend"].get_backend() adapter = SubscriptionRequestAdapter(backend=backend) requests_dict = adapter.search(date_from=date_from, date_to=date_to) for external_id, request_dict in requests_dict["rows"]: @@ -73,7 +60,7 @@ class SubscriptionRequest(models.Model): def backend_read(self, external_id): SRBinding = self.env["emc.binding.subscription.request"] - backend = self._get_backend() + backend = self.env["emc.backend"].get_backend() adapter = SubscriptionRequestAdapter(backend) _, request_values = adapter.read(external_id) @@ -95,7 +82,7 @@ class SubscriptionRequest(models.Model): @api.model def fetch_subscription_requests_cron(self): - backend = self._get_backend() + backend = self.env["emc.backend"].get_backend() date_to = date.today() date_from = date_to - timedelta(days=1) @@ -116,7 +103,7 @@ class SubscriptionRequest(models.Model): ).validate_subscription_request() if self.source == "emc_api": - backend = self._get_backend() + backend = self.env["emc.backend"].get_backend() sr_adapter = SubscriptionRequestAdapter(backend=backend) external_id, invoice_dict = sr_adapter.validate( self.binding_id.external_id diff --git a/easy_my_coop_connector/security/ir.model.access.csv b/easy_my_coop_connector/security/ir.model.access.csv index 9ce8ad3..96d779b 100644 --- a/easy_my_coop_connector/security/ir.model.access.csv +++ b/easy_my_coop_connector/security/ir.model.access.csv @@ -3,3 +3,5 @@ access_emc_backend_administrator,access_emc_backend_administrator,model_emc_back access_emc_binding_subscription_request_administrator,access_emc_binding_subscription_request_administrator,model_emc_binding_subscription_request,base.group_system,1,1,1,1 access_emc_binding_product_template_administrator,access_emc_binding_product_template_administrator,model_emc_binding_product_template,base.group_system,1,1,1,1 access_emc_binding_account_invoice_administrator,access_emc_binding_account_invoice_administrator,model_emc_binding_account_invoice,base.group_system,1,1,1,1 +access_emc_binding_account_payment_administrator,access_emc_binding_account_payment_administrator,model_emc_binding_account_payment,base.group_system,1,1,1,1 +access_emc_binding_account_journal_administrator,access_emc_binding_account_journal_administrator,model_emc_binding_account_journal,base.group_system,1,1,1,1 diff --git a/easy_my_coop_connector/tests/__init__.py b/easy_my_coop_connector/tests/__init__.py index 4346b2f..9098a70 100644 --- a/easy_my_coop_connector/tests/__init__.py +++ b/easy_my_coop_connector/tests/__init__.py @@ -1 +1,2 @@ from . import test_subscription_request +from . import test_payment diff --git a/easy_my_coop_connector/tests/test_data.py b/easy_my_coop_connector/tests/test_data.py new file mode 100644 index 0000000..e9b153e --- /dev/null +++ b/easy_my_coop_connector/tests/test_data.py @@ -0,0 +1,92 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +import json + +from odoo.fields import Date + + +def dict_to_dump(content): + return json.dumps(content).encode("utf-8") + + +NOT_FOUND_ERROR = {"name": "Not Found", "code": 404} +FORBIDDEN_ERROR = {"name": "Forbidden", "code": 403} +SERVER_ERROR = {"name": "Server Error", "code": 500} +NO_RESULT = {"count": 0, "rows": []} + +SR_SEARCH_RESULT = { + "count": 1, + "rows": [ + { + "id": 1, + "date": "2020-05-14", + "email": "manuel@demo.net", + "address": { + "city": "Brussels", + "street": "schaerbeekstraat", + "zip_code": "1111", + "country": "BE", + }, + "lang": "en_US", + "ordered_parts": 3, + "name": "Manuel Dublues", + "share_product": {"name": "Part B - Worker", "id": 31}, + "state": "draft", + } + ], +} + +SR_GET_RESULT = { + "id": 1, + "name": "Robin Des Bois", + "date": "2020-05-14", + "email": "manuel@demo.net", + "address": { + "city": "Brussels", + "street": "schaerbeekstraat", + "zip_code": "1111", + "country": "BE", + }, + "lang": "en_US", + "ordered_parts": 3, + "share_product": {"name": "Part B - Worker", "id": 31}, + "state": "draft", +} + +SR_VALIDATE_RESULT = { + "id": 9999, + "number": "SUBJ/2020/001", + "date_due": "2020-08-12", + "state": "open", + "date_invoice": "2020-08-12", + "date": "2020-08-12", + "type": "out_invoice", + "subscription_request": {"name": "Manuel Dublues", "id": 1}, + "partner": {"name": "Manuel Dublues", "id": 1}, + "invoice_lines": [ + { + "price_unit": 25.0, + "quantity": 3.0, + "account": {"name": "Product Sales", "id": 2}, + "name": "Part B - Worker", + "product": {"name": "Part B - Worker", "id": 2}, + } + ], + "journal": {"name": "Subscription Journal", "id": 1}, + "account": {"name": "Cooperators", "id": 1}, +} + +AP_CREATE_RESULT = { + "id": 9876, + "journal": {"id": 1, "name": "bank"}, + "invoice": { + "id": SR_VALIDATE_RESULT["id"], + "name": SR_VALIDATE_RESULT["number"], + }, + "payment_date": Date.to_string(Date.today()), + "amount": 75.0, + "communication": SR_VALIDATE_RESULT["number"], +} diff --git a/easy_my_coop_connector/tests/test_payment.py b/easy_my_coop_connector/tests/test_payment.py new file mode 100644 index 0000000..827f785 --- /dev/null +++ b/easy_my_coop_connector/tests/test_payment.py @@ -0,0 +1,72 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + + +from unittest.mock import Mock, patch + +import requests + +from odoo.fields import Date + +from odoo.addons.easy_my_coop.tests.test_base import EMCBaseCase + +from .test_data import AP_CREATE_RESULT, SR_VALIDATE_RESULT, dict_to_dump + + +class EMCPaymentConnectorCase(EMCBaseCase): + def setUp(self): + super().setUp() + self.backend = self.browse_ref( + "easy_my_coop_connector.emc_backend_demo" + ) + self.env["emc.binding.account.journal"].create( + { + "backend_id": self.backend.id, + "internal_id": self.bank_journal.id, + "external_id": 1, + } + ) + + def test_post_payment_sends_and_binds_request(self): + srequest = self.browse_ref("easy_my_coop.subscription_request_1_demo") + with patch.object(requests, "post") as mock_get: + mock_get.return_value = mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = dict_to_dump(SR_VALIDATE_RESULT) + + srequest.validate_subscription_request() + + capital_release_request = srequest.capital_release_request + + payment_method_manual_in = self.env.ref( + "account.account_payment_method_manual_in" + ) + ctx = { + "active_model": "account.invoice", + "active_ids": [capital_release_request.id], + } + register_payments = ( + self.env["account.register.payments"] + .with_context(ctx) + .create( + { + "payment_date": Date.today(), + "journal_id": self.bank_journal.id, + "payment_method_id": payment_method_manual_in.id, + } + ) + ) + + with patch.object(requests, "post") as mock_get: + mock_get.return_value = mock_response = Mock() + mock_response.status_code = 200 + mock_response.content = dict_to_dump(AP_CREATE_RESULT) + + register_payments.create_payments() + + self.assertEquals(capital_release_request.state, "paid") + + payment = capital_release_request.payment_ids + self.assertEquals(payment.state, "posted") + self.assertEquals(payment.binding_id.external_id, 9876) diff --git a/easy_my_coop_connector/tests/test_subscription_request.py b/easy_my_coop_connector/tests/test_subscription_request.py index c13f788..8f6f6f3 100644 --- a/easy_my_coop_connector/tests/test_subscription_request.py +++ b/easy_my_coop_connector/tests/test_subscription_request.py @@ -4,84 +4,21 @@ import datetime -import json from unittest.mock import Mock, patch import requests from odoo.addons.easy_my_coop.tests.test_base import EMCBaseCase -NOT_FOUND_ERROR = {"name": "Not Found", "code": 404} -FORBIDDEN_ERROR = {"name": "Forbidden", "code": 403} -SERVER_ERROR = {"name": "Server Error", "code": 500} -NO_RESULT = {"count": 0, "rows": []} -SEARCH_RESULT = { - "count": 1, - "rows": [ - { - "id": 1, - "date": "2020-05-14", - "email": "manuel@demo.net", - "address": { - "city": "Brussels", - "street": "schaerbeekstraat", - "zip_code": "1111", - "country": "BE", - }, - "lang": "en_US", - "ordered_parts": 3, - "name": "Manuel Dublues", - "share_product": {"name": "Part B - Worker", "id": 31}, - "state": "draft", - } - ], -} -GET_RESULT = { - "id": 1, - "name": "Robin Des Bois", - "date": "2020-05-14", - "email": "manuel@demo.net", - "address": { - "city": "Brussels", - "street": "schaerbeekstraat", - "zip_code": "1111", - "country": "BE", - }, - "lang": "en_US", - "ordered_parts": 3, - "share_product": {"name": "Part B - Worker", "id": 31}, - "state": "draft", -} - -VALIDATE_RESULT = { - "id": 9999, - "number": "SUBJ/2020/001", - "date_due": "2020-08-12", - "state": "open", - "date_invoice": "2020-08-12", - "date": "2020-08-12", - "type": "out_invoice", - "subscription_request": {"name": "Manuel Dublues", "id": 1}, - "partner": {"name": "Manuel Dublues", "id": 1}, - "invoice_lines": [ - { - "price_unit": 25.0, - "quantity": 3.0, - "account": {"name": "Product Sales", "id": 2}, - "name": "Part B - Worker", - "product": {"name": "Part B - Worker", "id": 2}, - } - ], - "journal": {"name": "Subscription Journal", "id": 1}, - "account": {"name": "Cooperators", "id": 1}, -} - - -def dict_to_dump(content): - return json.dumps(content).encode("utf-8") - - -class EMCConnectorCase(EMCBaseCase): +from .test_data import ( + SR_GET_RESULT, + SR_SEARCH_RESULT, + SR_VALIDATE_RESULT, + dict_to_dump, +) + + +class EMCSRConnectorCase(EMCBaseCase): def setUp(self): super().setUp() self.backend = self.browse_ref( @@ -102,7 +39,7 @@ class EMCConnectorCase(EMCBaseCase): with patch.object(requests, "get") as mock_get: mock_get.return_value = mock_response = Mock() mock_response.status_code = 200 - mock_response.content = dict_to_dump(SEARCH_RESULT) + mock_response.content = dict_to_dump(SR_SEARCH_RESULT) SubscriptionRequest.fetch_subscription_requests( date_from=date_from, date_to=date_to @@ -127,7 +64,7 @@ class EMCConnectorCase(EMCBaseCase): with patch.object(requests, "get") as mock_get: mock_get.return_value = mock_response = Mock() mock_response.status_code = 200 - mock_response.content = dict_to_dump(GET_RESULT) + mock_response.content = dict_to_dump(SR_GET_RESULT) SubscriptionRequest.backend_read(external_id) @@ -138,7 +75,7 @@ class EMCConnectorCase(EMCBaseCase): with patch.object(requests, "post") as mock_get: mock_get.return_value = mock_response = Mock() mock_response.status_code = 200 - mock_response.content = dict_to_dump(VALIDATE_RESULT) + mock_response.content = dict_to_dump(SR_VALIDATE_RESULT) srequest.validate_subscription_request() @@ -149,7 +86,7 @@ class EMCConnectorCase(EMCBaseCase): # local invoice linked to external invoice self.assertEquals( srequest.capital_release_request.binding_id.external_id, - VALIDATE_RESULT["id"], + SR_VALIDATE_RESULT["id"], ) # todo test 400 diff --git a/easy_my_coop_connector/views/actions.xml b/easy_my_coop_connector/views/actions.xml index 6790a5c..0a437ad 100644 --- a/easy_my_coop_connector/views/actions.xml +++ b/easy_my_coop_connector/views/actions.xml @@ -28,4 +28,16 @@ emc.binding.account.invoice tree,form + + + payment Bindings + emc.binding.account.payment + tree,form + + + + Journal Bindings + emc.binding.account.journal + tree,form + diff --git a/easy_my_coop_connector/views/emc_bindings.xml b/easy_my_coop_connector/views/emc_bindings.xml index 8d20980..8951737 100644 --- a/easy_my_coop_connector/views/emc_bindings.xml +++ b/easy_my_coop_connector/views/emc_bindings.xml @@ -87,4 +87,60 @@ + + + emc_binding_account_payment_view_form + emc.binding.account.payment + +
+ + + + + + + +
+
+
+ + + emc_binding_account_payment_view_tree + emc.binding.account.payment + + + + + + + + + + + emc_binding_account_journal_view_form + emc.binding.account.journal + +
+ + + + + + + +
+
+
+ + + emc_binding_account_journal_view_tree + emc.binding.account.journal + + + + + + + + diff --git a/easy_my_coop_connector/views/menus.xml b/easy_my_coop_connector/views/menus.xml index caac917..3ce45f8 100644 --- a/easy_my_coop_connector/views/menus.xml +++ b/easy_my_coop_connector/views/menus.xml @@ -40,6 +40,20 @@ groups="base.group_user" sequence="1040"/> + + + + Date: Thu, 13 Aug 2020 18:24:15 +0200 Subject: [PATCH 09/21] [REF] emcc: components to their own directory --- easy_my_coop_connector/__init__.py | 1 + easy_my_coop_connector/components/__init__.py | 3 +++ easy_my_coop_connector/{models => components}/emc_adapters.py | 0 easy_my_coop_connector/{models => components}/emc_backend.py | 0 easy_my_coop_connector/{models => components}/emc_bindings.py | 0 easy_my_coop_connector/models/__init__.py | 2 -- easy_my_coop_connector/models/account_payment.py | 4 ++-- easy_my_coop_connector/models/subscription_request.py | 2 +- 8 files changed, 7 insertions(+), 5 deletions(-) create mode 100644 easy_my_coop_connector/components/__init__.py rename easy_my_coop_connector/{models => components}/emc_adapters.py (100%) rename easy_my_coop_connector/{models => components}/emc_backend.py (100%) rename easy_my_coop_connector/{models => components}/emc_bindings.py (100%) diff --git a/easy_my_coop_connector/__init__.py b/easy_my_coop_connector/__init__.py index aee8895..79dbb94 100644 --- a/easy_my_coop_connector/__init__.py +++ b/easy_my_coop_connector/__init__.py @@ -1,2 +1,3 @@ +from . import components from . import models from . import wizards diff --git a/easy_my_coop_connector/components/__init__.py b/easy_my_coop_connector/components/__init__.py new file mode 100644 index 0000000..9239b7e --- /dev/null +++ b/easy_my_coop_connector/components/__init__.py @@ -0,0 +1,3 @@ +from . import emc_adapters +from . import emc_backend +from . import emc_bindings diff --git a/easy_my_coop_connector/models/emc_adapters.py b/easy_my_coop_connector/components/emc_adapters.py similarity index 100% rename from easy_my_coop_connector/models/emc_adapters.py rename to easy_my_coop_connector/components/emc_adapters.py diff --git a/easy_my_coop_connector/models/emc_backend.py b/easy_my_coop_connector/components/emc_backend.py similarity index 100% rename from easy_my_coop_connector/models/emc_backend.py rename to easy_my_coop_connector/components/emc_backend.py diff --git a/easy_my_coop_connector/models/emc_bindings.py b/easy_my_coop_connector/components/emc_bindings.py similarity index 100% rename from easy_my_coop_connector/models/emc_bindings.py rename to easy_my_coop_connector/components/emc_bindings.py diff --git a/easy_my_coop_connector/models/__init__.py b/easy_my_coop_connector/models/__init__.py index ae6fedf..e3930ee 100644 --- a/easy_my_coop_connector/models/__init__.py +++ b/easy_my_coop_connector/models/__init__.py @@ -1,5 +1,3 @@ -from . import emc_backend -from . import emc_bindings from . import account_invoice from . import account_journal from . import account_payment diff --git a/easy_my_coop_connector/models/account_payment.py b/easy_my_coop_connector/models/account_payment.py index abac561..1cf34b7 100644 --- a/easy_my_coop_connector/models/account_payment.py +++ b/easy_my_coop_connector/models/account_payment.py @@ -5,7 +5,7 @@ from odoo import _, api, fields, models from odoo.exceptions import ValidationError -from .emc_adapters import AccountPaymentAdapter +from ..components.emc_adapters import AccountPaymentAdapter class AccountPayment(models.Model): @@ -39,7 +39,7 @@ class AccountPayment(models.Model): external_id, external_record = adapter.create(payment) self.env["emc.binding.account.payment"].create( { - "backend": backend.id, + "backend_id": backend.id, "internal_id": payment.id, "external_id": external_id, } diff --git a/easy_my_coop_connector/models/subscription_request.py b/easy_my_coop_connector/models/subscription_request.py index 214315e..f06f126 100644 --- a/easy_my_coop_connector/models/subscription_request.py +++ b/easy_my_coop_connector/models/subscription_request.py @@ -7,7 +7,7 @@ from datetime import date, timedelta from odoo import api, fields, models -from .emc_adapters import SubscriptionRequestAdapter +from ..components.emc_adapters import SubscriptionRequestAdapter _logger = logging.getLogger(__name__) From 4a2c3f1fb223bd4d7641de6c2a617f1b86b3e731 Mon Sep 17 00:00:00 2001 From: "robin.keunen" Date: Tue, 18 Aug 2020 18:42:17 +0200 Subject: [PATCH 10/21] [FIX] emcc: name in backend view --- easy_my_coop_connector/components/emc_backend.py | 2 +- easy_my_coop_connector/views/emc_backend.xml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/easy_my_coop_connector/components/emc_backend.py b/easy_my_coop_connector/components/emc_backend.py index ee9924d..96fb821 100644 --- a/easy_my_coop_connector/components/emc_backend.py +++ b/easy_my_coop_connector/components/emc_backend.py @@ -19,7 +19,7 @@ class EMCBackend(models.Model): _description = "EMC Backend" name = fields.Char(string="Name", required=True) - location = fields.Char(string="Location") + location = fields.Char(string="URL") api_key = fields.Char(string="API Key") description = fields.Text(string="Description", required=False) active = fields.Boolean(string="active", default=True) diff --git a/easy_my_coop_connector/views/emc_backend.xml b/easy_my_coop_connector/views/emc_backend.xml index 27e52d8..50cf661 100644 --- a/easy_my_coop_connector/views/emc_backend.xml +++ b/easy_my_coop_connector/views/emc_backend.xml @@ -18,6 +18,7 @@ + From eabe83c0fedb2ba765bce3863fe485271c9c3736 Mon Sep 17 00:00:00 2001 From: "robin.keunen" Date: Thu, 20 Aug 2020 15:20:08 +0200 Subject: [PATCH 11/21] [IMP] emca: timestamp exported data --- easy_my_coop_api/models/__init__.py | 1 + easy_my_coop_api/models/external_id_mixin.py | 17 ++++++-------- .../models/subscription_request.py | 18 +++++++++++++++ .../services/subscription_request_service.py | 2 ++ .../tests/test_account_invoice.py | 22 ++++++++++++++----- .../tests/test_subscription_requests.py | 2 ++ .../components/emc_backend.py | 8 +++---- 7 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 easy_my_coop_api/models/subscription_request.py diff --git a/easy_my_coop_api/models/__init__.py b/easy_my_coop_api/models/__init__.py index 6740752..3e72c0b 100644 --- a/easy_my_coop_api/models/__init__.py +++ b/easy_my_coop_api/models/__init__.py @@ -1,2 +1,3 @@ from . import auth_api_key from . import external_id_mixin +from . import subscription_request diff --git a/easy_my_coop_api/models/external_id_mixin.py b/easy_my_coop_api/models/external_id_mixin.py index 1e699fa..bed33e3 100644 --- a/easy_my_coop_api/models/external_id_mixin.py +++ b/easy_my_coop_api/models/external_id_mixin.py @@ -28,6 +28,12 @@ class ExternalIdMixin(models.AbstractModel): string="External ID Sequence", required=False, ) + first_api_export_date = fields.Datetime( + string="First API Export Date", required=False + ) + last_api_export_date = fields.Datetime( + string="Last API Export Date", required=False + ) @api.multi def set_external_sequence(self): @@ -55,11 +61,7 @@ class ExternalIdMixin(models.AbstractModel): while True: try: next_id = self.external_id_sequence_id._next() - self.sudo().write( - { - "_api_external_id": next_id - } - ) + self.sudo().write({"_api_external_id": next_id}) break except IntegrityError as e: if n > 0: @@ -74,11 +76,6 @@ class ResPartner(models.Model): _inherit = ["res.partner", "external.id.mixin"] -class SubscriptionRequest(models.Model): - _name = "subscription.request" - _inherit = ["subscription.request", "external.id.mixin"] - - class AccountAccount(models.Model): _name = "account.account" _inherit = ["account.account", "external.id.mixin"] diff --git a/easy_my_coop_api/models/subscription_request.py b/easy_my_coop_api/models/subscription_request.py new file mode 100644 index 0000000..97b6c76 --- /dev/null +++ b/easy_my_coop_api/models/subscription_request.py @@ -0,0 +1,18 @@ +# Copyright 2020 Coop IT Easy SCRL fs +# Robin Keunen +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, models +from odoo.fields import Datetime + + +class SubscriptionRequest(models.Model): + _name = "subscription.request" + _inherit = ["subscription.request", "external.id.mixin"] + + @api.multi + def _timestamp_export(self): + self.write({"last_api_export_date": Datetime.now()}) + self.filtered(lambda sr: not sr.first_api_export_date).write( + {"first_api_export_date": Datetime.now()} + ) diff --git a/easy_my_coop_api/services/subscription_request_service.py b/easy_my_coop_api/services/subscription_request_service.py index ed05258..c966043 100644 --- a/easy_my_coop_api/services/subscription_request_service.py +++ b/easy_my_coop_api/services/subscription_request_service.py @@ -31,6 +31,7 @@ class SubscriptionRequestService(Component): [("_api_external_id", "=", _id)] ) if sr: + sr._timestamp_export() return self._to_dict(sr) else: raise wrapJsonException( @@ -49,6 +50,7 @@ class SubscriptionRequestService(Component): domain.append(("date", "<=", date_to)) requests = self.env["subscription.request"].search(domain) + requests._timestamp_export() response = { "count": len(requests), diff --git a/easy_my_coop_api/tests/test_account_invoice.py b/easy_my_coop_api/tests/test_account_invoice.py index 4919ef5..e6117ca 100644 --- a/easy_my_coop_api/tests/test_account_invoice.py +++ b/easy_my_coop_api/tests/test_account_invoice.py @@ -30,11 +30,20 @@ class TestAccountInvoiceController(BaseEMCRestCase): today = Date.to_string(Date.today()) self.demo_invoice_dict = { - "id": 1, + "id": self.capital_release.get_api_external_id(), "number": "xxx", # can't guess it - "partner": {"id": 1, "name": "Catherine des Champs"}, - "account": {"id": 1, "name": "Cooperators"}, - "journal": {"id": 1, "name": "Subscription Journal"}, + "partner": { + "id": self.coop_candidate.get_api_external_id(), + "name": self.coop_candidate.name, + }, + "account": { + "id": self.cooperator_account.get_api_external_id(), + "name": self.cooperator_account.name, + }, + "journal": { + "id": self.subscription_journal.get_api_external_id(), + "name": self.subscription_journal.name, + }, "subscription_request": {}, "state": "open", "date": today, @@ -47,7 +56,10 @@ class TestAccountInvoiceController(BaseEMCRestCase): "product": {"id": 1, "name": "Part A - Founder"}, "price_unit": 100.0, "quantity": 2.0, - "account": {"id": 2, "name": "Equity"}, + "account": { + "id": self.equity_account.get_api_external_id(), + "name": self.equity_account.name, + }, } ], } diff --git a/easy_my_coop_api/tests/test_subscription_requests.py b/easy_my_coop_api/tests/test_subscription_requests.py index 8abb081..824166d 100644 --- a/easy_my_coop_api/tests/test_subscription_requests.py +++ b/easy_my_coop_api/tests/test_subscription_requests.py @@ -76,6 +76,8 @@ class TestSRController(BaseEMCRestCase): date_sr = self.sr_service.search(date_from=date_from, date_to=date_to) self.assertTrue(date_sr) + self.assertTrue(self.demo_request_1.first_api_export_date) + self.assertTrue(self.demo_request_1.last_api_export_date) def test_route_get(self): external_id = self.demo_request_1.get_api_external_id() diff --git a/easy_my_coop_connector/components/emc_backend.py b/easy_my_coop_connector/components/emc_backend.py index 96fb821..6c06005 100644 --- a/easy_my_coop_connector/components/emc_backend.py +++ b/easy_my_coop_connector/components/emc_backend.py @@ -9,7 +9,7 @@ import requests from werkzeug.exceptions import BadRequest, InternalServerError, NotFound from odoo import _, api, fields, models -from odoo.exceptions import AccessDenied +from odoo.exceptions import AccessDenied, Warning as UserError _logger = logging.getLogger(__name__) @@ -113,12 +113,12 @@ class EMCBackend(models.Model): response = requests.get(url) except Exception as e: _logger.error(e) - raise Warning(_("Failed to connect to backend: %s" % str(e))) + raise UserError(_("Failed to connect to backend: %s" % str(e))) if response.status_code == 200: content = json.loads(response.content.decode("utf-8")) - raise Warning(_("Success: %s") % content["message"]) + raise UserError(_("Success: %s") % content["message"]) else: - raise Warning( + raise UserError( _("Failed to connect to backend: %s" % str(response.content)) ) From f4e82d1493c96de4f34bc0d4780137b4e92122bf Mon Sep 17 00:00:00 2001 From: "robin.keunen" Date: Tue, 25 Aug 2020 10:08:43 +0200 Subject: [PATCH 12/21] [ADD] emca: add form pages for API fields --- easy_my_coop_api/__manifest__.py | 2 +- easy_my_coop_api/models/external_id_mixin.py | 12 +- .../views/external_id_mixin_views.xml | 178 ++++++++++++++++++ 3 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 easy_my_coop_api/views/external_id_mixin_views.xml diff --git a/easy_my_coop_api/__manifest__.py b/easy_my_coop_api/__manifest__.py index 7aa896a..ac55137 100644 --- a/easy_my_coop_api/__manifest__.py +++ b/easy_my_coop_api/__manifest__.py @@ -17,7 +17,7 @@ "summary": """ Open Easy My Coop to the world: RESTful API. """, - "data": [], + "data": ["views/external_id_mixin_views.xml"], "demo": ["demo/demo.xml"], "installable": True, "application": False, diff --git a/easy_my_coop_api/models/external_id_mixin.py b/easy_my_coop_api/models/external_id_mixin.py index bed33e3..8206b1f 100644 --- a/easy_my_coop_api/models/external_id_mixin.py +++ b/easy_my_coop_api/models/external_id_mixin.py @@ -35,6 +35,11 @@ class ExternalIdMixin(models.AbstractModel): string="Last API Export Date", required=False ) + # only used to display and hide "Generate external ID" button + external_id_generated = fields.Boolean( + string="External ID Generated", default=False, required=False + ) + @api.multi def set_external_sequence(self): self.ensure_one() @@ -61,7 +66,12 @@ class ExternalIdMixin(models.AbstractModel): while True: try: next_id = self.external_id_sequence_id._next() - self.sudo().write({"_api_external_id": next_id}) + self.sudo().write( + { + "_api_external_id": next_id, + "external_id_generated": True, + } + ) break except IntegrityError as e: if n > 0: diff --git a/easy_my_coop_api/views/external_id_mixin_views.xml b/easy_my_coop_api/views/external_id_mixin_views.xml new file mode 100644 index 0000000..4a8cc8b --- /dev/null +++ b/easy_my_coop_api/views/external_id_mixin_views.xml @@ -0,0 +1,178 @@ + + + + + view_partner_form + res.partner + + + + + + + + + +