diff --git a/subscription_oca/README.rst b/subscription_oca/README.rst index d0f97a0c55..eda10b7f3c 100644 --- a/subscription_oca/README.rst +++ b/subscription_oca/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ======================= Subscription management ======================= @@ -17,7 +13,7 @@ Subscription management .. |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/license-AGPL--3-blue.png +.. |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%2Fcontract-lightgray.png?logo=github @@ -91,6 +87,13 @@ Contributors * Ilyas + +* `Binhex `__: + + * Adasat Torres de Leon + +* Chris Mann + Maintainers ~~~~~~~~~~~ diff --git a/subscription_oca/models/sale_subscription.py b/subscription_oca/models/sale_subscription.py index 30fd0ae9b2..a58bbc30d0 100644 --- a/subscription_oca/models/sale_subscription.py +++ b/subscription_oca/models/sale_subscription.py @@ -103,6 +103,36 @@ class SaleSubscription(models.Model): store=True, ondelete="restrict", ) + payment_token_id = fields.Many2one( + comodel_name="payment.token", + string="Payment Token", + store=True, + compute="_compute_payment_token_id", + domain="[('partner_id', '=', partner_id)]", + ) + invoicing_mode = fields.Selection(related="template_id.invoicing_mode") + + @api.depends("partner_id", "template_id") + def _compute_payment_token_id(self): + for record in self: + if record.template_id.invoicing_mode not in [ + "invoice_and_payment", + ]: + record.payment_token_id = False + continue + payment_token = ( + self.env["payment.token"] + .sudo() + .with_company(record.company_id) + .search( + [ + ("partner_id", "=", record.partner_id.id), + ], + limit=1, + order="write_date desc", + ) + ) + record.payment_token_id = payment_token.id if payment_token else False @api.model def _read_group_stage_ids(self, stages, domain, order): @@ -328,11 +358,21 @@ def create_sale_order(self): def generate_invoice(self): invoice_number = "" msg_static = _("Created invoice with reference") - if self.template_id.invoicing_mode in ["draft", "invoice", "invoice_send"]: + if self.template_id.invoicing_mode in [ + "draft", + "invoice", + "invoice_send", + "invoice_and_payment", + ]: invoice = self.create_invoice() if self.template_id.invoicing_mode != "draft": invoice.action_post() - if self.template_id.invoicing_mode == "invoice_send": + if self.template_id.invoicing_mode == "invoice_and_payment": + self.create_payment(invoice) + if self.template_id.invoicing_mode in ( + "invoice_send", + "invoice_and_payment", + ): mail_template = self.template_id.invoice_mail_template_id invoice.with_context(force_send=True).message_post_with_template( mail_template.id, @@ -363,6 +403,54 @@ def generate_invoice(self): self.calculate_recurring_next_date(self.recurring_next_date) self.message_post(body=message_body) + def create_payment(self, invoice): + invoice.ensure_one() + if not self.payment_token_id: + self.message_post( + body=_( + "No payment token found for partner %s" % invoice.partner_id.name + ) + ) + return + provider = self.payment_token_id.provider_id + method_line = self.env["account.payment.method.line"].search( + [ + ("payment_method_id.code", "=", provider.code), + ("company_id", "=", invoice.company_id.id), + ], + limit=1, + ) + + if not method_line: + self.message_post( + body=_( + "No payment method line found for payment provider %s" + % provider.name + ) + ) + return + payment_register = self.env["account.payment.register"] + payment_vals = { + "currency_id": invoice.currency_id.id, + "journal_id": provider.journal_id.id, + "company_id": invoice.company_id.id, + "partner_id": invoice.partner_id.id, + "communication": invoice.name, + "payment_type": "inbound", + "partner_type": "customer", + "payment_difference_handling": "open", + "writeoff_label": "Write-Off", + "payment_date": fields.Date.today(), + "amount": invoice.amount_total, + "payment_method_line_id": method_line.id, + "payment_token_id": self.payment_token_id.id, + } + payment_register.with_context( + active_model="account.move", + active_ids=invoice.ids, + active_id=invoice.id, + ).create(payment_vals).action_create_payments() + def manual_invoice(self): invoice_id = self.create_invoice() self.calculate_recurring_next_date(self.recurring_next_date) diff --git a/subscription_oca/models/sale_subscription_template.py b/subscription_oca/models/sale_subscription_template.py index df89f401b0..b5fe7610a8 100644 --- a/subscription_oca/models/sale_subscription_template.py +++ b/subscription_oca/models/sale_subscription_template.py @@ -35,6 +35,7 @@ class SaleSubscriptionTemplate(models.Model): ("invoice", "Invoice"), ("invoice_send", "Invoice & send"), ("sale_and_invoice", "Sale order & Invoice"), + ("invoice_and_payment", "Invoice & Recurring Payment"), ], ) code = fields.Char() diff --git a/subscription_oca/readme/CONTRIBUTORS.rst b/subscription_oca/readme/CONTRIBUTORS.rst index 8ddfb05210..58ac9c1ef1 100644 --- a/subscription_oca/readme/CONTRIBUTORS.rst +++ b/subscription_oca/readme/CONTRIBUTORS.rst @@ -5,3 +5,10 @@ * `Ooops404 `__: * Ilyas + + +* `Binhex `__: + + * Adasat Torres de Leon + +* Chris Mann diff --git a/subscription_oca/static/description/index.html b/subscription_oca/static/description/index.html index a730b363db..fc1bf43c29 100644 --- a/subscription_oca/static/description/index.html +++ b/subscription_oca/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Subscription management -
+
+

