diff --git a/base_upflow/README.rst b/base_upflow/README.rst index 0e020bc2a..2ba289365 100644 --- a/base_upflow/README.rst +++ b/base_upflow/README.rst @@ -7,7 +7,7 @@ Base Upflow.io !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:bd21d053b40a9912cbd5c2ebd07de0691a56ce1d8f3d68a9bb47ce3e7451c062 + !! source digest: sha256:d7c3cc9b72154534c57c4158ea7aef658511c0609a2631b2b5d2680065011595 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png diff --git a/base_upflow/__manifest__.py b/base_upflow/__manifest__.py index 376a95c71..89204ebba 100644 --- a/base_upflow/__manifest__.py +++ b/base_upflow/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Base Upflow.io", "summary": "Base module to generate Upflow.io API payloads format from odoo object", - "version": "14.0.1.1.0", + "version": "14.0.2.0.0", "development_status": "Alpha", "category": "EDI", "website": "https://github.com/OCA/credit-control", diff --git a/base_upflow/models/account_move.py b/base_upflow/models/account_move.py index 18af9404b..5db5849d2 100644 --- a/base_upflow/models/account_move.py +++ b/base_upflow/models/account_move.py @@ -2,9 +2,13 @@ # @author Pierre Verkest # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import base64 +import logging from odoo import _, fields, models from odoo.exceptions import UserError +from odoo.tools.float_utils import float_compare + +_logger = logging.getLogger(__name__) class AccountMove(models.Model): @@ -17,6 +21,81 @@ class AccountMove(models.Model): help="Technical field to get the upflow customer to use on account move", ) + upflow_type = fields.Selection( + selection=[ + ("none", "Not concerned"), + ("invoices", "Invoice"), + ("payments", "Invoice payment"), + ("creditNotes", "Refund"), + ("refunds", "Refund payment"), + ], + compute="_compute_upflow_type", + help=( + "Technical fields to make sure consistency " + "while sending Journal entry and reconcile " + "payloads. Key values are current payload " + "keys while sending reconcile. While creating " + "malicious entries it can be hard to automatically " + "choose proper type" + ), + ) + + def _compute_upflow_type(self): + for move in self: + if move.move_type.startswith("in_") or move.state != "posted": + move.upflow_type = "none" + continue + if move.move_type == "out_invoice": + move.upflow_type = "invoices" + elif move.move_type == "out_refund": + move.upflow_type = "creditNotes" + else: + receivables_lines = move.line_ids.filtered( + lambda line: line.account_id.user_type_id.type == "receivable" + ) + if not receivables_lines: + move.upflow_type = "none" + continue + debit = sum(receivables_lines.mapped("debit")) + credit = sum(receivables_lines.mapped("credit")) + if ( + float_compare( + debit, + 0, + precision_rounding=move.currency_id.rounding, + ) + == 0 + and float_compare( + credit, + 0, + precision_rounding=move.currency_id.rounding, + ) + != 0 + ): + move.upflow_type = "payments" + elif ( + float_compare( + debit, + 0, + precision_rounding=move.currency_id.rounding, + ) + != 0 + and float_compare( + credit, + 0, + precision_rounding=move.currency_id.rounding, + ) + == 0 + ): + move.upflow_type = "refunds" + else: + _logger.error( + "Sum of receivable move lines on %s have credit (%d) and debit(%d). " + "Which sounds suspicious and can't set upflow type", + move.name, + ) + move.upflow_type = "none" + def _compute_upflow_commercial_partner_id(self): # while using OD as counter part or bank statement # there are chance that partner_id is not set on account.move @@ -56,35 +135,11 @@ def _prepare_upflow_api_payload(self): def get_upflow_api_post_invoice_payload(self): """An upflow invoice match with account.move out_invoice odoo type""" self.ensure_one() - if self.move_type != "out_invoice": - raise UserError( - _( - "You try to get upflow invoice payload " - "on account entry %s with an other type %s " - "(expected out_invoice)" - ) - % ( - self.name, - self.move_type, - ) - ) return self._prepare_upflow_api_payload() def get_upflow_api_post_credit_note_payload(self): """An upflow credit note match with account.move out_refund odoo type""" self.ensure_one() - if self.move_type != "out_refund": - raise UserError( - _( - "You try to get upflow refund payload " - "on account entry %s with an other type %s " - "(expected out_refund)" - ) - % ( - self.name, - self.move_type, - ) - ) return self._prepare_upflow_api_payload() def get_upflow_api_post_payment_payload(self): diff --git a/base_upflow/models/account_partial_reconcile.py b/base_upflow/models/account_partial_reconcile.py index 1e5b643dc..826522965 100644 --- a/base_upflow/models/account_partial_reconcile.py +++ b/base_upflow/models/account_partial_reconcile.py @@ -5,12 +5,12 @@ class AccountPartialReconcile(models.Model): - - _inherit = ["account.partial.reconcile"] + _name = "account.partial.reconcile" + _inherit = ["account.partial.reconcile", "upflow.mixin"] def _prepare_reconcile_payload(self): payload = { - "externalId": str(self.id), + "externalId": "partial-" + str(self.id), "invoices": [], "payments": [], "creditNotes": [], @@ -18,6 +18,19 @@ def _prepare_reconcile_payload(self): } return payload + def _get_part_payload(self, move_line): + data = { + "externalId": str(move_line.move_id.id), + "amountLinked": self.company_currency_id.to_lowest_division(self.amount), + } + if move_line.move_id.upflow_uuid: + data["id"] = move_line.move_id.upflow_uuid + + if move_line.move_id.upflow_type in ["invoices", "creditNotes"]: + data["customId"] = move_line.move_id.name + + return data + def get_upflow_api_post_reconcile_payload(self): """expect to be called from account move type: @@ -28,32 +41,11 @@ def get_upflow_api_post_reconcile_payload(self): """ payload = self._prepare_reconcile_payload() - data = { - "externalId": str(self.debit_move_id.move_id.id), - "amountLinked": self.company_currency_id.to_lowest_division(self.amount), - } - if self.debit_move_id.move_id.upflow_uuid: - data["id"] = self.debit_move_id.move_id.upflow_uuid - if self.debit_move_id.move_id.move_type == "out_invoice": - kind = "invoices" - data["customId"] = self.debit_move_id.move_id.name - else: - kind = "refunds" - - payload[kind].append(data) - - data = { - "externalId": str(self.credit_move_id.move_id.id), - "amountLinked": self.company_currency_id.to_lowest_division(self.amount), - } - if self.credit_move_id.move_id.upflow_uuid: - data["id"] = self.credit_move_id.move_id.upflow_uuid - if self.credit_move_id.move_id.move_type == "out_refund": - kind = "creditNotes" - data["customId"] = self.credit_move_id.move_id.name - else: - kind = "payments" - - payload[kind].append(data) + payload[self.debit_move_id.move_id.upflow_type].append( + self._get_part_payload(self.debit_move_id) + ) + payload[self.credit_move_id.move_id.upflow_type].append( + self._get_part_payload(self.credit_move_id) + ) return payload diff --git a/base_upflow/static/description/index.html b/base_upflow/static/description/index.html index fe6c82b42..f63479340 100644 --- a/base_upflow/static/description/index.html +++ b/base_upflow/static/description/index.html @@ -367,7 +367,7 @@

