From 6a34ba452f38878c6eb81bb8e6193a278216b304 Mon Sep 17 00:00:00 2001 From: mjavint Date: Tue, 21 Jan 2025 10:41:11 -0500 Subject: [PATCH] [FIX] Fixed testing --- .../models/account_move.py | 143 ---------- .../models/sale_subscription.py | 12 - recurring_payment_stripe/tests/__init__.py | 1 - .../tests/test_payment_stripe_recurring.py | 178 ------------ requirements.txt | 1 - .../odoo/addons/recurring_payment_stripe | 1 - .../subscription_recurring_payment_stripe | 1 + .../setup.py | 0 .../README.rst | 23 +- .../__init__.py | 0 .../__manifest__.py | 8 +- .../data/ir_cron.xml | 16 +- .../models/__init__.py | 2 +- .../models/account_move.py | 72 +++++ .../models/sale_subscription.py | 10 + .../readme/CONFIGURE.rst | 0 .../readme/CONTRIBUTORS.rst | 0 .../readme/CREDITS.rst | 0 .../readme/DESCRIPTION.rst | 0 .../readme/DEVELOP.rst | 0 .../readme/HISTORY.rst | 0 .../readme/INSTALL.rst | 0 .../readme/ROADMAP.rst | 0 .../readme/USAGE.rst | 0 .../static/description/index.html | 18 +- .../tests/__init__.py | 2 + .../tests/common.py | 261 ++++++++++++++++++ .../tests/test_account_move.py | 134 +++++++++ .../views/sale_subscription_views.xml | 26 ++ 29 files changed, 542 insertions(+), 367 deletions(-) delete mode 100644 recurring_payment_stripe/models/account_move.py delete mode 100644 recurring_payment_stripe/models/sale_subscription.py delete mode 100644 recurring_payment_stripe/tests/__init__.py delete mode 100644 recurring_payment_stripe/tests/test_payment_stripe_recurring.py delete mode 120000 setup/recurring_payment_stripe/odoo/addons/recurring_payment_stripe create mode 120000 setup/subscription_recurring_payment_stripe/odoo/addons/subscription_recurring_payment_stripe rename setup/{recurring_payment_stripe => subscription_recurring_payment_stripe}/setup.py (100%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/README.rst (72%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/__init__.py (100%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/__manifest__.py (64%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/data/ir_cron.xml (52%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/models/__init__.py (100%) create mode 100644 subscription_recurring_payment_stripe/models/account_move.py create mode 100644 subscription_recurring_payment_stripe/models/sale_subscription.py rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/readme/CONFIGURE.rst (100%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/readme/CONTRIBUTORS.rst (100%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/readme/CREDITS.rst (100%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/readme/DESCRIPTION.rst (100%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/readme/DEVELOP.rst (100%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/readme/HISTORY.rst (100%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/readme/INSTALL.rst (100%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/readme/ROADMAP.rst (100%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/readme/USAGE.rst (100%) rename {recurring_payment_stripe => subscription_recurring_payment_stripe}/static/description/index.html (86%) create mode 100644 subscription_recurring_payment_stripe/tests/__init__.py create mode 100644 subscription_recurring_payment_stripe/tests/common.py create mode 100644 subscription_recurring_payment_stripe/tests/test_account_move.py create mode 100644 subscription_recurring_payment_stripe/views/sale_subscription_views.xml diff --git a/recurring_payment_stripe/models/account_move.py b/recurring_payment_stripe/models/account_move.py deleted file mode 100644 index f9e3a24f50..0000000000 --- a/recurring_payment_stripe/models/account_move.py +++ /dev/null @@ -1,143 +0,0 @@ -import logging - -import stripe - -from odoo import _, api, models -from odoo.exceptions import UserError - -_logger = logging.getLogger(__name__) - - -class AccountMove(models.Model): - _inherit = "account.move" - - def action_register_payment(self): - """ - Override `action_register_payment` to automatically process Stripe - 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 - - # Check if the subscription is recurring and has a payment method - if subscription and subscription.charge_automatically: - provider = subscription.provider_id - stripe.api_key = provider.stripe_secret_key - token = self._create_token(subscription) - try: - # Create the PaymentIntent and confirm it immediately - payment_intent = stripe.PaymentIntent.create( - # 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, - automatic_payment_methods={"enabled": True}, - # For automatic payments without user intervention - off_session=True, - # Confirm the PaymentIntent immediately - confirm=True, - metadata={"odoo_invoice_id": str(invoice.id)}, - ) - - # Handling the result of the PaymentIntent - if payment_intent["status"] == "succeeded": - # If the payment is successful, record the payment on the invoice - Payment = self.env["account.payment"].sudo() - payment_vals = { - "journal_id": self.env["account.journal"] - .search([("type", "=", "bank")], limit=1) - .id, - "amount": invoice.amount_total, - "payment_type": "inbound", - "partner_type": "customer", - "partner_id": invoice.partner_id.id, - "payment_method_id": self.env.ref( - "account.account_payment_method_manual_in" - ).id, - "ref": f"Stripe - {payment_intent['id']}", - } - payment = Payment.create(payment_vals) - payment.action_post() - invoice.payment_state = "paid" - elif payment_intent["status"] == "requires_action": - raise UserError( - _("Payment requires additional authentication (3D Secure).") - ) - else: - raise UserError( - f"Stripe payment error: {payment_intent['status']}" - ) - - except stripe.StripeError as e: - raise UserError(f"Stripe error: {e}") from e - - else: - 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): - """Process payment of overdue invoices for recurring subscriptions.""" - - for invoice in self: - # Find the subscription associated with the invoice - subscription = invoice.subscription_id - - # Check if it's a recurring subscription with Stripe - if subscription and subscription.charge_automatically: - try: - # Register the payment - invoice.action_register_payment() - except Exception as e: - _logger.error(f"Error Processing Due Invoices: {str(e)}") diff --git a/recurring_payment_stripe/models/sale_subscription.py b/recurring_payment_stripe/models/sale_subscription.py deleted file mode 100644 index f2a0874db0..0000000000 --- a/recurring_payment_stripe/models/sale_subscription.py +++ /dev/null @@ -1,12 +0,0 @@ -from odoo import fields, models - - -class SaleSubscription(models.Model): - _inherit = "sale.subscription" - - charge_automatically = fields.Boolean(default=True) - provider_id = fields.Many2one( - string="Provider", - domain=[("code", "=", "stripe")], - comodel_name="payment.provider", - ) diff --git a/recurring_payment_stripe/tests/__init__.py b/recurring_payment_stripe/tests/__init__.py deleted file mode 100644 index 3abe23bc2b..0000000000 --- a/recurring_payment_stripe/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 17be77d30a..0000000000 --- a/recurring_payment_stripe/tests/test_payment_stripe_recurring.py +++ /dev/null @@ -1,178 +0,0 @@ -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, - "provider_id": cls.provider.id, - } - ) - - 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): - 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/requirements.txt b/requirements.txt index 55fb35df0a..7d41f1be0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ # generated from manifests external_dependencies python-dateutil -stripe diff --git a/setup/recurring_payment_stripe/odoo/addons/recurring_payment_stripe b/setup/recurring_payment_stripe/odoo/addons/recurring_payment_stripe deleted file mode 120000 index a75e72c91c..0000000000 --- a/setup/recurring_payment_stripe/odoo/addons/recurring_payment_stripe +++ /dev/null @@ -1 +0,0 @@ -../../../../recurring_payment_stripe \ No newline at end of file diff --git a/setup/subscription_recurring_payment_stripe/odoo/addons/subscription_recurring_payment_stripe b/setup/subscription_recurring_payment_stripe/odoo/addons/subscription_recurring_payment_stripe new file mode 120000 index 0000000000..243c62a8e2 --- /dev/null +++ b/setup/subscription_recurring_payment_stripe/odoo/addons/subscription_recurring_payment_stripe @@ -0,0 +1 @@ +../../../../subscription_recurring_payment_stripe \ No newline at end of file diff --git a/setup/recurring_payment_stripe/setup.py b/setup/subscription_recurring_payment_stripe/setup.py similarity index 100% rename from setup/recurring_payment_stripe/setup.py rename to setup/subscription_recurring_payment_stripe/setup.py diff --git a/recurring_payment_stripe/README.rst b/subscription_recurring_payment_stripe/README.rst similarity index 72% rename from recurring_payment_stripe/README.rst rename to subscription_recurring_payment_stripe/README.rst index a209ffdc5e..44bf85ff1c 100644 --- a/recurring_payment_stripe/README.rst +++ b/subscription_recurring_payment_stripe/README.rst @@ -1,13 +1,13 @@ -============================= -Recurring Payment with Stripe -============================= +========================================== +Subscription Recurring Payment with Stripe +========================================== .. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:7aacc5b46917b14e43404143f0ccd1519d1411d580330b8eb8a8ef608fe3a5a1 + !! source digest: sha256:a04a69ddb9cd26d60deee3b1ee68e21c605708fffc07d6c6a270a5ca62c8cbad !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png @@ -17,10 +17,10 @@ Recurring Payment with Stripe :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/recurring_payment_stripe + :target: https://github.com/OCA/contract/tree/16.0/subscription_recurring_payment_stripe :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/contract-16-0-recurring_payment_stripe + :target: https://translation.odoo-community.org/projects/contract-16-0/contract-16-0-subscription_recurring_payment_stripe :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 @@ -40,7 +40,7 @@ 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -65,14 +65,17 @@ 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-Binhex| image:: https://github.com/Binhex.png?size=40px + :target: https://github.com/Binhex + :alt: Binhex .. |maintainer-mjavint| image:: https://github.com/mjavint.png?size=40px :target: https://github.com/mjavint :alt: mjavint -Current `maintainer `__: +Current `maintainers `__: -|maintainer-mjavint| +|maintainer-Binhex| |maintainer-mjavint| -This module is part of the `OCA/contract `_ project on GitHub. +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/__init__.py b/subscription_recurring_payment_stripe/__init__.py similarity index 100% rename from recurring_payment_stripe/__init__.py rename to subscription_recurring_payment_stripe/__init__.py diff --git a/recurring_payment_stripe/__manifest__.py b/subscription_recurring_payment_stripe/__manifest__.py similarity index 64% rename from recurring_payment_stripe/__manifest__.py rename to subscription_recurring_payment_stripe/__manifest__.py index 185d382589..d394104023 100644 --- a/recurring_payment_stripe/__manifest__.py +++ b/subscription_recurring_payment_stripe/__manifest__.py @@ -1,17 +1,17 @@ { - "name": "Recurring Payment with Stripe", + "name": "Subscription Recurring Payment with Stripe", "version": "16.0.1.0.0", - "summary": """Recurring Payment with Stripe""", + "summary": """Subscription Recurring Payment with Stripe""", "author": "Binhex, Odoo Community Association (OCA)", "website": "https://github.com/OCA/contract", "license": "AGPL-3", "category": "Subscription Management", "depends": ["subscription_recurring_payment", "payment_stripe"], "data": [ + "views/sale_subscription_views.xml", "data/ir_cron.xml", ], "installable": True, "auto_install": False, - "external_dependencies": {"python": ["stripe"]}, - "maintainers": ["mjavint"], + "maintainers": ["Binhex", "mjavint"], } diff --git a/recurring_payment_stripe/data/ir_cron.xml b/subscription_recurring_payment_stripe/data/ir_cron.xml similarity index 52% rename from recurring_payment_stripe/data/ir_cron.xml rename to subscription_recurring_payment_stripe/data/ir_cron.xml index dc55679997..decb130a64 100644 --- a/recurring_payment_stripe/data/ir_cron.xml +++ b/subscription_recurring_payment_stripe/data/ir_cron.xml @@ -1,13 +1,15 @@ - - + + Process Overdue Invoices for Subscriptions - - code - model.cron_process_due_invoices() - 1 - days + + + 24 + hours + -1 True + code + model.cron_process_due_invoices() diff --git a/recurring_payment_stripe/models/__init__.py b/subscription_recurring_payment_stripe/models/__init__.py similarity index 100% rename from recurring_payment_stripe/models/__init__.py rename to subscription_recurring_payment_stripe/models/__init__.py index 0146386eec..fe83caa1a8 100644 --- a/recurring_payment_stripe/models/__init__.py +++ b/subscription_recurring_payment_stripe/models/__init__.py @@ -1,2 +1,2 @@ -from . import account_move from . import sale_subscription +from . import account_move diff --git a/subscription_recurring_payment_stripe/models/account_move.py b/subscription_recurring_payment_stripe/models/account_move.py new file mode 100644 index 0000000000..d2f620812c --- /dev/null +++ b/subscription_recurring_payment_stripe/models/account_move.py @@ -0,0 +1,72 @@ +import logging + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class AccountMove(models.Model): + _inherit = "account.move" + + def cron_process_due_invoices(self): + """Process payment of overdue invoices for recurring subscriptions.""" + + for invoice in self.search( + [ + ("state", "in", ["draft"]), + ("invoice_date_due", "<=", fields.Date.today()), + ] + ).filtered(lambda inv: inv.subscription_id): + # Find the subscription associated with the invoice + subscription = invoice.subscription_id + + # Check if it's a recurring subscription with Stripe + if ( + subscription + and subscription.charge_automatically + and subscription.payment_token_id + ): + try: + # Post the invoice + invoice.action_post() + + # Prepare payment data + provider = invoice.subscription_id.provider_id + method_line = self.env["account.payment.method.line"].search( + [("payment_method_id.code", "=", provider.code)], + limit=1, + ) + journal = self.env["account.journal"].search( + [ + ("type", "in", ("bank", "cash")), + ("company_id", "=", invoice.company_id.id), + ], + limit=1, + ) + + payment_register = self.env["account.payment.register"] + + payment_vals = { + "currency_id": invoice.currency_id.id, + "journal_id": journal.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": subscription.payment_token_id.id, + } + # Create payment and pay the invoice + payment_register.with_context( + active_model="account.move", + active_ids=invoice.ids, + active_id=invoice.id, + ).create(payment_vals).action_create_payments() + _logger.info(f"Processed Due Invoice: {invoice.name}") + except Exception as e: + _logger.error(f"Error Processing Due Invoices: {str(e)}") diff --git a/subscription_recurring_payment_stripe/models/sale_subscription.py b/subscription_recurring_payment_stripe/models/sale_subscription.py new file mode 100644 index 0000000000..70e2371abf --- /dev/null +++ b/subscription_recurring_payment_stripe/models/sale_subscription.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class SaleSubscription(models.Model): + _inherit = "sale.subscription" + + payment_token_id = fields.Many2one( + string="Payment Token", + comodel_name="payment.token", + ) diff --git a/recurring_payment_stripe/readme/CONFIGURE.rst b/subscription_recurring_payment_stripe/readme/CONFIGURE.rst similarity index 100% rename from recurring_payment_stripe/readme/CONFIGURE.rst rename to subscription_recurring_payment_stripe/readme/CONFIGURE.rst diff --git a/recurring_payment_stripe/readme/CONTRIBUTORS.rst b/subscription_recurring_payment_stripe/readme/CONTRIBUTORS.rst similarity index 100% rename from recurring_payment_stripe/readme/CONTRIBUTORS.rst rename to subscription_recurring_payment_stripe/readme/CONTRIBUTORS.rst diff --git a/recurring_payment_stripe/readme/CREDITS.rst b/subscription_recurring_payment_stripe/readme/CREDITS.rst similarity index 100% rename from recurring_payment_stripe/readme/CREDITS.rst rename to subscription_recurring_payment_stripe/readme/CREDITS.rst diff --git a/recurring_payment_stripe/readme/DESCRIPTION.rst b/subscription_recurring_payment_stripe/readme/DESCRIPTION.rst similarity index 100% rename from recurring_payment_stripe/readme/DESCRIPTION.rst rename to subscription_recurring_payment_stripe/readme/DESCRIPTION.rst diff --git a/recurring_payment_stripe/readme/DEVELOP.rst b/subscription_recurring_payment_stripe/readme/DEVELOP.rst similarity index 100% rename from recurring_payment_stripe/readme/DEVELOP.rst rename to subscription_recurring_payment_stripe/readme/DEVELOP.rst diff --git a/recurring_payment_stripe/readme/HISTORY.rst b/subscription_recurring_payment_stripe/readme/HISTORY.rst similarity index 100% rename from recurring_payment_stripe/readme/HISTORY.rst rename to subscription_recurring_payment_stripe/readme/HISTORY.rst diff --git a/recurring_payment_stripe/readme/INSTALL.rst b/subscription_recurring_payment_stripe/readme/INSTALL.rst similarity index 100% rename from recurring_payment_stripe/readme/INSTALL.rst rename to subscription_recurring_payment_stripe/readme/INSTALL.rst diff --git a/recurring_payment_stripe/readme/ROADMAP.rst b/subscription_recurring_payment_stripe/readme/ROADMAP.rst similarity index 100% rename from recurring_payment_stripe/readme/ROADMAP.rst rename to subscription_recurring_payment_stripe/readme/ROADMAP.rst diff --git a/recurring_payment_stripe/readme/USAGE.rst b/subscription_recurring_payment_stripe/readme/USAGE.rst similarity index 100% rename from recurring_payment_stripe/readme/USAGE.rst rename to subscription_recurring_payment_stripe/readme/USAGE.rst diff --git a/recurring_payment_stripe/static/description/index.html b/subscription_recurring_payment_stripe/static/description/index.html similarity index 86% rename from recurring_payment_stripe/static/description/index.html rename to subscription_recurring_payment_stripe/static/description/index.html index 4b7bc5e21f..a328ec4767 100644 --- a/recurring_payment_stripe/static/description/index.html +++ b/subscription_recurring_payment_stripe/static/description/index.html @@ -3,7 +3,7 @@ -Recurring Payment with Stripe +Subscription Recurring Payment with Stripe -
-

Recurring Payment with Stripe

+
+

Subscription Recurring Payment with Stripe

-

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

Table of contents

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

    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.

    +feedback.

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

@@ -406,9 +406,9 @@

Maintainers

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.

+

Current maintainers:

+

Binhex 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_stripe/tests/__init__.py b/subscription_recurring_payment_stripe/tests/__init__.py new file mode 100644 index 0000000000..d4d76d6993 --- /dev/null +++ b/subscription_recurring_payment_stripe/tests/__init__.py @@ -0,0 +1,2 @@ +from . import common +from . import test_account_move diff --git a/subscription_recurring_payment_stripe/tests/common.py b/subscription_recurring_payment_stripe/tests/common.py new file mode 100644 index 0000000000..8878f0cb81 --- /dev/null +++ b/subscription_recurring_payment_stripe/tests/common.py @@ -0,0 +1,261 @@ +import uuid + +from dateutil.relativedelta import relativedelta + +from odoo import fields + +from odoo.addons.payment_stripe.tests.common import StripeCommon + + +class SubscriptionRecurringPaymentStripe(StripeCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.portal_user = cls.env.ref("base.demo_user0") + cls.cash_journal = cls.env["account.journal"].search( + [ + ("type", "=", "cash"), + ("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.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.pricelist2 = cls.env["product.pricelist"].create( + { + "name": "pricelist for contract test 2", + "discount_policy": "with_discount", + } + ) + cls.partner = cls.env["res.partner"].create( + { + "name": "partner test subscription_oca", + "property_product_pricelist": cls.pricelist1.id, + "email": "demo1@demo.com", + } + ) + cls.partner_2 = cls.env["res.partner"].create( + { + "name": "partner test subscription_oca 2", + "property_product_pricelist": cls.pricelist1.id, + "email": "demo2@demo.com", + } + ) + cls.tax_10pc_incl = cls.env["account.tax"].create( + { + "name": "10% Tax incl", + "amount_type": "percent", + "amount": 10, + "price_include": True, + } + ) + 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.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.tmpl1 = cls.create_sub_template({}) + cls.tmpl2 = cls.create_sub_template( + { + "recurring_rule_boundary": "limited", + "recurring_rule_type": "days", + } + ) + cls.tmpl3 = cls.create_sub_template( + { + "recurring_rule_boundary": "unlimited", + "recurring_rule_type": "weeks", + } + ) + cls.tmpl4 = cls.create_sub_template( + { + "recurring_rule_boundary": "limited", + "invoicing_mode": "invoice", + "recurring_rule_type": "years", + } + ) + cls.tmpl5 = cls.create_sub_template( + { + "recurring_rule_boundary": "unlimited", + "invoicing_mode": "invoice", + "recurring_rule_type": "days", + } + ) + + cls.stage = cls.env["sale.subscription.stage"].create( + { + "name": "Test Sub Stage", + } + ) + cls.stage_2 = cls.env["sale.subscription.stage"].create( + { + "name": "Test Sub Stage 2", + "type": "pre", + } + ) + cls.tag = cls.env["sale.subscription.tag"].create( + { + "name": "Test Tag", + } + ) + + cls.stripe = cls._prepare_provider( + "stripe", + update_values={ + "stripe_secret_key": "sk_test_KJtHgNwt2KS3xM7QJPr4O5E8", + "stripe_publishable_key": "pk_test_QSPnimmb4ZhtkEy3Uhdm4S6J", + "stripe_webhook_secret": "whsec_vG1fL6CMUouQ7cObF2VJprLVXT5jBLxB", + "payment_icon_ids": [(5, 0, 0)], + }, + ) + + cls.provider = cls.stripe + cls.token = cls.env["payment.token"].create( + { + "provider_id": cls.stripe.id, + "partner_id": cls.env.ref("base.res_partner_1").id, + "company_id": cls.env.ref("base.main_company").id, + "payment_details": "4242", + "provider_ref": "cus_LBxMCDggAFOiNR", + } + ) + + cls.sub1 = cls.create_sub( + { + "template_id": cls.tmpl2.id, + "pricelist_id": cls.pricelist2.id, + "date_start": fields.Date.today() - relativedelta(days=100), + "in_progress": True, + "journal_id": cls.bank_journal.id, + "charge_automatically": True, + "provider_id": cls.stripe.id, + "payment_token_id": cls.token.id, + } + ) + + cls.sub_line = cls.create_sub_line(cls.sub1) + cls.sub_line2 = cls.env["sale.subscription.line"].create( + { + "company_id": 1, + "sale_subscription_id": cls.sub1.id, + } + ) + + cls.close_reason = cls.env["sale.subscription.close.reason"].create( + { + "name": "Test Close Reason", + } + ) + cls.sub_line2.read(["name", "price_unit"]) + cls.sub_line2.unlink() + + # Pricelists. + cls.pricelist_default = cls.env.ref("product.list0") + cls.pricelist_l1 = cls._create_price_list("Level 1") + cls.pricelist_l2 = cls._create_price_list("Level 2") + cls.pricelist_l3 = cls._create_price_list("Level 3") + cls.env["product.pricelist.item"].create( + { + "pricelist_id": cls.pricelist_l3.id, + "applied_on": "0_product_variant", + "compute_price": "formula", + "base": "pricelist", + "base_pricelist_id": cls.pricelist_l1.id, + "product_id": cls.product_1.id, + } + ) + cls.env["product.pricelist.item"].create( + { + "pricelist_id": cls.pricelist_l2.id, + "applied_on": "3_global", + "compute_price": "formula", + "base": "pricelist", + "base_pricelist_id": cls.pricelist_l1.id, + } + ) + cls.env["product.pricelist.item"].create( + { + "pricelist_id": cls.pricelist_l1.id, + "applied_on": "3_global", + "compute_price": "formula", + "base": "standard_price", + "fixed_price": 1000, + } + ) + + @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.tmpl1.id, + "tag_ids": [(6, 0, [cls.tag.id])], + "stage_id": cls.stage.id, + "pricelist_id": cls.pricelist1.id, + "fiscal_position_id": cls.fiscal.id, + } + default_vals.update(vals) + rec = cls.env["sale.subscription"].create(default_vals) + return rec + + @classmethod + def create_sub_line(cls, sub, prod=None): + ssl = cls.env["sale.subscription.line"].create( + { + "company_id": 1, + "sale_subscription_id": sub.id, + "product_id": prod or cls.product_1.id, + } + ) + return ssl + + @classmethod + def _create_price_list(cls, name): + return cls.env["product.pricelist"].create( + { + "name": name, + "active": True, + "currency_id": cls.env.ref("base.USD").id, + "company_id": cls.env.user.company_id.id, + } + ) diff --git a/subscription_recurring_payment_stripe/tests/test_account_move.py b/subscription_recurring_payment_stripe/tests/test_account_move.py new file mode 100644 index 0000000000..0b5e8febeb --- /dev/null +++ b/subscription_recurring_payment_stripe/tests/test_account_move.py @@ -0,0 +1,134 @@ +import logging +from datetime import timedelta +from unittest.mock import patch + +from odoo import fields +from odoo.exceptions import ValidationError +from odoo.tests import tagged + +from odoo.addons.subscription_recurring_payment_stripe.tests.common import ( + SubscriptionRecurringPaymentStripe, +) + +_logger = logging.getLogger(__name__) + + +@tagged("post_install", "-at_install") +class TestAccountMove(SubscriptionRecurringPaymentStripe): + def setUp(self): + super().setUp() + self._setup_test_data() + + def _setup_test_data(self): + """Setup common test data for all test methods""" + self.subscription = self.sub1 + self.invoice = self._create_test_invoice() + + def _create_test_invoice(self): + """Helper method to create a test invoice""" + return self.env["account.move"].create( + { + "move_type": "out_invoice", + "partner_id": self.subscription.partner_id.id, + "invoice_date_due": fields.Date.today(), + "subscription_id": self.subscription.id, + "invoice_line_ids": [ + ( + 0, + 0, + { + "product_id": self.product_1.id, + "quantity": 1, + "price_unit": 100.0, + }, + ) + ], + } + ) + + def _prepare_payment_method(self): + """Helper method to prepare payment method data""" + provider = self.subscription.provider_id + payment_method = self.env["account.payment.method"].search( + [("code", "=", provider.code)] + ) + return self.env["account.payment.method.line"].create( + { + "payment_provider_id": provider.id, + "payment_method_id": payment_method.id, + "journal_id": self.bank_journal.id, + } + ) + + def test_01_process_due_invoice_success(self): + """Test successful processing of a due invoice""" + self.invoice.action_post() + method_line = self._prepare_payment_method() + + payment_register = self.env["account.payment.register"] + payment_vals = { + "currency_id": self.invoice.currency_id.id, + "journal_id": self.bank_journal.id, + "company_id": self.invoice.company_id.id, + "partner_id": self.invoice.partner_id.id, + "communication": self.invoice.name, + "payment_type": "inbound", + "partner_type": "customer", + "payment_difference_handling": "open", + "writeoff_label": "Write-Off", + "payment_date": fields.Date.today(), + "amount": self.invoice.amount_total, + "payment_method_line_id": method_line.id, + "payment_token_id": self.subscription.payment_token_id.id, + } + + # Process payment + payment = payment_register.with_context( + active_model="account.move", + active_ids=self.invoice.ids, + active_id=self.invoice.id, + ).create(payment_vals) + payment.action_create_payments() + + self.assertEqual(self.invoice.state, "posted") + self.assertEqual(self.invoice.payment_state, "paid") + + def test_02_process_due_invoice_no_token(self): + """Test processing invoice without payment token""" + self.subscription.payment_token_id = False + + self.env["account.move"].cron_process_due_invoices() + + self.assertEqual(self.invoice.state, "draft") + + def test_03_process_due_invoice_invalid_state(self): + """Test processing invoice in invalid state""" + self.invoice.state = "cancel" + + self.env["account.move"].cron_process_due_invoices() + self.assertEqual(self.invoice.payment_state, "not_paid") + + @patch( # noqa: B950 + "odoo.addons.payment_stripe.models.payment_provider.PaymentProvider._stripe_make_request" # noqa: B950 + ) + def test_04_process_due_invoice_stripe_error(self, mock_stripe): + """Test handling of Stripe API errors""" + mock_stripe.side_effect = ValidationError("Stripe API Error") + + self.env["account.move"].cron_process_due_invoices() + + self.assertEqual(self.invoice.payment_state, "not_paid") + + def test_05_process_multiple_due_invoices(self): + """Test processing multiple due invoices""" + # Create second invoice + invoice2 = self._create_test_invoice() + invoice2.invoice_date_due = fields.Date.today() - timedelta(days=1) + + invoices = self.invoice | invoice2 + invoices.action_post() + + self.env["account.move"].cron_process_due_invoices() + + for inv in invoices: + self.assertEqual(inv.state, "posted") diff --git a/subscription_recurring_payment_stripe/views/sale_subscription_views.xml b/subscription_recurring_payment_stripe/views/sale_subscription_views.xml new file mode 100644 index 0000000000..66d14613cb --- /dev/null +++ b/subscription_recurring_payment_stripe/views/sale_subscription_views.xml @@ -0,0 +1,26 @@ + + + + + + subscription.recurring.payment.stripe.form + sale.subscription + + 17 + + + + + + + + +