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"/> + + + +