diff --git a/contract_recurring_payment/README.rst b/contract_recurring_payment/README.rst new file mode 100644 index 0000000000..1db2a3ba1d --- /dev/null +++ b/contract_recurring_payment/README.rst @@ -0,0 +1,67 @@ +================== +Generic contract recurring payment +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6694fe97ba2c4c5343006af38811c58f4b864357bf894956bd3e4b51eae88b72 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| 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 +.. |badge2| 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 + :alt: Try me on Runboat + +|badge1| |badge2| + +This addon make contract invoicing cron plan each contract in a job instead of creating all invoices in one transaction + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +The feature to create generic recurring payment + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. + +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. + + +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/contract_recurring_payment/__init__.py b/contract_recurring_payment/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/contract_recurring_payment/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/contract_recurring_payment/__manifest__.py b/contract_recurring_payment/__manifest__.py new file mode 100644 index 0000000000..def1d20ca9 --- /dev/null +++ b/contract_recurring_payment/__manifest__.py @@ -0,0 +1,21 @@ +{ + "name": "Generic contract recurring payment", + "summary": """ + Generic Contract recurring payment".""", + "author": "Binhex,Odoo Community Association (OCA)", + "website": "https://github.com/OCA/contract", + "category": "Contract", + "version": "16.0.1.0.1", + "depends": [ + "account", + "contract", + "contract_payment_mode", + "payment", + ], + "license": "AGPL-3", + "data": [ + "data/ir_cron_recurring_payment.xml", + "views/contract_contract_view.xml", + ], + "images": ["static/src/description/icon.png"], +} diff --git a/contract_recurring_payment/data/ir_cron_recurring_payment.xml b/contract_recurring_payment/data/ir_cron_recurring_payment.xml new file mode 100644 index 0000000000..a1527ef1f1 --- /dev/null +++ b/contract_recurring_payment/data/ir_cron_recurring_payment.xml @@ -0,0 +1,15 @@ + + + + Generate recurring payment + + + 1 + days + -1 + + code + model.cron_recurring_payment() + + + diff --git a/contract_recurring_payment/models/__init__.py b/contract_recurring_payment/models/__init__.py new file mode 100644 index 0000000000..ba730c1fb1 --- /dev/null +++ b/contract_recurring_payment/models/__init__.py @@ -0,0 +1,3 @@ +from . import contract_contract +from . import account_payment +from . import account_payment_mode diff --git a/contract_recurring_payment/models/account_payment.py b/contract_recurring_payment/models/account_payment.py new file mode 100644 index 0000000000..b1ba221024 --- /dev/null +++ b/contract_recurring_payment/models/account_payment.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class AccountPayment(models.Model): + _inherit = "account.payment" + + contract_id = fields.Many2one("contract.contract", string="Contract") diff --git a/contract_recurring_payment/models/account_payment_mode.py b/contract_recurring_payment/models/account_payment_mode.py new file mode 100644 index 0000000000..d645af1a57 --- /dev/null +++ b/contract_recurring_payment/models/account_payment_mode.py @@ -0,0 +1,13 @@ +from odoo import models + + +class AccountPaymentMode(models.Model): + _inherit = "account.payment.mode" + + def _create_recurring_payments(self, invoices, contract_id): + """ + Generic method for each payment mode + :param invoices: + :param contract_id: + :return: + """ diff --git a/contract_recurring_payment/models/contract_contract.py b/contract_recurring_payment/models/contract_contract.py new file mode 100644 index 0000000000..19c6385dd9 --- /dev/null +++ b/contract_recurring_payment/models/contract_contract.py @@ -0,0 +1,49 @@ +from datetime import datetime + +from odoo import api, fields, models + +SELECTION_PAYMENT_TYPE = [ + ("fixed_date", "Fixed Date"), + ("invoice_due_date", "Invoice Due Date"), +] + + +class ContractContract(models.Model): + _inherit = "contract.contract" + + allow_use_payment_token = fields.Boolean( + default=False, + string="Allow use payment token", + help="Allow use payment token on recurring payment", + ) + + payment_mode_code = fields.Char(related="payment_mode_id.payment_method_id.code") + payment_ids = fields.One2many( + "account.payment", + "contract_id", + string="Payments", + ) + next_recurring_payment_date = fields.Date(string="Next Payment Date") + + @api.model + def cron_recurring_payment(self): + """ + Check contracts and generate recurring payments + :return: + """ + contracts = self.search( + [("active", "=", True), ("allow_use_payment_token", "=", True)] + ) + + for contract in contracts: + if contract.next_recurring_payment_date == datetime.utcnow().date(): + if contract.payment_mode_id: + invoices = contract._get_related_invoices().filtered( + lambda account_move: account_move.state == "posted" + and account_move.amount_residual_signed > 0 + and account_move.payment_state in ["not_paid", "partial"] + ) + if invoices: + contract.payment_mode_id._create_recurring_payments( + invoices=invoices, contract_id=contract + ) diff --git a/contract_recurring_payment/static/description/icon.png b/contract_recurring_payment/static/description/icon.png new file mode 100755 index 0000000000..6754ecdd18 Binary files /dev/null and b/contract_recurring_payment/static/description/icon.png differ diff --git a/contract_recurring_payment/views/contract_contract_view.xml b/contract_recurring_payment/views/contract_contract_view.xml new file mode 100644 index 0000000000..291aed9de1 --- /dev/null +++ b/contract_recurring_payment/views/contract_contract_view.xml @@ -0,0 +1,36 @@ + + + + + contract.contract form view (in contract).form + contract.contract + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contract_recurring_payment_ach/__init__.py b/contract_recurring_payment_ach/__init__.py new file mode 100755 index 0000000000..0650744f6b --- /dev/null +++ b/contract_recurring_payment_ach/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/contract_recurring_payment_ach/__manifest__.py b/contract_recurring_payment_ach/__manifest__.py new file mode 100755 index 0000000000..8626a3e57c --- /dev/null +++ b/contract_recurring_payment_ach/__manifest__.py @@ -0,0 +1,18 @@ +{ + "name": "ACH recurring payment from contract", + "summary": """ + Stripe recurring payment from contract""", + "author": "Binhex, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/contract", + "category": "Contract", + "version": "16.0.1.0.1", + "depends": [ + "contract_recurring_payment", + "account_banking_mandate", + "account_banking_ach_direct_debit", + ], + "data": [ + "views/contract_view.xml", + ], + "images": ["static/src/description/icon.png"], +} diff --git a/contract_recurring_payment_ach/models/__init__.py b/contract_recurring_payment_ach/models/__init__.py new file mode 100755 index 0000000000..fe2de0343f --- /dev/null +++ b/contract_recurring_payment_ach/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_payment_mode +from . import contract_contract diff --git a/contract_recurring_payment_ach/models/account_payment_mode.py b/contract_recurring_payment_ach/models/account_payment_mode.py new file mode 100644 index 0000000000..6c56510010 --- /dev/null +++ b/contract_recurring_payment_ach/models/account_payment_mode.py @@ -0,0 +1,101 @@ +from datetime import datetime + +from odoo import models + + +class AccountPaymentMode(models.Model): + _inherit = "account.payment.mode" + + def create_payment_order_line( + self, contract_id, invoice_move_line, account_payment_order_id + ): + if invoice_move_line.currency_id: + currency_id = invoice_move_line.currency_id.id + amount_currency = invoice_move_line.amount_residual_currency + else: + currency_id = invoice_move_line.company_id.currency_id.id + amount_currency = invoice_move_line.amount_residual + values = { + "partner_id": contract_id.partner_id.id, + "move_line_id": invoice_move_line.id, + "date": datetime.now().date(), + "currency_id": currency_id, + "amount_currency": amount_currency, + "communication_type": "normal", + "communication": invoice_move_line.move_id.name, + "mandate_id": contract_id.mandate_id.id, + "order_id": account_payment_order_id.id, + "partner_bank_id": contract_id.res_partner_bank_id.id, + } + self.env["account.payment.line"].sudo().create(values) + + def _create_recurring_payments(self, invoices, contract_id): + """ + Generic method for each payment mode + :param invoices: + :return: + """ + self.ensure_one() + if self.payment_method_id.code == "ACH-In": + active_payment_order = self.env["account.payment.order"].search( + [ + ( + "journal_id", + "=", + contract_id.payment_mode_id.fixed_journal_id.id, + ), + ("state", "=", "draft"), + ("payment_type", "=", "inbound"), + ( + "payment_mode_id.payment_method_id.code", + "=", + self.payment_method_id.code, + ), + ], + limit=1, + ) + + if active_payment_order: + for invoice in invoices: + invoice_move_line = self.get_reconciled_line_from_moves(invoice) + self.create_payment_order_line( + contract_id=contract_id, + invoice_move_line=invoice_move_line, + account_payment_order_id=active_payment_order, + ) + else: + company_partner_bank_id = ( + self.env["res.partner.bank"] + .sudo() + .search( + [("partner_id", "=", contract_id.company_id.partner_id.id)], + limit=1, + ) + ) + account_payment_order = ( + self.env["account.payment.order"] + .sudo() + .create( + { + "payment_mode_id": self.id, + "journal_id": self.fixed_journal_id.id, + "description": "Payment order generated automatically from contract", + "company_id": contract_id.company_id.id, + "company_partner_bank_id": company_partner_bank_id.id, + } + ) + ) + + for invoice in invoices: + invoice_move_line = self.get_reconciled_line_from_moves(invoice) + self.create_payment_order_line( + contract_id, invoice_move_line, account_payment_order + ) + + else: + return super()._create_recurring_payments(invoices, contract_id) + + def get_reconciled_line_from_moves(self, invoice): + return invoice.mapped("line_ids").filtered( + lambda line: not line.reconciled and line.account_id.reconcile == True + ) diff --git a/contract_recurring_payment_ach/models/contract_contract.py b/contract_recurring_payment_ach/models/contract_contract.py new file mode 100644 index 0000000000..6cc739b930 --- /dev/null +++ b/contract_recurring_payment_ach/models/contract_contract.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class ContractContract(models.Model): + _inherit = "contract.contract" + + res_partner_bank_id = fields.Many2one("res.partner.bank", string="Bank Account") + mandate_id = fields.Many2one("account.banking.mandate", string="Mandate") diff --git a/contract_recurring_payment_ach/static/description/icon.png b/contract_recurring_payment_ach/static/description/icon.png new file mode 100755 index 0000000000..6754ecdd18 Binary files /dev/null and b/contract_recurring_payment_ach/static/description/icon.png differ diff --git a/contract_recurring_payment_ach/views/contract_view.xml b/contract_recurring_payment_ach/views/contract_view.xml new file mode 100755 index 0000000000..0e2ebd2a4e --- /dev/null +++ b/contract_recurring_payment_ach/views/contract_view.xml @@ -0,0 +1,27 @@ + + + + + contract.contract form view (in contract_recurring_payment_ach) + + contract.contract + + + + + + + + + + diff --git a/contract_recurring_payment_stripe/README.rst b/contract_recurring_payment_stripe/README.rst new file mode 100644 index 0000000000..4a2e0f1a5d --- /dev/null +++ b/contract_recurring_payment_stripe/README.rst @@ -0,0 +1,68 @@ +================== +Generic contract recurring payment Stripe +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6694fe97ba2c4c5343006af38811c58f4b864357bf894956bd3e4b51eae88b72 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| 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 +.. |badge2| 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 + :alt: Try me on Runboat + +|badge1| |badge2| + +This addon make contract invoicing cron plan each contract in a job instead of creating all invoices in one transaction + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +The feature can be enabled by setting the ir.config_parameter +"contract.queue.job" to True. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. + +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. + + +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/contract_recurring_payment_stripe/__init__.py b/contract_recurring_payment_stripe/__init__.py new file mode 100755 index 0000000000..0650744f6b --- /dev/null +++ b/contract_recurring_payment_stripe/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/contract_recurring_payment_stripe/__manifest__.py b/contract_recurring_payment_stripe/__manifest__.py new file mode 100755 index 0000000000..127cc8bcfc --- /dev/null +++ b/contract_recurring_payment_stripe/__manifest__.py @@ -0,0 +1,19 @@ +{ + "name": "Stripe recurring payment from contract", + "summary": """ + Stripe recurring payment from contract""", + "author": "Binhex, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/contract", + "category": "Bank payment", + "version": "16.0.1.0.1", + "depends": [ + "contract_recurring_payment", + "payment", + "contract", + ], + "license": "AGPL-3", + "data": [ + "views/contract_view.xml" + ], + "images": ["static/src/description/icon.png"], +} diff --git a/contract_recurring_payment_stripe/models/__init__.py b/contract_recurring_payment_stripe/models/__init__.py new file mode 100755 index 0000000000..fe2de0343f --- /dev/null +++ b/contract_recurring_payment_stripe/models/__init__.py @@ -0,0 +1,2 @@ +from . import account_payment_mode +from . import contract_contract diff --git a/contract_recurring_payment_stripe/models/account_payment_mode.py b/contract_recurring_payment_stripe/models/account_payment_mode.py new file mode 100644 index 0000000000..0c9f21f204 --- /dev/null +++ b/contract_recurring_payment_stripe/models/account_payment_mode.py @@ -0,0 +1,66 @@ +from datetime import datetime + +from odoo import models + + +class AccountPaymentMode(models.Model): + _inherit = "account.payment.mode" + + def _create_recurring_payments(self, invoices, contract_id): + """ + Generic method for each payment mode + :param invoices: + :return: + """ + self.ensure_one() + if self.payment_method_id.code == "stripe": + payment_token = contract_id.payment_token_id + if payment_token: + + for invoice in invoices: + payment_transaction = self.create_payment_transaction_online_token( + payment_token, invoice, contract_id + ) + payment_transaction._send_payment_request() + if payment_transaction.state == "done": + payment_transaction._set_done() + payment_transaction._finalize_post_processing() + if payment_transaction.payment_id: + interval = contract_id.get_relative_delta( + contract_id.recurring_rule_type, + contract_id.recurring_interval, + ) + payment_transaction.payment_id.update( + {"contract_id": contract_id.id} + ) + contract_id.update( + { + "next_recurring_payment_date": datetime.utcnow().date() + + interval + } + ) + payment_lines = payment_transaction.payment_id.mapped( + "line_ids" + ).filtered( + lambda line: not line.reconciled + and line.account_type + in ["asset_receivable", "liability_payable"] + ) + # Pay due invoices + for payment_line in payment_lines: + if not payment_line.reconciled: + invoice.js_assign_outstanding_line(payment_line.id) + else: + return super()._create_recurring_payments(invoices, contract_id) + + def create_payment_transaction_online_token(self, token, invoice, contract_id): + values = { + "provider_id": token.provider_id.id, + "reference": f'{invoice.name}_{token.provider_ref}_{datetime.utcnow().strftime("%Y%m%d%H%M%S")}', + "amount": invoice.amount_residual_signed, + "currency_id": contract_id.pricelist_id.currency_id.id, + "partner_id": contract_id.partner_id.id, + "operation": f"online_token", + "token_id": token.id, + } + return self.env["payment.transaction"].sudo().create(values) diff --git a/contract_recurring_payment_stripe/models/contract_contract.py b/contract_recurring_payment_stripe/models/contract_contract.py new file mode 100644 index 0000000000..24dcb60743 --- /dev/null +++ b/contract_recurring_payment_stripe/models/contract_contract.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ContractContract(models.Model): + _inherit = "contract.contract" + + payment_token_id = fields.Many2one("payment.token") diff --git a/contract_recurring_payment_stripe/static/description/icon.png b/contract_recurring_payment_stripe/static/description/icon.png new file mode 100755 index 0000000000..6754ecdd18 Binary files /dev/null and b/contract_recurring_payment_stripe/static/description/icon.png differ diff --git a/contract_recurring_payment_stripe/views/contract_view.xml b/contract_recurring_payment_stripe/views/contract_view.xml new file mode 100755 index 0000000000..4e6fee5b43 --- /dev/null +++ b/contract_recurring_payment_stripe/views/contract_view.xml @@ -0,0 +1,22 @@ + + + + + contract.contract form view (in contract_recurring_payment_stripe) + + contract.contract + + + + + + + + + diff --git a/setup/contract_recurring_payment/odoo/addons/contract_recurring_payment b/setup/contract_recurring_payment/odoo/addons/contract_recurring_payment new file mode 120000 index 0000000000..6efbe422f9 --- /dev/null +++ b/setup/contract_recurring_payment/odoo/addons/contract_recurring_payment @@ -0,0 +1 @@ +../../../../contract_recurring_payment \ No newline at end of file diff --git a/setup/contract_recurring_payment/setup.py b/setup/contract_recurring_payment/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/contract_recurring_payment/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/contract_recurring_payment_ach/odoo/addons/contract_recurring_payment_ach b/setup/contract_recurring_payment_ach/odoo/addons/contract_recurring_payment_ach new file mode 120000 index 0000000000..14ace94396 --- /dev/null +++ b/setup/contract_recurring_payment_ach/odoo/addons/contract_recurring_payment_ach @@ -0,0 +1 @@ +../../../../contract_recurring_payment_ach \ No newline at end of file diff --git a/setup/contract_recurring_payment_ach/setup.py b/setup/contract_recurring_payment_ach/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/contract_recurring_payment_ach/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/contract_recurring_payment_stripe/odoo/addons/contract_recurring_payment_stripe b/setup/contract_recurring_payment_stripe/odoo/addons/contract_recurring_payment_stripe new file mode 120000 index 0000000000..f75233ea37 --- /dev/null +++ b/setup/contract_recurring_payment_stripe/odoo/addons/contract_recurring_payment_stripe @@ -0,0 +1 @@ +../../../../contract_recurring_payment_stripe \ No newline at end of file diff --git a/setup/contract_recurring_payment_stripe/setup.py b/setup/contract_recurring_payment_stripe/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/contract_recurring_payment_stripe/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)