diff --git a/base_upflow/README.rst b/base_upflow/README.rst new file mode 100644 index 000000000..0e020bc2a --- /dev/null +++ b/base_upflow/README.rst @@ -0,0 +1,105 @@ +============== +Base Upflow.io +============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:bd21d053b40a9912cbd5c2ebd07de0691a56ce1d8f3d68a9bb47ce3e7451c062 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fcredit--control-lightgray.png?logo=github + :target: https://github.com/OCA/credit-control/tree/14.0/base_upflow + :alt: OCA/credit-control +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/credit-control-14-0/credit-control-14-0-base_upflow + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/credit-control&target_branch=14.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + + +This modules provide methods to generate `upflow.io `_ +HTTP API endpoint payloads without any method to communicate with the API. + +This module is used by `edi_upflow`, we decide to separate payloads generation logic +from the edi framework in use. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Known issues / Roadmap +====================== + +* considering `out_receipt` account.move type (probably to be managed likes `out_refund`)? +* test with multi-currency for a given company +* improve the way to handler custom fields to be able to configure them + and automatically generate the payload + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Pierre Verkest + +Contributors +~~~~~~~~~~~~ + +* `Foodles `_ + + * Pierre Verkest + * Matthias BARKAT + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-petrus-v| image:: https://github.com/petrus-v.png?size=40px + :target: https://github.com/petrus-v + :alt: petrus-v + +Current `maintainer `__: + +|maintainer-petrus-v| + +This module is part of the `OCA/credit-control `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/base_upflow/__init__.py b/base_upflow/__init__.py new file mode 100644 index 000000000..0650744f6 --- /dev/null +++ b/base_upflow/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/base_upflow/__manifest__.py b/base_upflow/__manifest__.py new file mode 100644 index 000000000..376a95c71 --- /dev/null +++ b/base_upflow/__manifest__.py @@ -0,0 +1,31 @@ +# Copyright 2023 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +{ + "name": "Base Upflow.io", + "summary": "Base module to generate Upflow.io API payloads format from odoo object", + "version": "14.0.1.1.0", + "development_status": "Alpha", + "category": "EDI", + "website": "https://github.com/OCA/credit-control", + "author": "Pierre Verkest, Odoo Community Association (OCA)", + "maintainers": ["petrus-v"], + "license": "AGPL-3", + "application": True, + "depends": [ + "contacts", + "account", + ], + # I'm not sure for test depencies we shoud declared them here + "external_dependencies": {"python": ["jsonschema"]}, + "data": [ + "views/account_journal.xml", + "views/account_move.xml", + "views/res_partner.xml", + "views/menu.xml", + "datas/res_partner_position.xml", + "security/ir.model.access.csv", + ], + "demo": [], + "installable": True, +} diff --git a/base_upflow/datas/res_partner_position.xml b/base_upflow/datas/res_partner_position.xml new file mode 100644 index 000000000..3ceb29daa --- /dev/null +++ b/base_upflow/datas/res_partner_position.xml @@ -0,0 +1,31 @@ + + + + Accounting + ACCOUNTING + + + Payer + PAYER + + + Purchaser + PURCHASER + + + Sales + SALES + + diff --git a/base_upflow/i18n/base_upflow.pot b/base_upflow/i18n/base_upflow.pot new file mode 100644 index 000000000..c9b357b58 --- /dev/null +++ b/base_upflow/i18n/base_upflow.pot @@ -0,0 +1,289 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_upflow +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0+e\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: base_upflow +#: model:res.partner.upflow.position,name:base_upflow.upflow_res_partner_upflow_position_accounting +msgid "Accounting" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields.selection,name:base_upflow.selection__account_payment_method__upflow_instrument__card +msgid "Card" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields.selection,name:base_upflow.selection__account_payment_method__upflow_instrument__cash +msgid "Cash" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields.selection,name:base_upflow.selection__account_payment_method__upflow_instrument__check +msgid "Check" +msgstr "" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_res_partner +msgid "Contact" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_res_partner_upflow_position__create_uid +msgid "Created by" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_res_partner_upflow_position__create_date +msgid "Created on" +msgstr "" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_res_currency +msgid "Currency" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields.selection,name:base_upflow.selection__account_payment_method__upflow_instrument__direct_debit +msgid "Direct Debit" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_full_reconcile__display_name +#: model:ir.model.fields,field_description:base_upflow.field_account_journal__display_name +#: model:ir.model.fields,field_description:base_upflow.field_account_move__display_name +#: model:ir.model.fields,field_description:base_upflow.field_account_payment__display_name +#: model:ir.model.fields,field_description:base_upflow.field_account_payment_method__display_name +#: model:ir.model.fields,field_description:base_upflow.field_res_currency__display_name +#: model:ir.model.fields,field_description:base_upflow.field_res_partner__display_name +#: model:ir.model.fields,field_description:base_upflow.field_res_partner_upflow_position__display_name +#: model:ir.model.fields,field_description:base_upflow.field_upflow_mixin__display_name +msgid "Display Name" +msgstr "" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_account_full_reconcile +msgid "Full Reconcile" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_full_reconcile__id +#: model:ir.model.fields,field_description:base_upflow.field_account_journal__id +#: model:ir.model.fields,field_description:base_upflow.field_account_move__id +#: model:ir.model.fields,field_description:base_upflow.field_account_payment__id +#: model:ir.model.fields,field_description:base_upflow.field_account_payment_method__id +#: model:ir.model.fields,field_description:base_upflow.field_res_currency__id +#: model:ir.model.fields,field_description:base_upflow.field_res_partner__id +#: model:ir.model.fields,field_description:base_upflow.field_res_partner_upflow_position__id +#: model:ir.model.fields,field_description:base_upflow.field_upflow_mixin__id +msgid "ID" +msgstr "" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_account_journal +msgid "Journal" +msgstr "" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_account_move +msgid "Journal Entry" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_full_reconcile____last_update +#: model:ir.model.fields,field_description:base_upflow.field_account_journal____last_update +#: model:ir.model.fields,field_description:base_upflow.field_account_move____last_update +#: model:ir.model.fields,field_description:base_upflow.field_account_payment____last_update +#: model:ir.model.fields,field_description:base_upflow.field_account_payment_method____last_update +#: model:ir.model.fields,field_description:base_upflow.field_res_currency____last_update +#: model:ir.model.fields,field_description:base_upflow.field_res_partner____last_update +#: model:ir.model.fields,field_description:base_upflow.field_res_partner_upflow_position____last_update +#: model:ir.model.fields,field_description:base_upflow.field_upflow_mixin____last_update +msgid "Last Modified on" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_res_partner_upflow_position__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_res_partner_upflow_position__write_date +msgid "Last Updated on" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_fsm_location__main_contact_id +#: model:ir.model.fields,field_description:base_upflow.field_fsm_person__main_contact_id +#: model:ir.model.fields,field_description:base_upflow.field_res_partner__main_contact_id +#: model:ir.model.fields,field_description:base_upflow.field_res_users__main_contact_id +msgid "Main contact" +msgstr "" + +#. module: base_upflow +#: model:res.partner.upflow.position,name:base_upflow.upflow_res_partner_upflow_position_payer +msgid "Payer" +msgstr "" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_account_payment_method +msgid "Payment Methods" +msgstr "" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_account_payment +msgid "Payments" +msgstr "" + +#. module: base_upflow +#: model:ir.actions.act_window,name:base_upflow.action_res_partner_upflow_position +#: model:ir.model.fields,field_description:base_upflow.field_res_partner_upflow_position__name +#: model:ir.ui.menu,name:base_upflow.base_upflow_menu_position +msgid "Position" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_fsm_location__upflow_position_id +#: model:ir.model.fields,field_description:base_upflow.field_fsm_person__upflow_position_id +#: model:ir.model.fields,field_description:base_upflow.field_res_partner__upflow_position_id +#: model:ir.model.fields,field_description:base_upflow.field_res_users__upflow_position_id +msgid "Position (Upflow)" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,help:base_upflow.field_fsm_location__upflow_position_id +#: model:ir.model.fields,help:base_upflow.field_fsm_person__upflow_position_id +#: model:ir.model.fields,help:base_upflow.field_res_partner__upflow_position_id +#: model:ir.model.fields,help:base_upflow.field_res_users__upflow_position_id +msgid "Position of the contact in the company" +msgstr "" + +#. module: base_upflow +#: model:res.partner.upflow.position,name:base_upflow.upflow_res_partner_upflow_position_purchaser +msgid "Purchaser" +msgstr "" + +#. module: base_upflow +#: model:res.partner.upflow.position,name:base_upflow.upflow_res_partner_upflow_position_sales +msgid "Sales" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields.selection,name:base_upflow.selection__account_payment_method__upflow_instrument__unknown +msgid "Unknown" +msgstr "" + +#. module: base_upflow +#: model:ir.ui.menu,name:base_upflow.base_upflow_menu_config +msgid "Upflow" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_res_partner_upflow_position__code +msgid "Upflow Code" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_bank_statement_line__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_account_move__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_account_payment__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_fsm_location__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_fsm_person__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_res_partner__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_res_users__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_upflow_mixin__upflow_direct_url +msgid "Upflow Direct Url" +msgstr "" + +#. module: base_upflow +#: model_terms:ir.ui.view,arch_db:base_upflow.view_move_form +#: model_terms:ir.ui.view,arch_db:base_upflow.view_partner_form +msgid "Upflow Info" +msgstr "" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_res_partner_upflow_position +msgid "Upflow Partner Position" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_bank_statement_line__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_account_move__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_account_payment__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_fsm_location__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_fsm_person__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_res_partner__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_res_users__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_upflow_mixin__upflow_uuid +msgid "Upflow Uuid" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_journal__upflow_bank_account_uuid +msgid "Upflow bank account technical id" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_payment_method__upflow_instrument +msgid "Upflow instrument" +msgstr "" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_upflow_mixin +msgid "Upflow mixin to add common fields and behaviour" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,help:base_upflow.field_account_journal__upflow_bank_account_uuid +msgid "Upflow.io bank account UUID linked to payment done in this journal." +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields,help:base_upflow.field_account_payment_method__upflow_instrument +msgid "Used by upflow.io integration to set payment instrument field" +msgstr "" + +#. module: base_upflow +#: model:ir.model.fields.selection,name:base_upflow.selection__account_payment_method__upflow_instrument__wire_transfer +msgid "Wire Transfer" +msgstr "" + +#. module: base_upflow +#: code:addons/base_upflow/models/account_move.py:0 +#: code:addons/base_upflow/models/account_move.py:0 +#: code:addons/build/__editable_odoo_addons__/odoo/addons/base_upflow/models/account_move.py:0 +#, python-format +msgid "" +"You try to get upflow PDF payload on account entry %s with an unexpected " +"type %s (expected out_invoice or out_refund)" +msgstr "" + +#. module: base_upflow +#: code:addons/base_upflow/models/account_move.py:0 +#: code:addons/base_upflow/models/account_move.py:0 +#: code:addons/build/__editable_odoo_addons__/odoo/addons/base_upflow/models/account_move.py:0 +#, python-format +msgid "" +"You try to get upflow invoice payload on account entry %s with an other type" +" %s (expected out_invoice)" +msgstr "" + +#. module: base_upflow +#: code:addons/base_upflow/models/account_move.py:0 +#: code:addons/base_upflow/models/account_move.py:0 +#: code:addons/build/__editable_odoo_addons__/odoo/addons/base_upflow/models/account_move.py:0 +#, python-format +msgid "" +"You try to get upflow refund payload on account entry %s with an other type " +"%s (expected out_refund)" +msgstr "" diff --git a/base_upflow/i18n/fr.po b/base_upflow/i18n/fr.po new file mode 100644 index 000000000..2fa4a7c57 --- /dev/null +++ b/base_upflow/i18n/fr.po @@ -0,0 +1,250 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * base_upflow +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0+e\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: base_upflow +#: model:ir.model.fields.selection,name:base_upflow.selection__account_payment_method__upflow_instrument__card +msgid "Card" +msgstr "Carte" + +#. module: base_upflow +#: model:ir.model.fields.selection,name:base_upflow.selection__account_payment_method__upflow_instrument__cash +msgid "Cash" +msgstr "Espèces" + +#. module: base_upflow +#: model:ir.model.fields.selection,name:base_upflow.selection__account_payment_method__upflow_instrument__check +msgid "Check" +msgstr "Chèque" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_res_partner +msgid "Contact" +msgstr "Contact" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_res_currency +msgid "Currency" +msgstr "Devise" + +#. module: base_upflow +#: model:ir.model.fields.selection,name:base_upflow.selection__account_payment_method__upflow_instrument__direct_debit +msgid "Direct Debit" +msgstr "Prélèvement bancaire" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_full_reconcile__display_name +#: model:ir.model.fields,field_description:base_upflow.field_account_journal__display_name +#: model:ir.model.fields,field_description:base_upflow.field_account_move__display_name +#: model:ir.model.fields,field_description:base_upflow.field_account_payment__display_name +#: model:ir.model.fields,field_description:base_upflow.field_account_payment_method__display_name +#: model:ir.model.fields,field_description:base_upflow.field_res_currency__display_name +#: model:ir.model.fields,field_description:base_upflow.field_res_partner__display_name +#: model:ir.model.fields,field_description:base_upflow.field_upflow_mixin__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_account_full_reconcile +msgid "Full Reconcile" +msgstr "Lettrage (complet)" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_full_reconcile__id +#: model:ir.model.fields,field_description:base_upflow.field_account_journal__id +#: model:ir.model.fields,field_description:base_upflow.field_account_move__id +#: model:ir.model.fields,field_description:base_upflow.field_account_payment__id +#: model:ir.model.fields,field_description:base_upflow.field_account_payment_method__id +#: model:ir.model.fields,field_description:base_upflow.field_res_currency__id +#: model:ir.model.fields,field_description:base_upflow.field_res_partner__id +#: model:ir.model.fields,field_description:base_upflow.field_upflow_mixin__id +msgid "ID" +msgstr "" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_account_journal +msgid "Journal" +msgstr "Journal" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_account_move +msgid "Journal Entry" +msgstr "Pièce comptable" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_full_reconcile____last_update +#: model:ir.model.fields,field_description:base_upflow.field_account_journal____last_update +#: model:ir.model.fields,field_description:base_upflow.field_account_move____last_update +#: model:ir.model.fields,field_description:base_upflow.field_account_payment____last_update +#: model:ir.model.fields,field_description:base_upflow.field_account_payment_method____last_update +#: model:ir.model.fields,field_description:base_upflow.field_res_currency____last_update +#: model:ir.model.fields,field_description:base_upflow.field_res_partner____last_update +#: model:ir.model.fields,field_description:base_upflow.field_upflow_mixin____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_account_payment_method +msgid "Payment Methods" +msgstr "Méthode de paiement" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_account_payment +msgid "Payments" +msgstr "Paiements" + +#. module: base_upflow +#: model:ir.model.fields.selection,name:base_upflow.selection__account_payment_method__upflow_instrument__unknown +msgid "Unknown" +msgstr "Non spécifié" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_bank_statement_line__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_account_move__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_account_payment__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_fsm_location__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_fsm_person__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_res_partner__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_res_users__upflow_direct_url +#: model:ir.model.fields,field_description:base_upflow.field_upflow_mixin__upflow_direct_url +msgid "Upflow Direct Url" +msgstr "Lien direct upflow" + +#. module: base_upflow +#: model_terms:ir.ui.view,arch_db:base_upflow.view_move_form +#: model_terms:ir.ui.view,arch_db:base_upflow.view_partner_form +msgid "Upflow Info" +msgstr "Info upflow" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_bank_statement_line__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_account_move__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_account_payment__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_fsm_location__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_fsm_person__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_res_partner__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_res_users__upflow_uuid +#: model:ir.model.fields,field_description:base_upflow.field_upflow_mixin__upflow_uuid +msgid "Upflow Uuid" +msgstr "Identifiant upflow" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_journal__upflow_bank_account_uuid +msgid "Upflow bank account technical id" +msgstr "Identifiant technique upflow compte en banque." + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_account_payment_method__upflow_instrument +msgid "Upflow instrument" +msgstr "Moyen de paiement upflow" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_upflow_mixin +msgid "Upflow mixin to add common fields and behaviour" +msgstr "Mixin pour l'ajout de champs et comportement commun lié à upflow" + +#. module: base_upflow +#: model:ir.model.fields,help:base_upflow.field_account_journal__upflow_bank_account_uuid +msgid "Upflow.io bank account UUID linked to payment done in this journal." +msgstr "Identifant technique upflow du compte bancaire lié aux paiements effectué dans ce journal." + +#. module: base_upflow +#: model:ir.model.fields,help:base_upflow.field_account_payment_method__upflow_instrument +msgid "Used by upflow.io integration to set payment instrument field" +msgstr "Utilisé par l'intégration avec upflow.ui pour définir le moyen de paiement." + +#. module: base_upflow +#: model:ir.model.fields.selection,name:base_upflow.selection__account_payment_method__upflow_instrument__wire_transfer +msgid "Wire Transfer" +msgstr "Virement bancaire" + +#. module: base_upflow +#: code:addons/base_upflow/models/account_move.py:0 +#, python-format +msgid "" +"You try to get upflow PDF payload on account entry %s with an unexpected " +"type %s (expected out_invoice or out_refund)" +msgstr "" +"Vous essayez de générer le PDF pour upflow sur une pièce comptable %s " +"dont le type %s n'est pas reconnu (attendu out_invoice out out_refund)" + +#. module: base_upflow +#: code:addons/base_upflow/models/account_move.py:0 +#, python-format +msgid "" +"You try to get upflow invoice payload on account entry %s with an other type" +" %s (expected out_invoice)" +msgstr "" +"Vous essayez de générer des données pour upflow pour la facture %s " +"dont le type %s n'est pas reconnu (attendu out_invoice)" + + +#. module: base_upflow +#: code:addons/base_upflow/models/account_move.py:0 +#, python-format +msgid "" +"You try to get upflow refund payload on account entry %s with an other type " +"%s (expected out_refund)" +msgstr "" +"Vous essayez de générer des données pour upflow pour l'avoir %s " +"dont le type %s n'est pas reconnu (attendu out_refund)" + +#. module: base_upflow +#: model:res.partner.upflow.position,name:base_upflow.upflow_res_partner_upflow_position_accounting +msgid "Accounting" +msgstr "Comptable" + +#. module: base_upflow +#: model:res.partner.upflow.position,name:base_upflow.upflow_res_partner_upflow_position_payer +msgid "Payer" +msgstr "Payeur" + +#. module: base_upflow +#: model:res.partner.upflow.position,name:base_upflow.upflow_res_partner_upflow_position_purchaser +msgid "Purchaser" +msgstr "Acheteur" + +#. module: base_upflow +#: model:res.partner.upflow.position,name:base_upflow.upflow_res_partner_upflow_position_sales +msgid "Sales" +msgstr "Commerciale" + + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_res_partner_upflow_position__name +#: model:ir.ui.menu,name:base_upflow.base_upflow_menu_position +#: model:ir.actions.act_window,name:base_upflow.action_res_partner_position +msgid "Position" +msgstr "Fonction" + + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_res_partner__upflow_position_id +msgid "Position (Upflow)" +msgstr "Fonction (Upflow)" + +#. module: base_upflow +#: model:ir.model.fields,help:base_upflow.field_res_partner__upflow_position_id +msgid "Position of the contact in the company" +msgstr "Fonction du contact au sein de la société" + +#. module: base_upflow +#: model:ir.model,name:base_upflow.model_res_partner_upflow_position +msgid "Upflow Partner Position" +msgstr "Fonction du contact upflow" + +#. module: base_upflow +#: model:ir.model.fields,field_description:base_upflow.field_res_partner__main_contact_id +msgid "Main contact" +msgstr "Contact principal" diff --git a/base_upflow/models/__init__.py b/base_upflow/models/__init__.py new file mode 100644 index 000000000..2911f478b --- /dev/null +++ b/base_upflow/models/__init__.py @@ -0,0 +1,11 @@ +from . import upflow_mixin +from . import ( + account_full_reconcile, + account_journal, + account_move, + account_payment, + account_payment_method, + res_currency, + res_partner, + res_partner_upflow_position, +) diff --git a/base_upflow/models/account_full_reconcile.py b/base_upflow/models/account_full_reconcile.py new file mode 100644 index 000000000..4f71cb902 --- /dev/null +++ b/base_upflow/models/account_full_reconcile.py @@ -0,0 +1,63 @@ +# Copyright 2023 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import models + + +class AccountJournal(models.Model): + _name = "account.full.reconcile" + _inherit = ["account.full.reconcile"] + + def _prepare_reconcile_payload(self): + payload = { + "externalId": str(self.id), + "invoices": [], + "payments": [], + "creditNotes": [], + "refunds": [], + } + return payload + + def get_upflow_api_post_reconcile_payload(self): + """expect to be called from account move type: + + * customer invoice + * customer refund + + Once there are considered fully paid + """ + payload = self._prepare_reconcile_payload() + for partial in self.partial_reconcile_ids: + data = { + "externalId": str(partial.debit_move_id.move_id.id), + "amountLinked": partial.company_currency_id.to_lowest_division( + partial.amount + ), + } + if partial.debit_move_id.move_id.upflow_uuid: + data["id"] = partial.debit_move_id.move_id.upflow_uuid + if partial.debit_move_id.move_id.move_type == "out_invoice": + kind = "invoices" + data["customId"] = partial.debit_move_id.move_id.name + else: + kind = "refunds" + + payload[kind].append(data) + + data = { + "externalId": str(partial.credit_move_id.move_id.id), + "amountLinked": partial.company_currency_id.to_lowest_division( + partial.amount + ), + } + if partial.credit_move_id.move_id.upflow_uuid: + data["id"] = partial.credit_move_id.move_id.upflow_uuid + if partial.credit_move_id.move_id.move_type == "out_refund": + kind = "creditNotes" + data["customId"] = partial.credit_move_id.move_id.name + else: + kind = "payments" + + payload[kind].append(data) + + return payload diff --git a/base_upflow/models/account_journal.py b/base_upflow/models/account_journal.py new file mode 100644 index 000000000..3e3a9e890 --- /dev/null +++ b/base_upflow/models/account_journal.py @@ -0,0 +1,13 @@ +# Copyright 2023 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class AccountJournal(models.Model): + _inherit = "account.journal" + + upflow_bank_account_uuid = fields.Char( + "Upflow bank account technical id", + help="Upflow.io bank account UUID linked to payment done in this journal.", + ) diff --git a/base_upflow/models/account_move.py b/base_upflow/models/account_move.py new file mode 100644 index 000000000..9a6ec8c7e --- /dev/null +++ b/base_upflow/models/account_move.py @@ -0,0 +1,146 @@ +# Copyright 2023 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import base64 + +from odoo import _, models +from odoo.exceptions import UserError + + +class AccountMove(models.Model): + _name = "account.move" + _inherit = ["account.move", "upflow.mixin"] + + def _format_upflow_amount(self, amount, currency=None): + if not currency: + currency = self.currency_id + return currency.to_lowest_division(amount) + + def _prepare_upflow_api_payload(self): + payload = self.prepare_base_payload() + payload.update( + { + "customId": self.name, + "issuedAt": self.invoice_date.isoformat(), + "dueDate": self.invoice_date_due.isoformat(), + "name": self.name, + "currency": self.currency_id.name, + "grossAmount": self._format_upflow_amount(self.amount_total), + "netAmount": self._format_upflow_amount(self.amount_untaxed), + "customer": { + # "id": self.partner_id.commercial_partner_id.upflow_uuid, + "externalId": str(self.partner_id.commercial_partner_id.id), + }, + } + ) + return payload + + 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): + """An upflow payment refer to payment received from customer for invoices, + in odoo it could be done in different ways: + * account.payment + * account.bank_statement.line + * ... + + which in any case generate `entry` type account.move + + So we should not consider an `inbound` account.payment is necessarily present + """ + self.ensure_one() + data = self._prepare_upflow_payment_api_payload() + if self.journal_id.upflow_bank_account_uuid: + data["bankAccountId"] = self.journal_id.upflow_bank_account_uuid + return data + + def get_upflow_api_post_refund_payload(self): + """An upflow refund refer to payment send to customer for refunds, + in odoo it could be done with different objects: + * account.payment + * account.bank_statement.line + * ... + + Which in any case generate `entry` type account.move + + So we should not consider an `outbound` account.payment is necessarily present + """ + self.ensure_one() + return self._prepare_upflow_payment_api_payload() + + def _prepare_upflow_payment_api_payload(self): + # TODO: we probably wants to check we are linked to a bank journal + # assert self.journal_id.type == 'bank' + payload = self.prepare_base_payload() + payload.update( + { + "currency": self.currency_id.name, + "amount": self._format_upflow_amount(self.amount_total), + "validatedAt": self.date.isoformat(), + "customer": { + # "id": self.partner_id.commercial_partner_id.upflow_uuid, + "externalId": str(self.partner_id.commercial_partner_id.id), + }, + } + ) + if self.payment_id: + payload.update(self.payment_id._get_upflow_extra_payment_api_payload()) + return payload + + def get_upflow_api_pdf_payload(self): + self.ensure_one() + if self.move_type not in [ + "out_invoice", + "out_refund", + ]: + raise UserError( + _( + "You try to get upflow PDF payload " + "on account entry %s with an unexpected type %s " + "(expected out_invoice or out_refund)" + ) + % ( + self.name, + self.move_type, + ) + ) + return { + "data": self._get_b64_invoice_pdf(), + } + + def _get_b64_invoice_pdf(self): + report = self.env.ref("account.account_invoices_without_payment") + pdf_content, _kind = report.sudo()._render_qweb_pdf(self.id) + return base64.b64encode(pdf_content).decode() diff --git a/base_upflow/models/account_payment.py b/base_upflow/models/account_payment.py new file mode 100644 index 000000000..0e67dc75d --- /dev/null +++ b/base_upflow/models/account_payment.py @@ -0,0 +1,14 @@ +# Copyright 2023 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import models + + +class AccountPayment(models.Model): + _inherit = "account.payment" + + def _get_upflow_extra_payment_api_payload(self): + self.ensure_one() + return { + "instrument": self.payment_method_id.upflow_instrument, + } diff --git a/base_upflow/models/account_payment_method.py b/base_upflow/models/account_payment_method.py new file mode 100644 index 000000000..099fd930c --- /dev/null +++ b/base_upflow/models/account_payment_method.py @@ -0,0 +1,26 @@ +# Copyright 2023 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class AccountPaymentMethod(models.Model): + _inherit = "account.payment.method" + + # to be place in a view extending account_payment_mode module + # and/or setting data + upflow_instrument = fields.Selection( + [ + ("WIRE_TRANSFER", "Wire Transfer"), + ("DIRECT_DEBIT", "Direct Debit"), + ("CARD", "Card"), + ("CASH", "Cash"), + ("CHECK", "Check"), + ("UNKNOWN", "Unknown"), + ], + "Upflow instrument", + required=True, + default="UNKNOWN", + copy=False, + help="Used by upflow.io integration to set payment instrument field", + ) diff --git a/base_upflow/models/res_currency.py b/base_upflow/models/res_currency.py new file mode 100644 index 000000000..1c79dfac9 --- /dev/null +++ b/base_upflow/models/res_currency.py @@ -0,0 +1,15 @@ +# Copyright 2023 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import models + + +class Currency(models.Model): + _inherit = "res.currency" + + def to_lowest_division(self, currency_amount): + """return amount as integer to represent + the lowest division of the currency + (e.g., cents for US Dollars). + """ + return round(currency_amount / self.rounding) diff --git a/base_upflow/models/res_partner.py b/base_upflow/models/res_partner.py new file mode 100644 index 000000000..196698cbb --- /dev/null +++ b/base_upflow/models/res_partner.py @@ -0,0 +1,99 @@ +# Copyright 2023 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class Partner(models.Model): + _name = "res.partner" + _inherit = ["res.partner", "upflow.mixin"] + + upflow_position_id = fields.Many2one( + "res.partner.upflow.position", + string="Position (Upflow)", + help="Position of the contact in the company", + ) + main_contact_id = fields.Many2one( + comodel_name="res.partner", + string="Main contact", + domain="[('parent_id', '=', id),('email', '!=', False)]", + ) + + def _prepare_customer_custom_field_payloads(self): + """Return a list of custom fields to be send in customer payloads: + + `id`: upflow uuid the custom field reference + `value`: the value of the custom field for the current customer + "customFields": [ + { + "id": "00a70b35-2be3-4c43-aefb-397190134655", + "value": None, + } + ], + """ + return [] + + def get_upflow_api_post_customers_payload(self): + customer_company = self.commercial_partner_id + payload = customer_company.prepare_base_payload() + payload.update( + { + "name": customer_company.name, + "vatNumber": customer_company.vat or "", + # "accountingRef": "UPFL", + "externalId": str(customer_company.id), + # "accountManagerId": "00a70b35-2be3-4c43-aefb-397190134655", + # "dunningPlanId": "7a6c91dc-3580-4c43-aefb-397190134655", + "address": { + "address": ( + f"{customer_company.street or ''} " + f"{customer_company.street2 or ''}".strip() + ), + "zipcode": customer_company.zip or "", + "city": customer_company.city or "", + "state": customer_company.state_id.name or "", + "country": customer_company.country_id.name or "", + }, + # "parent": { + # "id": "00a70b35-2be3-4c43-aefb-397190134655", + # "externalId": "1a2c3b", + # }, + # "paymentMethods": { + # "card": {"enabled": False}, + # "check": {"enabled": False}, + # "achDebit": {"enabled": False}, + # "sepaDebit": {"enabled": False}, + # "goCardless": {"enabled": False}, + # "wireTransfer": { + # "enabled": False, + # "bankAccount": {"id": "00a70b35-2be3-4c43-aefb-397190134655"}, + # "bankAccounts": [{"id": "00a70b35-2be3-4c43-aefb-397190134655"}], + # }, + # }, + "customFields": customer_company._prepare_customer_custom_field_payloads(), + "contacts": [ + contact.get_upflow_api_post_contacts_payload() + for contact in customer_company.child_ids + if contact.email + ], + } + ) + return payload + + def get_upflow_api_post_contacts_payload(self): + payload = self.prepare_base_payload() + payload.update( + { + "firstName": self.name, + # "lastName": "", + "phone": self.mobile or "", + "email": self.email, + "externalId": str(self.id), + "isMain": self.commercial_partner_id + and self.commercial_partner_id.main_contact_id == self, + # "id": "00a70b35-2be3-4c43-aefb-397190134655", + } + ) + if self.upflow_position_id: + payload["position"] = self.upflow_position_id.code + return payload diff --git a/base_upflow/models/res_partner_upflow_position.py b/base_upflow/models/res_partner_upflow_position.py new file mode 100644 index 000000000..703e4a5b3 --- /dev/null +++ b/base_upflow/models/res_partner_upflow_position.py @@ -0,0 +1,12 @@ +# Copyright 2023 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class PartnerUpflowPosition(models.Model): + _name = "res.partner.upflow.position" + _description = "Upflow Partner Position" + + name = fields.Char(string="Position", required=True, translate=True) + code = fields.Char(string="Upflow Code", required=True) diff --git a/base_upflow/models/upflow_mixin.py b/base_upflow/models/upflow_mixin.py new file mode 100644 index 000000000..6ed7aa843 --- /dev/null +++ b/base_upflow/models/upflow_mixin.py @@ -0,0 +1,21 @@ +# Copyright 2023 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class UpflowMixin(models.AbstractModel): + _name = "upflow.mixin" + _description = "Upflow mixin to add common fields and behaviour" + + upflow_uuid = fields.Char(readonly=True, copy=False) + upflow_direct_url = fields.Char(readonly=True, copy=False) + + def prepare_base_payload(self): + self.ensure_one() + data = { + "externalId": str(self.id), + } + if self.upflow_uuid: + data["id"] = self.upflow_uuid + return data diff --git a/base_upflow/readme/CONFIGURATION.rst b/base_upflow/readme/CONFIGURATION.rst new file mode 100644 index 000000000..e69de29bb diff --git a/base_upflow/readme/CONTRIBUTORS.rst b/base_upflow/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..3a780810b --- /dev/null +++ b/base_upflow/readme/CONTRIBUTORS.rst @@ -0,0 +1,4 @@ +* `Foodles `_ + + * Pierre Verkest + * Matthias BARKAT diff --git a/base_upflow/readme/DESCRIPTION.rst b/base_upflow/readme/DESCRIPTION.rst new file mode 100644 index 000000000..11c9640d0 --- /dev/null +++ b/base_upflow/readme/DESCRIPTION.rst @@ -0,0 +1,6 @@ + +This modules provide methods to generate `upflow.io `_ +HTTP API endpoint payloads without any method to communicate with the API. + +This module is used by `edi_upflow`, we decide to separate payloads generation logic +from the edi framework in use. diff --git a/base_upflow/readme/ROADMAP.rst b/base_upflow/readme/ROADMAP.rst new file mode 100644 index 000000000..7a1fb5b32 --- /dev/null +++ b/base_upflow/readme/ROADMAP.rst @@ -0,0 +1,4 @@ +* considering `out_receipt` account.move type (probably to be managed likes `out_refund`)? +* test with multi-currency for a given company +* improve the way to handler custom fields to be able to configure them + and automatically generate the payload diff --git a/base_upflow/security/ir.model.access.csv b/base_upflow/security/ir.model.access.csv new file mode 100644 index 000000000..8dc99232b --- /dev/null +++ b/base_upflow/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_res_partner_upflow_position,full access res.partner.upflow.position,model_res_partner_upflow_position,base.group_user,1,0,0,0 diff --git a/base_upflow/static/description/index.html b/base_upflow/static/description/index.html new file mode 100644 index 000000000..fe6c82b42 --- /dev/null +++ b/base_upflow/static/description/index.html @@ -0,0 +1,449 @@ + + + + + + +Base Upflow.io + + + +
+

