From 251c36bf6bf023ad44c596cf4d0e40a7eab85660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Mart=C3=ADnez?= Date: Mon, 11 Mar 2024 13:27:46 +0100 Subject: [PATCH] [16.0][ADD] account_banking_mandate_sale_contact: new module --- .../README.rst | 104 ++++ .../__init__.py | 3 + .../__manifest__.py | 15 + .../models/__init__.py | 6 + .../models/res_company.py | 31 ++ .../models/res_config_settings.py | 14 + .../models/res_partner.py | 36 ++ .../models/sale_order.py | 40 ++ .../readme/CONFIGURE.md | 7 + .../readme/CONTRIBUTORS.md | 3 + .../readme/DESCRIPTION.md | 3 + .../readme/USAGE.md | 8 + .../static/description/icon.png | Bin 0 -> 35820 bytes .../static/description/index.html | 446 ++++++++++++++++++ .../tests/__init__.py | 3 + ...st_account_banking_mandate_sale_contact.py | 154 ++++++ .../views/res_config_settings.xml | 26 + .../views/res_partner.xml | 21 + .../account_banking_mandate_sale_contact | 1 + .../setup.py | 6 + 20 files changed, 927 insertions(+) create mode 100644 account_banking_mandate_sale_contact/README.rst create mode 100644 account_banking_mandate_sale_contact/__init__.py create mode 100644 account_banking_mandate_sale_contact/__manifest__.py create mode 100644 account_banking_mandate_sale_contact/models/__init__.py create mode 100644 account_banking_mandate_sale_contact/models/res_company.py create mode 100644 account_banking_mandate_sale_contact/models/res_config_settings.py create mode 100644 account_banking_mandate_sale_contact/models/res_partner.py create mode 100644 account_banking_mandate_sale_contact/models/sale_order.py create mode 100644 account_banking_mandate_sale_contact/readme/CONFIGURE.md create mode 100644 account_banking_mandate_sale_contact/readme/CONTRIBUTORS.md create mode 100644 account_banking_mandate_sale_contact/readme/DESCRIPTION.md create mode 100644 account_banking_mandate_sale_contact/readme/USAGE.md create mode 100644 account_banking_mandate_sale_contact/static/description/icon.png create mode 100644 account_banking_mandate_sale_contact/static/description/index.html create mode 100644 account_banking_mandate_sale_contact/tests/__init__.py create mode 100644 account_banking_mandate_sale_contact/tests/test_account_banking_mandate_sale_contact.py create mode 100644 account_banking_mandate_sale_contact/views/res_config_settings.xml create mode 100644 account_banking_mandate_sale_contact/views/res_partner.xml create mode 120000 setup/account_banking_mandate_sale_contact/odoo/addons/account_banking_mandate_sale_contact create mode 100644 setup/account_banking_mandate_sale_contact/setup.py diff --git a/account_banking_mandate_sale_contact/README.rst b/account_banking_mandate_sale_contact/README.rst new file mode 100644 index 000000000..0a66b1f0e --- /dev/null +++ b/account_banking_mandate_sale_contact/README.rst @@ -0,0 +1,104 @@ +==================================== +Account Banking Mandate Sale Contact +==================================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:7cfbc94c4688e616b2efe836942f2a7d58ae4b7c7fecd870882b8d4b2942a810 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fbank--payment-lightgray.png?logo=github + :target: https://github.com/OCA/bank-payment/tree/16.0/account_banking_mandate_sale_contact + :alt: OCA/bank-payment +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/bank-payment-16-0/bank-payment-16-0-account_banking_mandate_sale_contact + :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/bank-payment&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module combines the functionality of account_banking_mandate_sale +with account_banking_mandate_contact and to allows you to add a specific +contact mandate to sale orders. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To configure this module, you need to: + +#. Go to Settings/Sales/Invoicing and select the Default Mandates +option. This allows you to choose if you want the mandate of the sale +partner, invoice address or delivery address. + +#. If you want to specifically change the default mandate for a +customer, you can go to the "Sales & Purchase" tab of his contact form. + +Usage +===== + +For selecting the mandate at contact level: + +#. Go to *Invoicing > Customers > Customers*. #. Open or create one +contact. #. On the "Sales & Purchase" page, fill *Contact Mandate*. + +Then, when you select a payment mode that requires mandate on a sale +order, Odoo will choose the mandate selected at contact level. That +mandate will be copied from the sale order to the invoice. + +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 +------- + +* Alberto Martínez + +Contributors +------------ + +- ``Sygel ``\ \_: + + - Alberto Martínez + +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. + +This module is part of the `OCA/bank-payment `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_banking_mandate_sale_contact/__init__.py b/account_banking_mandate_sale_contact/__init__.py new file mode 100644 index 000000000..31660d6a9 --- /dev/null +++ b/account_banking_mandate_sale_contact/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models diff --git a/account_banking_mandate_sale_contact/__manifest__.py b/account_banking_mandate_sale_contact/__manifest__.py new file mode 100644 index 000000000..3ea2f6ac0 --- /dev/null +++ b/account_banking_mandate_sale_contact/__manifest__.py @@ -0,0 +1,15 @@ +# Copyright 2024 Alberto Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Account Banking Mandate Sale Contact", + "summary": "Add a specific contact mandate to sale orders", + "version": "16.0.1.0.0", + "category": "Banking addons", + "website": "https://github.com/OCA/bank-payment", + "author": "Alberto Martínez, Odoo Community Association (OCA)", + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": ["account_banking_mandate_contact", "account_banking_mandate_sale"], + "data": ["views/res_config_settings.xml", "views/res_partner.xml"], +} diff --git a/account_banking_mandate_sale_contact/models/__init__.py b/account_banking_mandate_sale_contact/models/__init__.py new file mode 100644 index 000000000..d9c0dc324 --- /dev/null +++ b/account_banking_mandate_sale_contact/models/__init__.py @@ -0,0 +1,6 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import res_company +from . import res_config_settings +from . import res_partner +from . import sale_order diff --git a/account_banking_mandate_sale_contact/models/res_company.py b/account_banking_mandate_sale_contact/models/res_company.py new file mode 100644 index 000000000..261512d37 --- /dev/null +++ b/account_banking_mandate_sale_contact/models/res_company.py @@ -0,0 +1,31 @@ +# Copyright 2024 Alberto Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + sale_default_mandate_contact = fields.Selection( + selection=[ + ("partner_id", "Customer Mandate"), + ("commercial_partner_id", "Commercial Customer Mandate"), + ("partner_invoice_id", "Invoice Address Mandate"), + ("partner_shipping_id", "Delivery Address Mandate"), + ], + string="Default Sale Mandate Contact", + default="partner_id", + help="The contact of this company in which odoo will search for the mandate on sales\n" + "- Customer Mandate: Odoo will look the mandate in the sale partner," + " whether is an individual or the company\n" + "- Commercial Customer Mandate: Odoo will look the mandate in the" + " sale partner company\n" + "- Invoice Address Mandate: Odoo will look the mandate in the" + " sale invoice address\n" + "- Delivery Address Mandate: Odoo will look the mandate in the" + " sale delivery address\n" + "- False: Odoo will use the first mandate he founds for the partner company." + " Odoo will also use this option if no default mandate is found in the" + " partner of the above options", + ) diff --git a/account_banking_mandate_sale_contact/models/res_config_settings.py b/account_banking_mandate_sale_contact/models/res_config_settings.py new file mode 100644 index 000000000..a900fea6f --- /dev/null +++ b/account_banking_mandate_sale_contact/models/res_config_settings.py @@ -0,0 +1,14 @@ +# Copyright 2024 Alberto Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + sale_default_mandate_contact = fields.Selection( + string="Default Sale Mandate Contact", + related="company_id.sale_default_mandate_contact", + readonly=False, + ) diff --git a/account_banking_mandate_sale_contact/models/res_partner.py b/account_banking_mandate_sale_contact/models/res_partner.py new file mode 100644 index 000000000..6e00d76ea --- /dev/null +++ b/account_banking_mandate_sale_contact/models/res_partner.py @@ -0,0 +1,36 @@ +# Copyright 2024 Alberto Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + sale_default_mandate_contact = fields.Selection( + selection=[ + ("partner_id", "Customer Mandate"), + ("commercial_partner_id", "Commercial Customer Mandate"), + ("partner_invoice_id", "Invoice Address Mandate"), + ("partner_shipping_id", "Delivery Address Mandate"), + ], + string="Default Sale Mandate Contact", + help="The partner of the sales in which odoo will search for the mandate\n" + "- Customer Mandate: Odoo will look the mandate in the sale partner," + " whether is an individual or the company\n" + "- Commercial Customer Mandate: Odoo will look the mandate in the" + " sale partner company\n" + "- Invoice Address Mandate: Odoo will look the mandate in the" + " sale invoice address\n" + "- Delivery Address Mandate: Odoo will look the mandate in the" + " sale delivery address\n" + "- False: Odoo will use the first mandate he founds for the partner company." + " Odoo will also use this option if no default mandate is found in the" + " partner of the above options", + ) + + @api.model + def _commercial_fields(self): + return super()._commercial_fields() + [ + "sale_default_mandate_contact", + ] diff --git a/account_banking_mandate_sale_contact/models/sale_order.py b/account_banking_mandate_sale_contact/models/sale_order.py new file mode 100644 index 000000000..d0a5980ed --- /dev/null +++ b/account_banking_mandate_sale_contact/models/sale_order.py @@ -0,0 +1,40 @@ +# Copyright 2024 Alberto Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + @api.depends( + "partner_id", "partner_invoice_id", "partner_shipping_id", "payment_mode_id" + ) + def _compute_mandate_id(self): + procesed_orders = self.browse() + for order in self: + if ( + order.partner_invoice_id + and order.payment_mode_id + and order.payment_mode_id.payment_method_id.mandate_required + ): + partner_mandate_config = ( + order.commercial_invoice_partner_id.sale_default_mandate_contact + or order.company_id.sale_default_mandate_contact + ) + if partner_mandate_config: + mandate = False + if partner_mandate_config == "partner_id": + mandate = order.partner_id.contact_mandate_id + if partner_mandate_config == "commercial_partner_id": + mandate = ( + order.partner_id.commercial_partner_id.contact_mandate_id + ) + elif partner_mandate_config == "partner_invoice_id": + mandate = order.partner_invoice_id.contact_mandate_id + elif partner_mandate_config == "partner_shipping_id": + mandate = order.partner_shipping_id.contact_mandate_id + if mandate: + order.mandate_id = mandate + procesed_orders |= order + return super(SaleOrder, self - procesed_orders)._compute_mandate_id() diff --git a/account_banking_mandate_sale_contact/readme/CONFIGURE.md b/account_banking_mandate_sale_contact/readme/CONFIGURE.md new file mode 100644 index 000000000..e0bc5adb3 --- /dev/null +++ b/account_banking_mandate_sale_contact/readme/CONFIGURE.md @@ -0,0 +1,7 @@ +To configure this module, you need to: + +#. Go to Settings/Sales/Invoicing and select the Default Mandates option. + This allows you to choose if you want the mandate of the sale partner, + invoice address or delivery address. + +#. If you want to specifically change the default mandate for a customer, you can go to the "Sales & Purchase" tab of his contact form. diff --git a/account_banking_mandate_sale_contact/readme/CONTRIBUTORS.md b/account_banking_mandate_sale_contact/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..736ea050a --- /dev/null +++ b/account_banking_mandate_sale_contact/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +* `Sygel `_: + + * Alberto Martínez \ No newline at end of file diff --git a/account_banking_mandate_sale_contact/readme/DESCRIPTION.md b/account_banking_mandate_sale_contact/readme/DESCRIPTION.md new file mode 100644 index 000000000..ff1a091d2 --- /dev/null +++ b/account_banking_mandate_sale_contact/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +This module combines the functionality of account_banking_mandate_sale +with account_banking_mandate_contact and to allows you to +add a specific contact mandate to sale orders. diff --git a/account_banking_mandate_sale_contact/readme/USAGE.md b/account_banking_mandate_sale_contact/readme/USAGE.md new file mode 100644 index 000000000..03880ef79 --- /dev/null +++ b/account_banking_mandate_sale_contact/readme/USAGE.md @@ -0,0 +1,8 @@ +For selecting the mandate at contact level: + +#. Go to *Invoicing > Customers > Customers*. +#. Open or create one contact. +#. On the "Sales & Purchase" page, fill *Contact Mandate*. + +Then, when you select a payment mode that requires mandate on a sale order, Odoo will +choose the mandate selected at contact level. That mandate will be copied from the sale order to the invoice. diff --git a/account_banking_mandate_sale_contact/static/description/icon.png b/account_banking_mandate_sale_contact/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..207fb7ad5535215836f426bcebfdccdbc51c0d27 GIT binary patch literal 35820 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 + + + + + +Account Banking Mandate Sale Contact + + + +
+