Subscription management

- - -Odoo Community Association - -
-

Subscription management

-

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

+

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

This module allows creating subscriptions that generate recurring invoices or orders. It also enables the sale of products that generate subscriptions.

Table of contents

@@ -391,7 +386,7 @@

Subscription management

-

Usage

+

Usage

To make a subscription:

  1. Go to Subscriptions > Configuration > Subscription templates.
  2. @@ -408,14 +403,14 @@

    Usage

-

Known issues / Roadmap

+

Known issues / Roadmap

  • Refactor all the onchanges that have business logic to computed write-able fields when possible. Keep onchanges only for UI purposes.
  • Add tests.
-

Bug Tracker

+

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 @@ -423,15 +418,15 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Domatix
-

Contributors

+

Contributors

+
  • Binhex: +
  • +
  • Chris Mann <chrisandrewmann>
  • -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -455,6 +455,5 @@

    Maintainers

    -
    diff --git a/subscription_oca/tests/test_subscription_oca.py b/subscription_oca/tests/test_subscription_oca.py index 70e5bf99d9..dcaa40177a 100644 --- a/subscription_oca/tests/test_subscription_oca.py +++ b/subscription_oca/tests/test_subscription_oca.py @@ -38,6 +38,14 @@ def setUpClass(cls): ("company_id", "=", cls.env.ref("base.main_company").id), ] )[0] + + cls.bank_journal = cls.env["account.journal"].search( + [ + ("type", "=", "bank"), + ("company_id", "=", cls.env.ref("base.main_company").id), + ] + )[0] + cls.pricelist1 = cls.env["product.pricelist"].create( { "name": "pricelist for contract test", @@ -116,6 +124,14 @@ def setUpClass(cls): } ) + cls.tmpl6 = cls.create_sub_template( + { + "recurring_rule_boundary": "unlimited", + "invoicing_mode": "invoice_and_payment", + "recurring_rule_type": "years", + } + ) + cls.stage = cls.env["sale.subscription.stage"].create( { "name": "Test Sub Stage", @@ -193,6 +209,14 @@ def setUpClass(cls): } ) + cls.sub10 = cls.create_sub( + { + "template_id": cls.tmpl6.id, + "recurring_rule_boundary": False, + "date_start": fields.Date.today(), + } + ) + cls.sub_line = cls.create_sub_line(cls.sub1) cls.sub_line2 = cls.env["sale.subscription.line"].create( { @@ -210,6 +234,7 @@ def setUpClass(cls): cls.sub_line52 = cls.create_sub_line(cls.sub5, cls.product_2.id) cls.sub_line71 = cls.create_sub_line(cls.sub7) cls.sub_line72 = cls.create_sub_line(cls.sub7, cls.product_2.id) + cls.sub_line102 = cls.create_sub_line(cls.sub10, cls.product_2.id) cls.close_reason = cls.env["sale.subscription.close.reason"].create( { @@ -541,7 +566,7 @@ def test_subscription_oca_sub_stage(self): def test_x_subscription_oca_pricelist_related(self): res = self.partner.read(["subscription_count", "subscription_ids"]) - self.assertEqual(res[0]["subscription_count"], 9) + self.assertEqual(res[0]["subscription_count"], 10) res = self.partner.action_view_subscription_ids() self.assertIsInstance(res, dict) sale_order = self.sub1.create_sale_order() @@ -692,3 +717,77 @@ def _collect_all_sub_test_results(self, subscription): ) test_res.append(group_stage_ids) return test_res + + def test_subscription_invoice_and_payment(self): + + account_payment_method = self.env["account.payment.method"].create( + { + "name": "Test Payment Method", + "code": "none", + "payment_type": "inbound", + } + ) + + account_payment_method_line = self.env["account.payment.method.line"].create( + { + "payment_method_id": account_payment_method.id, + "company_id": self.env.ref("base.main_company").id, + "name": "Test Method Line", + } + ) + + journal = self.env["account.journal"].create( + { + "name": "Test Journal", + "type": "bank", + "company_id": self.env.ref("base.main_company").id, + "code": "TESTJNL", + } + ) + + provider_test = self.env["payment.provider"].create( + { + "name": "Test Provider for Subscriptions", + "code": "none", + "company_id": self.env.ref("base.main_company").id, + "journal_id": journal.id, + "state": "test", + } + ) + + subscription = self.sub10 + subscription.generate_invoice() + error_count = len( + self.sub10.message_ids.filtered( + lambda msg: "No payment token found for partner" in msg.body + ) + ) + self.assertEqual(error_count, 1) + self.assertEqual(len(subscription.invoice_ids), 1) + self.assertEqual(subscription.invoice_ids.state, "posted") + self.sub10.payment_token_id = self.env["payment.token"].create( + { + "payment_details": "1234", + "provider_id": provider_test.id, + "partner_id": self.partner.id, + "provider_ref": "provider Ref (TEST)", + "active": True, + } + ) + subscription.generate_invoice() + self.assertEqual(len(subscription.invoice_ids), 2) + last_invoice = subscription.invoice_ids[-1] + self.assertEqual(last_invoice.state, "posted") + error_count = len( + self.sub10.message_ids.filtered( + lambda msg: "No payment method line found for payment provider" + in msg.body + ) + ) + journal.write( + {"inbound_payment_method_line_ids": [(4, account_payment_method_line.id)]} + ) + subscription.generate_invoice() + self.assertEqual(len(subscription.invoice_ids), 3) + last_invoice = subscription.invoice_ids[-1] + self.assertEqual(last_invoice.state, "posted") diff --git a/subscription_oca/views/sale_subscription_template_views.xml b/subscription_oca/views/sale_subscription_template_views.xml index f6ac959cea..d640fe861e 100644 --- a/subscription_oca/views/sale_subscription_template_views.xml +++ b/subscription_oca/views/sale_subscription_template_views.xml @@ -80,7 +80,10 @@ @@ -96,12 +99,9 @@ /> - - - diff --git a/subscription_oca/views/sale_subscription_views.xml b/subscription_oca/views/sale_subscription_views.xml index 912aab2eb7..0b2e0a6f34 100644 --- a/subscription_oca/views/sale_subscription_views.xml +++ b/subscription_oca/views/sale_subscription_views.xml @@ -93,6 +93,14 @@ attrs="{'invisible': [('active', '=', True)]}" /> + +