Base Upflow.io

+ + +

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

+

This modules provide methods to generate upflow.io +HTTP API endpoint payloads without any method to communicate with the API.

+

This module is used by edi_upflow, we decide to separate payloads generation logic +from the edi framework in use.

+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+ +
+

Known issues / Roadmap

+
    +
  • considering out_receipt account.move type (probably to be managed likes out_refund)?
  • +
  • test with multi-currency for a given company
  • +
  • improve the way to handler custom fields to be able to configure them +and automatically generate the payload
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Pierre Verkest
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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

+

Current maintainer:

+

petrus-v

+

This module is part of the OCA/credit-control project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/base_upflow/tests/__init__.py b/base_upflow/tests/__init__.py new file mode 100644 index 000000000..a26419f5d --- /dev/null +++ b/base_upflow/tests/__init__.py @@ -0,0 +1,4 @@ +from . import ( + test_upflow_post_invoices_payload, + test_res_partner, +) diff --git a/base_upflow/tests/common.py b/base_upflow/tests/common.py new file mode 100644 index 000000000..134523d49 --- /dev/null +++ b/base_upflow/tests/common.py @@ -0,0 +1,235 @@ +import time +from unittest import TestCase + + +class AccountingCommonCase(TestCase): + """Provide utilities to test accounting + + Those utility shouldn't not assume what's installed or not + in the database to make determinist unit test whatever + installed data + """ + + @classmethod + def _setup_accounting(cls): + main_company = cls.env.ref("base.main_company") + assert ( + main_company.chart_template_id + and cls.env["account.journal"].search_count([]) > 0 + ), "This test require an account chart to be installed" + cls.pay_terms_multiple = cls.env["account.payment.term"].create( + { + "name": "30% Advance End of Following Month", + "note": "Payment terms: 30% Advance End of Following Month", + "line_ids": [ + ( + 0, + 0, + { + "value": "percent", + "value_amount": 30.0, + "sequence": 400, + "days": 0, + "option": "day_after_invoice_date", + }, + ), + ( + 0, + 0, + { + "value": "balance", + "value_amount": 0.0, + "sequence": 500, + "days": 31, + "option": "day_following_month", + }, + ), + ], + } + ) + + @classmethod + def _create_invoice( + cls, + move_type="out_invoice", + unit_price=50, + currency_id=None, + partner_id=None, + date_invoice=None, + payment_term_id=False, + auto_validate=False, + vat_ids=None, + ): + """Code overwrite from + odoo.addons.account.tests.common.TestAccountReconciliationCommon._create_invoice + with following supper: + + * Allow to set VAT + """ + if not vat_ids: + vat_ids = ( + cls.env["account.tax"] + .search( + [ + ("type_tax_use", "=", "sale"), + ("amount", "!=", 0), + ("company_id", "=", cls.env.company.id), + ], + limit=1, + ) + .ids + ) + date_invoice = date_invoice or time.strftime("%Y") + "-07-01" + + invoice_vals = { + "move_type": move_type, + "partner_id": partner_id or cls.partner, + "invoice_date": date_invoice, + "date": date_invoice, + "invoice_line_ids": [ + ( + 0, + 0, + { + "name": "product with unit price %s" % unit_price, + "quantity": 1, + "price_unit": unit_price, + "tax_ids": [(6, 0, vat_ids)], + }, + ) + ], + } + + if payment_term_id: + invoice_vals["invoice_payment_term_id"] = payment_term_id + + if currency_id: + invoice_vals["currency_id"] = currency_id + + invoice = ( + cls.env["account.move"] + .with_context(default_move_type=move_type) + .create(invoice_vals) + ) + if auto_validate: + invoice.action_post() + return invoice + + @classmethod + def _payment_params( + cls, + account_move, + bank_journal=None, + method=None, + payment_date=None, + amount=None, + currency=None, + ): + if not bank_journal: + bank_journal = cls.env["account.journal"].search( + [ + ("type", "=", "bank"), + ("company_id", "=", cls.env.company.id), + ], + limit=1, + ) + if not method: + method = cls.env.ref("account.account_payment_method_manual_in") + if not payment_date: + payment_date = account_move.invoice_date_due + if not amount: + amount = account_move.amount_residual + if not currency: + currency = account_move.currency_id + return bank_journal, method, payment_date, amount, currency + + @classmethod + def _make_credit_transfer_payment_reconciled( + cls, + invoice, + bank_journal=None, + payment_date=None, + amount=None, + reconcile_param=None, + partner=None, + ): + """payment registered by from bank statement reconciliation + + :param partner: False value means not set while None means get partner from invoice + """ + (bank_journal, _method, payment_date, amount, _currency,) = cls._payment_params( + invoice, + bank_journal=bank_journal, + method=None, + payment_date=payment_date, + amount=amount, + currency=None, + ) + if not reconcile_param: + reconcile_param = [] + # make difference between partner is False and partner is None + if partner is None: + partner = invoice.partner_id + bank_stmt = cls.env["account.bank.statement"].create( + { + "journal_id": bank_journal.id, + "date": payment_date, + "name": "payment" + invoice.name, + "line_ids": [ + ( + 0, + 0, + { + "payment_ref": "payment", + "partner_id": partner.id if partner else False, + "amount": amount, + # "amount_currency": amount, + # "foreign_currency_id": currency.id, + }, + ) + ], + } + ) + bank_stmt.button_post() + + bank_stmt.line_ids[0].reconcile(reconcile_param) + return bank_stmt.line_ids[0].move_id + + @classmethod + def _register_manual_payment_reconciled( + cls, + account_move, + payment_type="inbound", + bank_journal=None, + method=None, + payment_date=None, + amount=None, + currency=None, + ): + bank_journal, method, payment_date, amount, currency = cls._payment_params( + account_move, + bank_journal=bank_journal, + method=method, + payment_date=payment_date, + amount=amount, + currency=currency, + ) + payment_wiz = cls.env["account.payment.register"].with_context( + active_model="account.move", active_ids=[account_move.id] + ) + return ( + payment_wiz.create( + { + "payment_type": payment_type, + "amount": amount, + "payment_method_id": method.id, + "payment_date": payment_date, + "journal_id": bank_journal.id, + "currency_id": currency.id, + # "partner_type": "customer", + "partner_id": account_move.partner_id.id, + } + ) + ._create_payments() + .move_id + ) diff --git a/base_upflow/tests/json_schema/post-contacts.json b/base_upflow/tests/json_schema/post-contacts.json new file mode 100644 index 000000000..e6ecd2ee0 --- /dev/null +++ b/base_upflow/tests/json_schema/post-contacts.json @@ -0,0 +1,33 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$comment": "source: https://upflow.docs.apiary.io/#reference/0/contacts/create-customer-contact", + "type": "object", + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "email": { + "type": "string" + }, + "position": { + "enum": ["ACCOUNTING", "SALES", "PAYER", "PURCHASER"] + }, + "externalId": { + "type": "string" + }, + "isMain": { + "type": "boolean" + }, + "id": { + "type": "string" + } + }, + "$comment": "TODO: I'm (PV) not sure in customers API endpoint if id is required", + "required": ["email"] +} diff --git a/base_upflow/tests/json_schema/post-credit_notes.json b/base_upflow/tests/json_schema/post-credit_notes.json new file mode 100644 index 000000000..94eee412e --- /dev/null +++ b/base_upflow/tests/json_schema/post-credit_notes.json @@ -0,0 +1,53 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$comment": "source: https://upflow.docs.apiary.io/#reference/0/credit-notes/import-credit-note", + "type": "object", + "properties": { + "customId": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "issuedAt": { + "type": "string" + }, + "dueDate": { + "type": "string" + }, + "name": { + "type": "string" + }, + "currency": { + "anyOf": [ + { + "type": "string" + }, + { + "enum": ["EUR", "CHF", "USD", "CAD", "GBP"] + } + ] + }, + "grossAmount": { + "type": "number" + }, + "netAmount": { + "type": "number" + }, + "customer": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "externalId": { + "type": "string" + } + } + }, + "linkedInvoices": { + "type": "array" + } + }, + "required": ["customId", "currency", "grossAmount", "netAmount"] +} diff --git a/base_upflow/tests/json_schema/post-customers.json b/base_upflow/tests/json_schema/post-customers.json new file mode 100644 index 000000000..8e0ff820d --- /dev/null +++ b/base_upflow/tests/json_schema/post-customers.json @@ -0,0 +1,127 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$comment": "source: https://upflow.docs.apiary.io/#reference/0/customers/import-customer", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "vatNumber": { + "type": "string" + }, + "accountingRef": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "accountManagerId": { + "type": "string" + }, + "dunningPlanId": { + "type": "string" + }, + "address": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "zipcode": { + "type": "string" + }, + "city": { + "type": "string" + }, + "state": { + "type": "string" + }, + "country": { + "type": "string" + } + } + }, + "parent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "externalId": { + "type": "string" + } + } + }, + "paymentMethods": { + "type": "object", + "properties": { + "card": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "check": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "achDebit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "sepaDebit": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "goCardless": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "wireTransfer": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "bankAccount": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, + "bankAccounts": { + "type": "array" + } + } + } + } + }, + "customFields": { + "type": "array" + }, + "contacts": { + "type": "array" + } + }, + "required": ["name"] +} diff --git a/base_upflow/tests/json_schema/post-invoices.json b/base_upflow/tests/json_schema/post-invoices.json new file mode 100644 index 000000000..f23a5795f --- /dev/null +++ b/base_upflow/tests/json_schema/post-invoices.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$comment": "source: https://upflow.docs.apiary.io/#reference/0/invoices/import-invoice", + "type": "object", + "properties": { + "customId": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "issuedAt": { + "type": "string" + }, + "dueDate": { + "type": "string" + }, + "name": { + "type": "string" + }, + "currency": { + "anyOf": [ + { + "type": "string" + }, + { + "enum": ["EUR", "CHF", "USD", "CAD", "GBP"] + } + ] + }, + "grossAmount": { + "type": "number" + }, + "netAmount": { + "type": "number" + }, + "customer": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "externalId": { + "type": "string" + } + } + } + }, + "required": [ + "customId", + "issuedAt", + "dueDate", + "currency", + "grossAmount", + "netAmount", + "customer" + ] +} diff --git a/base_upflow/tests/json_schema/post-payments.json b/base_upflow/tests/json_schema/post-payments.json new file mode 100644 index 000000000..c0322a2b1 --- /dev/null +++ b/base_upflow/tests/json_schema/post-payments.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$comment": "source: https://upflow.docs.apiary.io/#reference/0/payments/import-payment", + "type": "object", + "properties": { + "currency": { + "anyOf": [ + { + "type": "string" + }, + { + "enum": ["EUR", "CHF", "USD", "CAD", "GBP"] + } + ] + }, + "amount": { + "type": "number" + }, + "instrument": { + "anyOf": [ + { + "type": "string" + }, + { + "enum": ["WIRE_TRANSFER", "DIRECT_DEBIT", "CARD", "CASH", "CHECK", "UNKNOWN"] + } + ] + }, + "validatedAt": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "bankAccountId": { + "type": "string" + }, + "linkedInvoices": { + "type": "array" + }, + "customer": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "externalId": { + "type": "string" + } + } + } + }, + "required": ["currency", "amount", "validatedAt"] +} diff --git a/base_upflow/tests/json_schema/post-pdf.json b/base_upflow/tests/json_schema/post-pdf.json new file mode 100644 index 000000000..8573c7c5f --- /dev/null +++ b/base_upflow/tests/json_schema/post-pdf.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$comment": "source: https://upflow.docs.apiary.io/#reference/0/invoices/upload-invoice-pdf and https://upflow.docs.apiary.io/#reference/0/credit-notes/upload-credit-note-pdf", + "type": "object", + "properties": { + "data": { + "type": "string" + }, + "file": { + "type": "string" + } + } +} diff --git a/base_upflow/tests/json_schema/post-reconcile.json b/base_upflow/tests/json_schema/post-reconcile.json new file mode 100644 index 000000000..e0aa52a56 --- /dev/null +++ b/base_upflow/tests/json_schema/post-reconcile.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$comment": "source: https://upflow.docs.apiary.io/#reference/0/operations/reconcile-invoices,-credit-notes,-payments,-refunds", + "type": "object", + "properties": { + "externalId": { + "type": "string" + }, + "invoices": { + "type": "array" + }, + "payments": { + "type": "array" + }, + "creditNotes": { + "type": "array" + }, + "refunds": { + "type": "array" + } + } +} diff --git a/base_upflow/tests/json_schema/post-refunds.json b/base_upflow/tests/json_schema/post-refunds.json new file mode 100644 index 000000000..fdd5f31c1 --- /dev/null +++ b/base_upflow/tests/json_schema/post-refunds.json @@ -0,0 +1,54 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$comment": "source: https://upflow.docs.apiary.io/#reference/0/refunds/import-refund", + "type": "object", + "properties": { + "currency": { + "anyOf": [ + { + "type": "string" + }, + { + "enum": ["EUR", "CHF", "USD", "CAD", "GBP"] + } + ] + }, + "amount": { + "type": "number" + }, + "instrument": { + "anyOf": [ + { + "type": "string" + }, + { + "enum": ["WIRE_TRANSFER", "DIRECT_DEBIT", "CARD", "CASH", "CHECK", "UNKNOWN"] + } + ] + }, + "validatedAt": { + "type": "string" + }, + "externalId": { + "type": "string" + }, + "linkedPayments": { + "type": "array" + }, + "linkedCreditNotes": { + "type": "array" + }, + "customer": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "externalId": { + "type": "string" + } + } + } + }, + "required": ["currency", "amount", "validatedAt"] +} diff --git a/base_upflow/tests/test_res_partner.py b/base_upflow/tests/test_res_partner.py new file mode 100644 index 000000000..eca9d406c --- /dev/null +++ b/base_upflow/tests/test_res_partner.py @@ -0,0 +1,33 @@ +from uuid import uuid4 + +from odoo.tests import SavepointCase + + +class TestResPartner(SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create( + { + "name": "Partner", + "email": "email@example.com", + "phone": "123456789", + "mobile": "123456789", + "street": "Street", + "street2": "Street2", + "zip": "12345", + "city": "City", + } + ) + + def test_uplow_uuid_not_duplicated(self): + self.partner.upflow_uuid = str(uuid4()) + partner2 = self.partner.copy() + self.assertFalse(partner2.upflow_uuid) + self.assertNotEqual(self.partner.upflow_uuid, partner2.upflow_uuid) + + def test_upflow_direct_url_not_duplicate(self): + self.partner.upflow_direct_url = "/invoice/1234" + partner2 = self.partner.copy() + self.assertFalse(partner2.upflow_direct_url) + self.assertNotEqual(self.partner.upflow_direct_url, partner2.upflow_direct_url) diff --git a/base_upflow/tests/test_upflow_post_invoices_payload.py b/base_upflow/tests/test_upflow_post_invoices_payload.py new file mode 100644 index 000000000..a740772c4 --- /dev/null +++ b/base_upflow/tests/test_upflow_post_invoices_payload.py @@ -0,0 +1,563 @@ +# Copyright 2022 Foodles (https://www.foodles.com/) +# @author Pierre Verkest +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import json +import os +from uuid import uuid4 + +from jsonschema import validate + +from odoo.exceptions import UserError +from odoo.tests.common import SavepointCase, tagged + +from .common import AccountingCommonCase + +SCHEMA_DIRECTORY = os.path.join( + os.path.dirname(__file__), + "json_schema", +) + + +@tagged("post_install", "-at_install") +class UpflowAccountMovePayloadTest(SavepointCase, AccountingCommonCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._setup_accounting() + cls.customer_company = 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.contact = cls.env["res.partner"].create( + { + "name": "Jack Flag", + "parent_id": cls.customer_company.id, + "phone": "+33238365503", + "mobile": "+33604060810", + "email": "jack.flag@customer-company.com", + "upflow_position_id": cls.env.ref( + "base_upflow.upflow_res_partner_upflow_position_accounting" + ).id, + } + ) + cls.invoice = cls._create_invoice( + date_invoice="2022-01-01", + partner_id=cls.contact.id, + payment_term_id=cls.pay_terms_multiple.id, + auto_validate=True, + ) + cls.refund = cls._create_invoice( + date_invoice="2022-01-01", + partner_id=cls.contact.id, + move_type="out_refund", + auto_validate=True, + ) + cls.refund_payment_move = cls._register_manual_payment_reconciled(cls.refund) + + def assertValidUpflowPayload(self, schema_file_name, content): + validate( + schema=json.loads( + open(f"{SCHEMA_DIRECTORY}/{schema_file_name}.json").read() + ), + instance=content, + ) + + def test_upflow_uuid_not_duplicate(self): + self.invoice.upflow_uuid = str(uuid4()) + invoice2 = self.invoice.copy() + self.assertFalse(invoice2.upflow_uuid) + self.assertNotEqual(self.invoice.upflow_uuid, invoice2.upflow_uuid) + + def test_upflow_direct_url_not_duplicate(self): + self.invoice.upflow_direct_url = "/invoice/1234" + invoice2 = self.invoice.copy() + self.assertFalse(invoice2.upflow_direct_url) + self.assertNotEqual(self.invoice.upflow_direct_url, invoice2.upflow_direct_url) + + def test_post_invocies_pdf_format(self): + self.assertValidUpflowPayload( + "post-pdf", + self.invoice.get_upflow_api_pdf_payload(), + ) + + def test_post_invocies_format(self): + self.assertValidUpflowPayload( + "post-invoices", + self.invoice.get_upflow_api_post_invoice_payload(), + ) + + def test_post_invoice_payload_add_upflow_id_if_present(self): + uuid = str(uuid4()) + self.invoice.upflow_uuid = uuid + payload = self.invoice.get_upflow_api_post_invoice_payload() + self.assertEqual(payload["id"], uuid) + + def test_get_upflow_api_post_invoices_payload_content(self): + content = self.invoice.get_upflow_api_post_invoice_payload() + self.assertEqual(content["customId"], self.invoice.name) + self.assertEqual(content["externalId"], str(self.invoice.id)) + self.assertEqual(content["issuedAt"], "2022-01-01") + self.assertEqual(content["dueDate"], "2022-02-28") + self.assertEqual(content["name"], self.invoice.name) + self.assertEqual(content["currency"], self.invoice.currency_id.name) + self.assertNotEqual(content["grossAmount"], content["netAmount"]) + self.assertAlmostEqual( + content["grossAmount"], + self.invoice.amount_total * 1 / self.invoice.currency_id.rounding, + ) + self.assertAlmostEqual( + content["netAmount"], + self.invoice.amount_untaxed * 1 / self.invoice.currency_id.rounding, + ) + self.assertEqual( + content["customer"]["externalId"], + str(self.customer_company.id), + ) + + def test_get_upflow_api_post_customers_payload_format(self): + self.assertValidUpflowPayload( + "post-customers", + self.customer_company.get_upflow_api_post_customers_payload(), + ) + + def test_get_upflow_api_post_customers_without_vat_payload_format(self): + self.customer_company.vat = False + self.assertValidUpflowPayload( + "post-customers", + self.customer_company.get_upflow_api_post_customers_payload(), + ) + + def test_post_customers_payload_only_contact_with_email_are_send(self): + self.env["res.partner"].create( + { + "name": "Someone else without email", + "parent_id": self.customer_company.id, + "phone": "+33238365503", + "mobile": "+33604060810", + "email": False, + } + ) + payload = self.customer_company.get_upflow_api_post_customers_payload() + self.assertEqual(len(payload["contacts"]), 1) + self.assertEqual(payload["contacts"][0]["firstName"], self.contact.name) + + def test_post_contact_phone_field_not_define(self): + self.contact.mobile = False + payload = self.contact.get_upflow_api_post_contacts_payload() + self.assertEqual(payload["phone"], "") + + def test_post_customers_streets_field_not_define(self): + # the address field is required by upflow but here we want + # to make sure if one is missing it's not replaced by `False` text + self.customer_company.street = False + self.customer_company.street2 = False + payload = self.customer_company.get_upflow_api_post_customers_payload() + self.assertEqual(payload["address"]["address"], "") + + def test_post_customers_payload_add_upflow_id_if_present(self): + uuid = str(uuid4()) + self.customer_company.commercial_partner_id.upflow_uuid = uuid + payload = self.customer_company.get_upflow_api_post_customers_payload() + self.assertEqual(payload["id"], uuid) + + def test_get_upflow_api_post_customers_payload_content(self): + content = self.customer_company.get_upflow_api_post_customers_payload() + self.assertEqual(content["name"], "My customer company") + self.assertEqual(content["vatNumber"], "FR23334175221") + self.assertEqual(content["externalId"], str(self.customer_company.id)) + self.assertEqual(content["address"]["address"], "Street 1 and more") + self.assertEqual(content["address"]["zipcode"], "45500") + self.assertEqual(content["address"]["city"], "Customer city") + self.assertEqual(content["address"]["state"], "") + self.assertEqual(content["address"]["country"], "") + + def test_get_upflow_api_post_contacts_payload_format(self): + self.assertValidUpflowPayload( + "post-contacts", + self.contact.get_upflow_api_post_contacts_payload(), + ) + + def test_post_contacts_payload_add_upflow_id_if_present(self): + customer_uuid = str(uuid4()) + contact_uuid = str(uuid4()) + self.customer_company.upflow_uuid = customer_uuid + self.contact.upflow_uuid = contact_uuid + payload = self.contact.get_upflow_api_post_contacts_payload() + self.assertEqual(payload["id"], contact_uuid) + payload = self.contact.get_upflow_api_post_customers_payload() + self.assertEqual(payload["id"], customer_uuid) + + def test_get_upflow_api_post_contacts_payload_content(self): + content = self.contact.get_upflow_api_post_contacts_payload() + self.assertEqual(content["firstName"], "Jack Flag") + # self.assertEqual(content["lastName"], "") + self.assertEqual(content["phone"], "+33604060810") + self.assertEqual(content["email"], "jack.flag@customer-company.com") + self.assertEqual(content["position"], "ACCOUNTING") + self.assertEqual(content["externalId"], str(self.contact.id)) + # self.assertEqual(content["isMain"], True) + + def test_get_upflow_api_post_contacts_payload_without_position(self): + self.contact.upflow_position_id = False + content = self.contact.get_upflow_api_post_contacts_payload() + self.assertEqual("position" in content, False) + + def test_get_upflow_api_post_customers_payload_same_on_contact(self): + self.assertEqual( + self.customer_company.get_upflow_api_post_customers_payload(), + self.contact.get_upflow_api_post_customers_payload(), + ) + + def test_get_upflow_api_post_credit_notes_payload_format(self): + """refund in odoo <=> credit notes in upflow.io""" + self.assertValidUpflowPayload( + "post-credit_notes", + self.refund.get_upflow_api_post_credit_note_payload(), + ) + + def test_post_credit_notes_pdf_format(self): + self.assertValidUpflowPayload( + "post-pdf", + self.invoice.get_upflow_api_pdf_payload(), + ) + + def test_post_credit_note_payload_add_upflow_id_if_present(self): + uuid = str(uuid4()) + self.refund.upflow_uuid = uuid + payload = self.refund.get_upflow_api_post_credit_note_payload() + self.assertEqual(payload["id"], uuid) + + def test_get_upflow_api_post_credit_notes_payload_content(self): + """refund in odoo <=> credit notes in upflow.io""" + content = self.refund.get_upflow_api_post_credit_note_payload() + self.assertEqual(content["customId"], self.refund.name) + self.assertEqual(content["externalId"], str(self.refund.id)) + self.assertEqual(content["issuedAt"], "2022-01-01") + self.assertEqual(content["dueDate"], "2022-01-01") + self.assertEqual(content["name"], self.refund.name) + self.assertEqual(content["currency"], self.refund.currency_id.name) + self.assertNotEqual(content["grossAmount"], content["netAmount"]) + self.assertAlmostEqual( + content["grossAmount"], + self.refund.amount_total * 1 / self.refund.currency_id.rounding, + ) + self.assertAlmostEqual( + content["netAmount"], + self.refund.amount_untaxed * 1 / self.refund.currency_id.rounding, + ) + self.assertEqual( + content["customer"]["externalId"], + 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") + self.invoice.currency_id = currency_dynar + self.assertEqual(self.invoice._format_upflow_amount(12.258), 12258) + self.assertEqual( + self.invoice._format_upflow_amount(12.258, currency=currency_euro), 1226 + ) + self.invoice.currency_id = currency_euro + self.assertEqual(self.invoice._format_upflow_amount(12.258), 1226) + self.assertEqual( + self.invoice._format_upflow_amount(12.258, currency=currency_dynar), 12258 + ) + + def test_get_upflow_api_post_payments_payload_format(self): + invoice_payment_move = self._register_manual_payment_reconciled(self.invoice) + self.assertValidUpflowPayload( + "post-payments", + invoice_payment_move.get_upflow_api_post_payment_payload(), + ) + invoice_payment_move.journal_id.upflow_bank_account_uuid = "abcd" + self.assertValidUpflowPayload( + "post-payments", + invoice_payment_move.get_upflow_api_post_payment_payload(), + ) + + def test_post_payment_payload_add_upflow_id_if_present(self): + invoice_payment_move = self._register_manual_payment_reconciled(self.invoice) + uuid = str(uuid4()) + invoice_payment_move.upflow_uuid = uuid + payload = invoice_payment_move.get_upflow_api_post_payment_payload() + self.assertEqual(payload["id"], uuid) + + def test_get_upflow_api_post_payments_payload(self): + """customer invoice payement (received from customer)""" + invoice_payment_move = self._register_manual_payment_reconciled(self.invoice) + content = invoice_payment_move.get_upflow_api_post_payment_payload() + self.assertEqual(content["currency"], self.refund.currency_id.name) + self.assertAlmostEqual( + content["amount"], + self.refund.amount_total * 1 / self.refund.currency_id.rounding, + ) + self.assertEqual(content["externalId"], str(invoice_payment_move.id)) + self.assertEqual(content["validatedAt"], "2022-02-28") + + self.assertEqual( + content["customer"]["externalId"], + str(self.customer_company.id), + ) + + def test_get_upflow_api_post_payments_payload_without_payment(self): + """customer invoice payement (received from customer)""" + invoice_payment_move = self._make_credit_transfer_payment_reconciled( + self.invoice + ) + + self.assertValidUpflowPayload( + "post-payments", + invoice_payment_move.get_upflow_api_post_payment_payload(), + ) + + content = invoice_payment_move.get_upflow_api_post_payment_payload() + self.assertEqual(content["currency"], self.refund.currency_id.name) + self.assertAlmostEqual( + content["amount"], + self.refund.amount_total * 1 / self.refund.currency_id.rounding, + ) + self.assertEqual(content["externalId"], str(invoice_payment_move.id)) + self.assertEqual(content["validatedAt"], "2022-02-28") + + self.assertEqual( + content["customer"]["externalId"], + str(self.customer_company.id), + ) + + def test_get_upflow_api_post_refunds_payload_format(self): + self.assertValidUpflowPayload( + "post-refunds", + self.refund_payment_move.get_upflow_api_post_refund_payload(), + ) + self.refund_payment_move.journal_id.upflow_bank_account_uuid = "abcd" + self.assertValidUpflowPayload( + "post-payments", + self.refund_payment_move.get_upflow_api_post_refund_payload(), + ) + + def test_post_refund_payload_add_upflow_id_if_present(self): + uuid = str(uuid4()) + self.refund_payment_move.upflow_uuid = uuid + payload = self.refund_payment_move.get_upflow_api_post_refund_payload() + self.assertEqual(payload["id"], uuid) + + def test_get_upflow_api_post_refunds_payload(self): + """customer refund payement (send to customer)""" + content = self.refund_payment_move.get_upflow_api_post_refund_payload() + self.assertEqual(content["currency"], self.refund.currency_id.name) + self.assertAlmostEqual( + content["amount"], + self.refund.amount_total * 1 / self.refund.currency_id.rounding, + ) + self.assertEqual(content["externalId"], str(self.refund_payment_move.id)) + self.assertEqual(content["validatedAt"], "2022-01-01") + + self.assertEqual( + content["customer"]["externalId"], + str(self.customer_company.id), + ) + + def test_get_upflow_api_post_reconcile_payload_multi_link(self): + """customer invoice 600€ reconciled with 3 kind : + + * customer payment 100€ (Using GUI manual interface or batch payment) + * customer payment from bank statement 200€ + (in such case there are no account.payment generated) + * customer refund 300€ + """ + vat_ids = ( + self.env["account.tax"] + .search( + [ + ("type_tax_use", "=", "sale"), + ("company_id", "=", self.env.company.id), + ], + limit=1, + ) + .ids + ) + invoice = self._create_invoice( + unit_price=600, vat_ids=vat_ids, partner_id=self.contact, auto_validate=True + ) + total_due_amount = invoice.amount_residual + invoice.upflow_uuid = str(uuid4()) + manual_payment_move = self._register_manual_payment_reconciled( + invoice, amount=100 + ) + manual_payment_move.upflow_uuid = str(uuid4()) + self.assertAlmostEqual(invoice.amount_residual, total_due_amount - 100) + refund = self._create_invoice( + move_type="out_refund", + unit_price=300, + vat_ids=vat_ids, + partner_id=self.contact, + auto_validate=True, + ) + refund.upflow_uuid = str(uuid4()) + (invoice.line_ids | refund.line_ids).filtered( + lambda line: line.account_id.reconcile + ).reconcile() + direct_transfer_amount = total_due_amount - 100 - refund.amount_total + self.assertAlmostEqual(invoice.amount_residual, direct_transfer_amount) + direct_transfer_move = self._make_credit_transfer_payment_reconciled( + invoice, + amount=direct_transfer_amount, + reconcile_param=[ + { + "id": invoice.line_ids.filtered( + lambda line: line.account_internal_type + in ("receivable", "payable") + ).id + } + ], + ) + direct_transfer_move.upflow_uuid = str(uuid4()) + self.assertEqual(invoice.amount_residual, 0) + full_reconcile = invoice.mapped("line_ids.full_reconcile_id") + reconcile_content = full_reconcile.get_upflow_api_post_reconcile_payload() + self.assertValidUpflowPayload( + "post-reconcile", + reconcile_content, + ) + + def convert_to_cent(euro_amount): + return int(euro_amount * 100) + + expected = { + "externalId": str(full_reconcile.id), + "invoices": [ + { + "id": invoice.upflow_uuid, + "externalId": str(invoice.id), + "customId": invoice.name, + "amountLinked": convert_to_cent(100), + }, + { + "id": invoice.upflow_uuid, + "externalId": str(invoice.id), + "customId": invoice.name, + "amountLinked": convert_to_cent(refund.amount_total), + }, + { + "id": invoice.upflow_uuid, + "externalId": str(invoice.id), + "customId": invoice.name, + "amountLinked": convert_to_cent(direct_transfer_amount), + }, + ], + "payments": [ + { + "id": manual_payment_move.upflow_uuid, + "externalId": str(manual_payment_move.id), + "amountLinked": convert_to_cent(100), + }, + { + "id": direct_transfer_move.upflow_uuid, + "externalId": str(direct_transfer_move.id), + "amountLinked": convert_to_cent(direct_transfer_amount), + }, + ], + "creditNotes": [ + { + "id": refund.upflow_uuid, + "externalId": str(refund.id), + "customId": refund.name, + "amountLinked": convert_to_cent(refund.amount_total), + } + ], + "refunds": [], + } + self.maxDiff = None + self.assertEqual(reconcile_content, expected) + + def test_get_upflow_api_post_reconcile_refund_payload(self): + """customer refund reconciled refund payment""" + + vat_ids = ( + self.env["account.tax"] + .search( + [ + ("type_tax_use", "=", "sale"), + ("company_id", "=", self.env.company.id), + ], + limit=1, + ) + .ids + ) + refund = self._create_invoice( + move_type="out_refund", + unit_price=150, + vat_ids=vat_ids, + partner_id=self.contact, + auto_validate=True, + ) + + manual_payment_move = self._register_manual_payment_reconciled( + refund, amount=refund.amount_total + ) + full_reconcile = refund.mapped("line_ids.full_reconcile_id") + reconcile_content = full_reconcile.get_upflow_api_post_reconcile_payload() + self.assertValidUpflowPayload( + "post-reconcile", + reconcile_content, + ) + + expected = { + "externalId": str(full_reconcile.id), + "invoices": [], + "payments": [], + "creditNotes": [ + { + # "id": "00a70b35-2be3-4c43-aefb-397190134655", + "externalId": str(refund.id), + "customId": refund.name, + "amountLinked": int(refund.amount_total * 100), + } + ], + "refunds": [ + { + # "id": "00a70b35-2be3-4c43-aefb-397190134655", + "externalId": str(manual_payment_move.id), + "amountLinked": int(refund.amount_total * 100), + }, + ], + } + self.maxDiff = None + self.assertEqual(reconcile_content, expected) + + def test_post_reconcile_payload_add_upflow_id_if_present(self): + self._register_manual_payment_reconciled(self.invoice) + full_reconcile = self.invoice.mapped("line_ids.full_reconcile_id") + payload = full_reconcile.get_upflow_api_post_reconcile_payload() + self.assertEqual(payload["externalId"], str(full_reconcile.id)) + + 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() + self.assertEqual(content.get("isMain"), False) + + def test_get_upflow_api_post_contacts_payload_with_main_id(self): + self.customer_company.main_contact_id = self.contact.id + content = self.contact.get_upflow_api_post_contacts_payload() + self.assertEqual(content.get("isMain"), True) diff --git a/base_upflow/views/account_journal.xml b/base_upflow/views/account_journal.xml new file mode 100644 index 000000000..d5cb94919 --- /dev/null +++ b/base_upflow/views/account_journal.xml @@ -0,0 +1,20 @@ + + + + + + account.journal.form + account.journal + 1 + + + + + + + + + + diff --git a/base_upflow/views/account_move.xml b/base_upflow/views/account_move.xml new file mode 100644 index 000000000..fb1e51f22 --- /dev/null +++ b/base_upflow/views/account_move.xml @@ -0,0 +1,32 @@ + + + + + + account.move.form + account.move + + + + + + + + + + + + + diff --git a/base_upflow/views/menu.xml b/base_upflow/views/menu.xml new file mode 100644 index 000000000..f6dbf757f --- /dev/null +++ b/base_upflow/views/menu.xml @@ -0,0 +1,26 @@ + + + + + + Position + res.partner.upflow.position + tree,form + + + + + + + diff --git a/base_upflow/views/res_partner.xml b/base_upflow/views/res_partner.xml new file mode 100644 index 000000000..0c4632ff7 --- /dev/null +++ b/base_upflow/views/res_partner.xml @@ -0,0 +1,65 @@ + + + + + res.partner.form.inherit + res.partner + + + + + + + + + + + + + + + + + + + + + + + res.partner.form.account.property + res.partner + + + + + + + + + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..49393dcf4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +jsonschema diff --git a/setup/base_upflow/odoo/addons/base_upflow b/setup/base_upflow/odoo/addons/base_upflow new file mode 120000 index 000000000..96403e368 --- /dev/null +++ b/setup/base_upflow/odoo/addons/base_upflow @@ -0,0 +1 @@ +../../../../base_upflow \ No newline at end of file diff --git a/setup/base_upflow/setup.py b/setup/base_upflow/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/base_upflow/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)