Account Banking Mandate Sale Contact

+ + +

Beta License: AGPL-3 OCA/bank-payment Translate me on Weblate Try me on Runboat

+

This module combines the functionality of account_banking_mandate_sale +with account_banking_mandate_contact and to allows you to add a specific +contact mandate to sale orders.

+

Table of contents

+ +
+

Configuration

+

To configure this module, you need to:

+

#. Go to Settings/Sales/Invoicing and select the Default Mandates +option. This allows you to choose if you want the mandate of the sale +partner, invoice address or delivery address.

+

#. If you want to specifically change the default mandate for a +customer, you can go to the “Sales & Purchase” tab of his contact form.

+
+
+

Usage

+

For selecting the mandate at contact level:

+

#. Go to Invoicing > Customers > Customers. #. Open or create one +contact. #. On the “Sales & Purchase” page, fill Contact Mandate.

+

Then, when you select a payment mode that requires mandate on a sale +order, Odoo will choose the mandate selected at contact level. That +mandate will be copied from the sale order to the invoice.

+
+
+

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

+
    +
  • Alberto Martínez
  • +
+
+
+

Contributors

+
    +
  • Sygel <https://www.sygel.es>_:
      +
    • Alberto Martínez
    • +
    +
  • +
+
+
+

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.

+