Base Upflow.io

!! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -!! source digest: sha256:bd21d053b40a9912cbd5c2ebd07de0691a56ce1d8f3d68a9bb47ce3e7451c062 +!! source digest: sha256:d7c3cc9b72154534c57c4158ea7aef658511c0609a2631b2b5d2680065011595 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Alpha License: AGPL-3 OCA/credit-control Translate me on Weblate Try me on Runboat

This modules provide methods to generate upflow.io diff --git a/base_upflow/tests/test_account_move.py b/base_upflow/tests/test_account_move.py index a443de75f..3714d46e6 100644 --- a/base_upflow/tests/test_account_move.py +++ b/base_upflow/tests/test_account_move.py @@ -1,4 +1,7 @@ from odoo.tests.common import SavepointCase +from odoo.tools import mute_logger + +from .common import AccountingCommonCase class TestAccountMove(SavepointCase): @@ -67,3 +70,151 @@ def test_compute_upflow_commercial_partner_id_invoice(self): self.account_move.upflow_commercial_partner_id, self.partner.commercial_partner_id, ) + + +class TestAccountMoveUpflowType(SavepointCase, AccountingCommonCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._setup_accounting() + cls.partner = cls.env["res.partner"].create( + { + "name": "My customer company", + "is_company": True, + "vat": "FR23334175221", + "street": "Street 1", + "street2": "and more", + "zip": "45500", + "city": "Customer city", + } + ) + cls.invoice = cls._create_invoice() + cls.refund = cls._create_invoice(move_type="out_refund") + cls.purchase_invoice = cls._create_invoice(move_type="in_invoice") + cls.payment_bnk_stmt = cls._make_credit_transfer_payment_reconciled(cls.invoice) + + def test_compute_upflow_type_draft(self): + self.assertEqual(self.invoice.upflow_type, "none") + self.assertEqual(self.refund.upflow_type, "none") + self.assertEqual(self.purchase_invoice.upflow_type, "none") + + def test_compute_upflow_type_invoices(self): + self.invoice.action_post() + self.assertEqual(self.invoice.upflow_type, "invoices") + + def test_compute_upflow_type_credit_notes(self): + self.refund.action_post() + self.assertEqual(self.refund.upflow_type, "creditNotes") + + def test_payment_by_bank_statment(self): + self.assertEqual(self.payment_bnk_stmt.upflow_type, "payments") + + def test_customer_payment_from_bank_statement(self): + self.refund.action_post() + payment_bnk_stmt = self._make_credit_transfer_payment_reconciled( + self.refund, + amount=-self.refund.amount_residual, + reconcile_param=[ + { + "id": self.refund.line_ids.filtered( + lambda line: line.account_internal_type + in ("receivable", "payable") + ).id + } + ], + ) + self.assertEqual(payment_bnk_stmt.upflow_type, "refunds") + + def test_customer_payment_manual_payment(self): + self.refund.action_post() + move = self._register_manual_payment_reconciled(self.refund) + self.assertEqual(move.upflow_type, "refunds") + + def test_bank_statment_not_reconciled(self): + # at that time we don't know yet if payment entry is related to + # payment refunds or paid purchase invoice + + ( + bank_journal, + _method, + payment_date, + amount, + _currency, + ) = self._payment_params( + self.invoice, + ) + bank_stmt = self.env["account.bank.statement"].create( + { + "journal_id": bank_journal.id, + "date": payment_date, + "name": "payment", + "line_ids": [ + ( + 0, + 0, + { + "payment_ref": "payment", + "partner_id": self.partner.id, + "amount": -amount, + }, + ) + ], + } + ) + bank_stmt.button_post() + self.assertEqual(bank_stmt.line_ids[0].move_id.upflow_type, "none") + + def test_no_receivable_lines(self): + self.purchase_invoice.action_post() + self.assertEqual(self.purchase_invoice.upflow_type, "none") + + @mute_logger("odoo.addons.base_upflow.models.account_move") + def test_receivables_null(self): + + account_user_type = self.env["account.account.type"].create( + { + "name": "Test account type", + "type": "receivable", + "internal_group": "asset", + } + ) + account = self.env["account.account"].create( + { + "name": "Test account", + "code": "TEST", + "user_type_id": account_user_type.id, + "reconcile": True, + } + ) + entry_move = self.env["account.move"].create( + { + "journal_id": self.invoice.journal_id.id, + "move_type": "entry", + "line_ids": [ + ( + 0, + 0, + { + "account_id": account.id, + "partner_id": self.partner.id, + "name": "Test entry", + "debit": 0, + "credit": 0, + }, + ), + ( + 0, + 0, + { + "account_id": account.id, + "partner_id": self.partner.id, + "name": "Test entry", + "debit": 0, + "credit": 0, + }, + ), + ], + } + ) + entry_move.action_post() + self.assertEquals(entry_move.upflow_type, "none") diff --git a/base_upflow/tests/test_upflow_post_invoices_payload.py b/base_upflow/tests/test_upflow_post_invoices_payload.py index 9fb839201..25ad20f6d 100644 --- a/base_upflow/tests/test_upflow_post_invoices_payload.py +++ b/base_upflow/tests/test_upflow_post_invoices_payload.py @@ -257,19 +257,11 @@ def test_get_upflow_api_post_credit_notes_payload_content(self): str(self.customer_company.id), ) - def test_get_payload_not_an_invoice(self): - with self.assertRaisesRegex(UserError, "expected out_invoice"): - self.refund.get_upflow_api_post_invoice_payload() - def test_get_invoice_pdf_payload_not_an_invoice(self): invoice_payment_move = self._register_manual_payment_reconciled(self.invoice) with self.assertRaisesRegex(UserError, "expected out_invoice"): invoice_payment_move.get_upflow_api_pdf_payload() - def test_get_payload_not_a_refund(self): - with self.assertRaisesRegex(UserError, "expected out_refund"): - self.invoice.get_upflow_api_post_credit_note_payload() - def test_format_upflow_amount(self): currency_euro = self.env.ref("base.EUR") currency_dynar = self.env.ref("base.LYD") @@ -512,7 +504,7 @@ def convert_to_cent(euro_amount): reconcile_content, ) - expected_payload["externalId"] = str(partial_reconcile.id) + expected_payload["externalId"] = "partial-" + str(partial_reconcile.id) self.assertEqual(reconcile_content, expected_payload) def test_get_upflow_api_post_reconcile_refund_payload(self): @@ -550,7 +542,7 @@ def test_get_upflow_api_post_reconcile_refund_payload(self): ) expected = { - "externalId": str(full_reconcile.partial_reconcile_ids.id), + "externalId": "partial-" + str(full_reconcile.partial_reconcile_ids.id), "invoices": [], "payments": [], "creditNotes": [ @@ -572,21 +564,6 @@ def test_get_upflow_api_post_reconcile_refund_payload(self): self.maxDiff = None self.assertEqual(reconcile_content, expected) - def test_post_reconcile_payload_add_upflow_id_if_present(self): - uuid = str(uuid4()) - self.invoice.upflow_uuid = uuid - self._register_manual_payment_reconciled(self.invoice) - reconciles = self.invoice.line_ids.full_reconcile_id.partial_reconcile_ids - payloads = [ - reconcile.get_upflow_api_post_reconcile_payload() - for reconcile in reconciles - ] - - self.assertEqual({i["id"] for p in payloads for i in p["invoices"]}, {uuid}) - self.assertTrue( - all(["id" not in p for payload in payloads for p in payload["payments"]]) - ) - def test_get_upflow_api_post_contacts_payload_without_main_id(self): self.customer_company.main_contact_id = False content = self.contact.get_upflow_api_post_contacts_payload() diff --git a/base_upflow/views/account_move.xml b/base_upflow/views/account_move.xml index fb1e51f22..f3a87956a 100644 --- a/base_upflow/views/account_move.xml +++ b/base_upflow/views/account_move.xml @@ -4,29 +4,26 @@ License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). --> - - account.move.form - account.move - - - - - - - + account.move.form + account.move + + + + + + + + - - - - - + + + + +