diff --git a/subscription_oca/README.rst b/subscription_oca/README.rst index 57a6d31363..d328e070df 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 @@ -112,7 +108,11 @@ Contributors - Valentin Vinagre - Alberto Martínez -- Dennis Sluijk +- `Binhex `__: + + - Adasat Torres de León + +- Chris Mann Maintainers ----------- diff --git a/subscription_oca/models/sale_subscription.py b/subscription_oca/models/sale_subscription.py index 114db35968..3794ac38d3 100644 --- a/subscription_oca/models/sale_subscription.py +++ b/subscription_oca/models/sale_subscription.py @@ -105,6 +105,37 @@ class SaleSubscription(models.Model): 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): stage_ids = stages.search([], order=stages._order) @@ -328,12 +359,18 @@ def create_sale_order(self): def generate_invoice(self): invoice_number = "" - message_body = "" msg_static = self.env._("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_and_payment": + self.create_payment(invoice) mail_template = self.template_id.invoice_mail_template_id self.env["account.move.send"]._generate_and_send_invoices( invoice, mail_template=mail_template, sending_methods=["email"] @@ -487,3 +524,50 @@ def create(self, vals_list): .id ) return super().create(vals_list) + + def create_payment(self, invoice): + invoice.ensure_one() + if not self.payment_token_id: + self.message_post( + body=self.env._( + f"No payment token found for partner {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=self.env._( + f"No payment method line found for payment provider {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() diff --git a/subscription_oca/models/sale_subscription_template.py b/subscription_oca/models/sale_subscription_template.py index acf82a7b12..7a7c44290a 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.md b/subscription_oca/readme/CONTRIBUTORS.md index 885a89aa19..56b234a6d5 100644 --- a/subscription_oca/readme/CONTRIBUTORS.md +++ b/subscription_oca/readme/CONTRIBUTORS.md @@ -6,4 +6,6 @@ - Harald Panten - Valentin Vinagre - Alberto Martínez -- Dennis Sluijk \<\> +- [Binhex](https://www.binhex.cloud): + - Adasat Torres de León \<\> +- Chris Mann diff --git a/subscription_oca/static/description/index.html b/subscription_oca/static/description/index.html index 50e903fc3d..b79588fff4 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.

@@ -393,7 +388,7 @@

Subscription management

-

Usage

+

Usage

To make a subscription:

  1. Go to Subscriptions > Configuration > Subscription templates.
  2. @@ -423,7 +418,7 @@

    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.
  • @@ -431,7 +426,7 @@

    Known issues / Roadmap

-

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 @@ -439,16 +434,16 @@

Bug Tracker

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

-

Credits

+

Credits

-

Authors

+

Authors

  • Domatix
  • Onestein
-

Contributors

+

Contributors

-
  • Dennis Sluijk <d.sluijk@onestein.nl>
  • +
  • Binhex: +
  • +
  • Chris Mann
  • -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -479,6 +478,5 @@

    Maintainers

    -
    diff --git a/subscription_oca/tests/test_subscription_oca.py b/subscription_oca/tests/test_subscription_oca.py index 0d9ca7afca..3c73f89e5d 100644 --- a/subscription_oca/tests/test_subscription_oca.py +++ b/subscription_oca/tests/test_subscription_oca.py @@ -118,6 +118,13 @@ def setUpClass(cls): "recurring_rule_type": "days", } ) + 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( { @@ -195,6 +202,13 @@ def setUpClass(cls): "recurring_rule_boundary": True, } ) + 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( @@ -213,6 +227,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( { @@ -540,7 +555,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() @@ -683,3 +698,79 @@ def _collect_all_sub_test_results(self, subscription): ) test_res.append(group_stage_ids) return test_res + + def test_subscription_invoice_and_payment(self): + payment_method_unknown = self.env.ref("payment.payment_method_unknown") + + 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, + "payment_method_id": payment_method_unknown.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 3190c89b82..b351179cde 100644 --- a/subscription_oca/views/sale_subscription_template_views.xml +++ b/subscription_oca/views/sale_subscription_template_views.xml @@ -78,7 +78,7 @@ diff --git a/subscription_oca/views/sale_subscription_views.xml b/subscription_oca/views/sale_subscription_views.xml index 1342fc5019..1f5e99352e 100644 --- a/subscription_oca/views/sale_subscription_views.xml +++ b/subscription_oca/views/sale_subscription_views.xml @@ -82,6 +82,14 @@ name="close_reason_id" invisible="stage_type != 'post'" /> + + + +