This module is part of the OCA/bank-payment project on GitHub.

+

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

+
+
+
+ + diff --git a/account_banking_mandate_sale_contact/tests/__init__.py b/account_banking_mandate_sale_contact/tests/__init__.py new file mode 100644 index 000000000..bde316600 --- /dev/null +++ b/account_banking_mandate_sale_contact/tests/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_account_banking_mandate_sale_contact diff --git a/account_banking_mandate_sale_contact/tests/test_account_banking_mandate_sale_contact.py b/account_banking_mandate_sale_contact/tests/test_account_banking_mandate_sale_contact.py new file mode 100644 index 000000000..048db6ea5 --- /dev/null +++ b/account_banking_mandate_sale_contact/tests/test_account_banking_mandate_sale_contact.py @@ -0,0 +1,154 @@ +# Copyright 2024 Alberto Martínez +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from unittest.mock import patch + +from odoo import fields +from odoo.tests.common import Form, TransactionCase + +from odoo.addons.account.models.account_payment_method import AccountPaymentMethod + + +class TestAccountBankingMandateSaleContact(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner_company = cls.env["res.partner"].create( + { + "name": "Test Partner Company", + "company_type": "company", + } + ) + cls.partner_invoice = cls.env["res.partner"].create( + { + "name": "Test Partner Invoice Address", + "company_type": "person", + "type": "invoice", + "parent_id": cls.partner_company.id, + } + ) + cls.partner_delivery = cls.env["res.partner"].create( + { + "name": "Test Partner Delivery Address", + "company_type": "person", + "type": "delivery", + "parent_id": cls.partner_company.id, + } + ) + + cls.partner_bank = cls._create_res_partner_bank( + cls.partner_company, "Test Bank" + ) + cls.mandate_first = cls._create_mandate(cls.partner_bank, "Test Mandate") + cls.mandate_company = cls._create_mandate( + cls.partner_bank, "Test Company Mandate" + ) + cls.mandate_invoice = cls._create_mandate( + cls.partner_bank, "Test Invoice Mandate" + ) + cls.mandate_delivery = cls._create_mandate( + cls.partner_bank, "Test Delivery Mandate" + ) + cls.payment_method = cls._create_payment_method( + { + "name": "Test Payment Method", + "code": "test_payment_method", + "payment_type": "inbound", + "bank_account_required": True, + "mandate_required": True, + } + ) + cls.journal_bank = cls.env["account.journal"].create( + {"name": "Test Journal", "type": "bank", "code": "bank"} + ) + payment_form = Form(cls.env["account.payment.mode"]) + payment_form.name = "Test Payment Mode" + payment_form.payment_method_id = cls.payment_method + payment_form.bank_account_link = "fixed" + payment_form.fixed_journal_id = cls.journal_bank + payment_form.payment_order_ok = True + cls.payment_mode = payment_form.save() + cls.partner_company.update( + { + "customer_payment_mode_id": cls.payment_mode.id, + "contact_mandate_id": cls.mandate_company.id, + } + ) + cls.partner_invoice.contact_mandate_id = cls.mandate_invoice + cls.partner_delivery.contact_mandate_id = cls.mandate_delivery + + @classmethod + def _create_res_partner_bank(cls, partner_id, acc_number): + res_partner_bank_form = Form(cls.env["res.partner.bank"]) + res_partner_bank_form.partner_id = partner_id + res_partner_bank_form.acc_number = acc_number + return res_partner_bank_form.save() + + @classmethod + def _create_mandate(cls, partner_bank, scheme): + mandate_form = Form(cls.env["account.banking.mandate"]) + mandate_form.partner_bank_id = partner_bank + mandate_form.signature_date = fields.Date.from_string("2021-01-01") + mandate = mandate_form.save() + mandate.validate() + return mandate + + @classmethod + def _create_payment_method(cls, payment_method_vals): + method_get_payment_method_information = ( + AccountPaymentMethod._get_payment_method_information + ) + + def _get_payment_method_information(cls): + res = method_get_payment_method_information(cls) + res[payment_method_vals["code"]] = { + "mode": "multi", + "domain": [("type", "=", "bank")], + } + return res + + with patch.object( + AccountPaymentMethod, + "_get_payment_method_information", + _get_payment_method_information, + ): + return cls.env["account.payment.method"].create(payment_method_vals) + + def test_sale_mandate(self): + """Tests the computed sale mandate with the default company configuration""" + sale_form = Form(self.env["sale.order"].with_context()) + sale_form.partner_id = self.partner_company + sale = sale_form.save() + self.assertEqual(sale.mandate_id, self.mandate_company) + + def test_sale_mandate_before(self): + """Tests the default sale mendate before this module, the first mandate found""" + self.env.user.company_id.sale_default_mandate_contact = False + sale_form = Form(self.env["sale.order"].with_context()) + sale_form.partner_id = self.partner_company + sale = sale_form.save() + self.assertEqual(sale.mandate_id, self.mandate_first) + + def test_sale_mandate_invoice_address(self): + """Tests the computed sale mendate with a config based on invoice address""" + self.partner_company.sale_default_mandate_contact = "partner_invoice_id" + sale_form = Form(self.env["sale.order"].with_context()) + sale_form.partner_id = self.partner_company + sale = sale_form.save() + self.assertEqual(sale.mandate_id, self.mandate_invoice) + + def test_sale_mandate_delivery_address(self): + """Tests the computed sale mendate with a config based on delivery address""" + self.partner_company.sale_default_mandate_contact = "partner_shipping_id" + sale_form = Form(self.env["sale.order"].with_context()) + sale_form.partner_id = self.partner_company + sale = sale_form.save() + self.assertEqual(sale.mandate_id, self.mandate_delivery) + + def test_sale_mandate_commercial_partner(self): + """Tests the computed sale mendate with a config based on delivery address""" + self.partner_company.sale_default_mandate_contact = "commercial_partner_id" + sale_form = Form(self.env["sale.order"].with_context()) + sale_form.partner_id = self.partner_invoice + sale = sale_form.save() + self.assertEqual(sale.mandate_id, self.mandate_company) diff --git a/account_banking_mandate_sale_contact/views/res_config_settings.xml b/account_banking_mandate_sale_contact/views/res_config_settings.xml new file mode 100644 index 000000000..bf41713ce --- /dev/null +++ b/account_banking_mandate_sale_contact/views/res_config_settings.xml @@ -0,0 +1,26 @@ + + + + + settings.view.form.sale.warning + res.config.settings + + + +
+
+
+ Default Mandates +
+ The partner of the sales in which odoo will search for the mandate +
+
+ +
+
+
+ + + + diff --git a/account_banking_mandate_sale_contact/views/res_partner.xml b/account_banking_mandate_sale_contact/views/res_partner.xml new file mode 100644 index 000000000..bc4577942 --- /dev/null +++ b/account_banking_mandate_sale_contact/views/res_partner.xml @@ -0,0 +1,21 @@ + + + + + res.partner + + + + + + + + + diff --git a/setup/account_banking_mandate_sale_contact/odoo/addons/account_banking_mandate_sale_contact b/setup/account_banking_mandate_sale_contact/odoo/addons/account_banking_mandate_sale_contact new file mode 120000 index 000000000..776a7585e --- /dev/null +++ b/setup/account_banking_mandate_sale_contact/odoo/addons/account_banking_mandate_sale_contact @@ -0,0 +1 @@ +../../../../account_banking_mandate_sale_contact \ No newline at end of file diff --git a/setup/account_banking_mandate_sale_contact/setup.py b/setup/account_banking_mandate_sale_contact/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/account_banking_mandate_sale_contact/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)