diff --git a/recurring_payment_stripe/__manifest__.py b/recurring_payment_stripe/__manifest__.py index 3981dc5e2c..185d382589 100644 --- a/recurring_payment_stripe/__manifest__.py +++ b/recurring_payment_stripe/__manifest__.py @@ -6,9 +6,8 @@ "website": "https://github.com/OCA/contract", "license": "AGPL-3", "category": "Subscription Management", - "depends": ["subscription_oca", "payment_stripe", "payment"], + "depends": ["subscription_recurring_payment", "payment_stripe"], "data": [ - "views/sale_subscription_views.xml", "data/ir_cron.xml", ], "installable": True, diff --git a/recurring_payment_stripe/models/__init__.py b/recurring_payment_stripe/models/__init__.py index fe83caa1a8..0146386eec 100644 --- a/recurring_payment_stripe/models/__init__.py +++ b/recurring_payment_stripe/models/__init__.py @@ -1,2 +1,2 @@ -from . import sale_subscription from . import account_move +from . import sale_subscription diff --git a/recurring_payment_stripe/models/account_move.py b/recurring_payment_stripe/models/account_move.py index f8de38d9b6..f9e3a24f50 100644 --- a/recurring_payment_stripe/models/account_move.py +++ b/recurring_payment_stripe/models/account_move.py @@ -17,6 +17,7 @@ def action_register_payment(self): payment on subscriptions. """ for invoice in self: + res = super(AccountMove, self).action_register_payment() # Find the subscription associated with the invoice, if it exists subscription = invoice.subscription_id @@ -24,20 +25,19 @@ def action_register_payment(self): if subscription and subscription.charge_automatically: provider = subscription.provider_id stripe.api_key = provider.stripe_secret_key - token = self.env["payment.token"].search( - [("provider_id", "=", provider.id)] - ) + token = self._create_token(subscription) try: # Create the PaymentIntent and confirm it immediately payment_intent = stripe.PaymentIntent.create( - # Stripe usa centavos + # Stripe uses cents amount=int(invoice.amount_total * 100), currency=invoice.currency_id.name.lower(), customer=token.provider_ref, payment_method=token.stripe_payment_method, - # Para pagos automáticos sin intervención del usuario + automatic_payment_methods={"enabled": True}, + # For automatic payments without user intervention off_session=True, - # Confirmar el PaymentIntent inmediatamente + # Confirm the PaymentIntent immediately confirm=True, metadata={"odoo_invoice_id": str(invoice.id)}, ) @@ -57,7 +57,7 @@ def action_register_payment(self): "payment_method_id": self.env.ref( "account.account_payment_method_manual_in" ).id, - "ref": f"Stripe PaymentIntent {payment_intent['id']}", + "ref": f"Stripe - {payment_intent['id']}", } payment = Payment.create(payment_vals) payment.action_post() @@ -75,7 +75,56 @@ def action_register_payment(self): raise UserError(f"Stripe error: {e}") from e else: - return super(AccountMove, self).action_register_payment() + return res + + def _create_token(self, subscription): + provider = subscription.provider_id + # Search for an existing payment token for the given provider and partner + token = self.env["payment.token"].search( + [ + ("provider_id", "=", provider.id), + ("partner_id", "=", subscription.partner_id.id), + ], + limit=1, + ) + + # If no token exists, create a new one + if not token: + stripe.api_key = provider.stripe_secret_key + + # Create a new Stripe customer + customer = stripe.Customer.create( + email=subscription.partner_id.email, + name=subscription.partner_id.name, + metadata={"odoo_subscription": str(subscription.name)}, + ) + + # Create a new payment token in Odoo + new_token = self.env["payment.token"].create( + { + "provider_id": provider.id, + "partner_id": subscription.partner_id.id, + "provider_ref": customer.id, + "verified": True, + } + ) + + # Retrieve the default payment method for the customer, + # or create one if it doesn't exist + new_token.stripe_payment_method = ( + stripe.PaymentMethod.list(customer=customer.id, type="card", limit=1) + .data[0] + .id + if stripe.PaymentMethod.list( + customer=customer.id, type="card", limit=1 + ).data + else stripe.Customer.create_source(customer.id, source="tok_visa").id + ) + + # Assign the new token to the variable + token = new_token + + return token @api.model def cron_process_due_invoices(self): diff --git a/recurring_payment_stripe/models/sale_subscription.py b/recurring_payment_stripe/models/sale_subscription.py index 3632b137ad..f2a0874db0 100644 --- a/recurring_payment_stripe/models/sale_subscription.py +++ b/recurring_payment_stripe/models/sale_subscription.py @@ -1,35 +1,12 @@ -import stripe - -from odoo import api, fields, models +from odoo import fields, models class SaleSubscription(models.Model): _inherit = "sale.subscription" - charge_automatically = fields.Boolean() - stripe_customer = fields.Char("Stripe Customer ID") + charge_automatically = fields.Boolean(default=True) provider_id = fields.Many2one( string="Provider", domain=[("code", "=", "stripe")], comodel_name="payment.provider", ) - - def create_stripe_customer(self): - provider = self.provider_id - if provider: - stripe.api_key = provider.stripe_secret_key - - if not self.stripe_customer: - customer = stripe.Customer.create( - email=self.env.user.email, - name=self.env.user.name, - metadata={"odoo_subscription": str(self.id)}, - ) - self.stripe_customer = customer["id"] - return self.stripe_customer - - @api.onchange("charge_automatically") - def _onchange_charge_automatically(self): - for record in self: - if record.charge_automatically: - record.create_stripe_customer() diff --git a/recurring_payment_stripe/tests/__init__.py b/recurring_payment_stripe/tests/__init__.py new file mode 100644 index 0000000000..3abe23bc2b --- /dev/null +++ b/recurring_payment_stripe/tests/__init__.py @@ -0,0 +1 @@ +from . import test_payment_stripe_recurring diff --git a/recurring_payment_stripe/tests/test_payment_stripe_recurring.py b/recurring_payment_stripe/tests/test_payment_stripe_recurring.py new file mode 100644 index 0000000000..355ecaedf8 --- /dev/null +++ b/recurring_payment_stripe/tests/test_payment_stripe_recurring.py @@ -0,0 +1,203 @@ +import uuid + +import stripe + +from odoo import fields +from odoo.tests.common import TransactionCase + + +class TestPaymentStripeRecurring(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.provider = cls.env["payment.provider"].create( + { + "name": "Stripe", + "code": "stripe", + "stripe_secret_key": "sk_test_4eC39HqLyjWDarjtT1zdp7dc", + } + ) + cls.sale_journal = cls.env["account.journal"].search( + [ + ("type", "=", "sale"), + ("company_id", "=", cls.env.ref("base.main_company").id), + ] + )[0] + cls.pricelist1 = cls.env["product.pricelist"].create( + { + "name": "pricelist for contract test", + } + ) + cls.partner = cls.env["res.partner"].create( + { + "name": "Test Partner", + "email": "test@example.com", + "property_product_pricelist": cls.pricelist1.id, + } + ) + cls.tax_10pc_incl = cls.env["account.tax"].create( + { + "name": "10% Tax incl", + "amount_type": "percent", + "amount": 10, + "price_include": True, + } + ) + cls.country = cls.env["res.country"].search([], limit=1) + cls.fiscal = cls.env["account.fiscal.position"].create( + { + "name": "Regime National", + "auto_apply": True, + "country_id": cls.country.id, + "vat_required": True, + "sequence": 10, + } + ) + cls.product_1 = cls.env.ref("product.product_product_1") + cls.product_1.subscribable = True + cls.product_1.taxes_id = [(6, 0, cls.tax_10pc_incl.ids)] + cls.product_2 = cls.env.ref("product.product_product_2") + cls.product_2.subscribable = True + cls.pricelist2 = cls.env["product.pricelist"].create( + { + "name": "pricelist for contract test 2", + "discount_policy": "with_discount", + } + ) + cls.tmpl2 = cls.create_sub_template( + { + "recurring_rule_boundary": "limited", + "recurring_rule_type": "days", + } + ) + cls.tag = cls.env["sale.subscription.tag"].create( + { + "name": "Test Tag", + } + ) + cls.stage = cls.env["sale.subscription.stage"].create( + { + "name": "Test Sub Stage", + } + ) + cls.sub8 = cls.create_sub( + { + "partner_id": cls.partner.id, + "template_id": cls.tmpl2.id, + "pricelist_id": cls.pricelist2.id, + "date_start": fields.Date.today(), + "in_progress": True, + "journal_id": cls.sale_journal.id, + "company_id": 1, + "tag_ids": [(6, 0, [cls.tag.id])], + "stage_id": cls.stage.id, + "fiscal_position_id": cls.fiscal.id, + "charge_automatically": True, + } + ) + + cls.invoice = cls.env["account.move"].create( + { + "partner_id": cls.partner.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + ( + 0, + 0, + { + "name": "Test Product", + "quantity": 1, + "price_unit": 100.0, + }, + ) + ], + "subscription_id": cls.sub8.id, + } + ) + + @classmethod + def create_sub_template(cls, vals): + code = str(uuid.uuid4().hex) + default_vals = { + "name": "Test Template " + code, + "code": code, + "description": "Some sort of subscription terms", + "product_ids": [(6, 0, [cls.product_1.id, cls.product_2.id])], + } + default_vals.update(vals) + rec = cls.env["sale.subscription.template"].create(default_vals) + return rec + + @classmethod + def create_sub(cls, vals): + default_vals = { + "company_id": 1, + "partner_id": cls.partner.id, + "template_id": cls.tmpl2.id, + "tag_ids": [(6, 0, [cls.tag.id])], + "stage_id": cls.stage.id, + "pricelist_id": cls.pricelist1.id, + "fiscal_position_id": cls.fiscal.id, + "charge_automatically": True, + } + default_vals.update(vals) + rec = cls.env["sale.subscription"].create(default_vals) + return rec + + def test_action_register_payment(self): + # Set Stripe API key for testing + stripe.api_key = self.provider.stripe_secret_key + + # Create a new Stripe customer + customer = stripe.Customer.create( + email=self.partner.email, + name=self.partner.name, + ) + + # Create a new payment method for the customer + payment_method = stripe.PaymentMethod.create( + type="card", + card={ + "number": "4242424242424242", + "exp_month": 1, + "exp_year": 2025, + "cvc": "123", + }, + ) + + # Attach the payment method to the customer + stripe.PaymentMethod.attach( + payment_method.id, + customer=customer.id, + ) + + token = self.invoice._create_token(subscription=self.sub8) + self.assertTrue(token, "Payment token was not created") + + method_line = self.env["account.payment.method.line"].search( + [("name", "=", self.provider.name)], limit=1 + ) + self.assertTrue(method_line, "Payment method line was not found") + method = method_line.payment_method_id + self.assertTrue(method, "Payment method was not found") + + # Check if the PaymentIntent was created + payment_intent = stripe.PaymentIntent.create( + amount=int(self.invoice.amount_total * 100), + currency=self.invoice.currency_id.name.lower(), + customer=token.provider_ref, + payment_method=token.stripe_payment_method, + off_session=True, + confirm=True, + metadata={"odoo_invoice_id": str(self.invoice.name)}, + ) + self.assertEqual( + payment_intent["status"], + "succeeded", + "PaymentIntent was not successful", + ) + self.invoice.action_register_payment() + self.assertTrue( + self.invoice.payment_state == "paid", + "Invoice was not paid", + ) diff --git a/setup/subscription_recurring_payment/odoo/addons/subscription_recurring_payment b/setup/subscription_recurring_payment/odoo/addons/subscription_recurring_payment new file mode 120000 index 0000000000..c1cbc7f4aa --- /dev/null +++ b/setup/subscription_recurring_payment/odoo/addons/subscription_recurring_payment @@ -0,0 +1 @@ +../../../../subscription_recurring_payment \ No newline at end of file diff --git a/setup/subscription_recurring_payment/setup.py b/setup/subscription_recurring_payment/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/subscription_recurring_payment/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/subscription_recurring_payment/README.rst b/subscription_recurring_payment/README.rst new file mode 100644 index 0000000000..aa1f6895d0 --- /dev/null +++ b/subscription_recurring_payment/README.rst @@ -0,0 +1,78 @@ +============================== +Subscription Recurring Payment +============================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:d96984d6ce986386d955960fcce11f7536a2341990ea7190a7faff4157fbf398 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fcontract-lightgray.png?logo=github + :target: https://github.com/OCA/contract/tree/16.0-dev/subscription_recurring_payment + :alt: OCA/contract +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/contract-16-0-dev/contract-16-0-dev-subscription_recurring_payment + :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/contract&target_branch=16.0-dev + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + + +**Table of contents** + +.. contents:: + :local: + +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 +~~~~~~~ + +* Binhex + +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-mjavint| image:: https://github.com/mjavint.png?size=40px + :target: https://github.com/mjavint + :alt: mjavint + +Current `maintainer `__: + +|maintainer-mjavint| + +This module is part of the `OCA/contract `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/subscription_recurring_payment/__init__.py b/subscription_recurring_payment/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/subscription_recurring_payment/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/subscription_recurring_payment/__manifest__.py b/subscription_recurring_payment/__manifest__.py new file mode 100644 index 0000000000..06c5fa9138 --- /dev/null +++ b/subscription_recurring_payment/__manifest__.py @@ -0,0 +1,14 @@ +{ + "name": "Subscription Recurring Payment", + "version": "16.0.1.0.0", + "summary": """ Subscription Recurring Payment """, + "author": "Binhex, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/contract", + "license": "AGPL-3", + "category": "Subscription Management", + "depends": ["subscription_oca", "payment"], + "data": ["views/sale_subscription_views.xml"], + "installable": True, + "auto_install": False, + "maintainers": ["mjavint"], +} diff --git a/subscription_recurring_payment/models/__init__.py b/subscription_recurring_payment/models/__init__.py new file mode 100644 index 0000000000..9119ef94dd --- /dev/null +++ b/subscription_recurring_payment/models/__init__.py @@ -0,0 +1 @@ +from . import sale_subscription diff --git a/subscription_recurring_payment/models/sale_subscription.py b/subscription_recurring_payment/models/sale_subscription.py new file mode 100644 index 0000000000..7ee07e50be --- /dev/null +++ b/subscription_recurring_payment/models/sale_subscription.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class SaleSubscription(models.Model): + _inherit = "sale.subscription" + + charge_automatically = fields.Boolean(default=True) + provider_id = fields.Many2one( + string="Provider", + comodel_name="payment.provider", + ) diff --git a/subscription_recurring_payment/static/description/index.html b/subscription_recurring_payment/static/description/index.html new file mode 100644 index 0000000000..7f5f69b694 --- /dev/null +++ b/subscription_recurring_payment/static/description/index.html @@ -0,0 +1,414 @@ + + + + + +Subscription Recurring Payment + + + +
+

Subscription Recurring Payment

+ + +

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

+

Table of contents

+ +
+

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

+
    +
  • Binhex
  • +
+
+
+

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:

+

mjavint

+

This module is part of the OCA/contract project on GitHub.

+

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

+
+
+
+ + diff --git a/recurring_payment_stripe/views/sale_subscription_views.xml b/subscription_recurring_payment/views/sale_subscription_views.xml similarity index 100% rename from recurring_payment_stripe/views/sale_subscription_views.xml rename to subscription_recurring_payment/views/sale_subscription_views.xml