From 0736427e8769ad8ba191f5affbfb4c5148ac12ca Mon Sep 17 00:00:00 2001 From: Ronny Montano Date: Wed, 3 Jul 2024 09:43:03 -0600 Subject: [PATCH] [16.0][ADD] Generate recurring payment from contract with ACH + Stripe --- contract_recurring_payment/README.rst | 67 ++++++++++++ contract_recurring_payment/__init__.py | 1 + contract_recurring_payment/__manifest__.py | 21 ++++ .../data/ir_cron_recurring_payment.xml | 15 +++ contract_recurring_payment/models/__init__.py | 3 + .../models/account_payment.py | 7 ++ .../models/account_payment_mode.py | 13 +++ .../models/contract_contract.py | 49 +++++++++ .../static/description/icon.png | Bin 0 -> 3471 bytes .../views/contract_contract_view.xml | 36 +++++++ contract_recurring_payment_ach/__init__.py | 1 + .../__manifest__.py | 18 ++++ .../models/__init__.py | 2 + .../models/account_payment_mode.py | 101 ++++++++++++++++++ .../models/contract_contract.py | 8 ++ .../static/description/icon.png | Bin 0 -> 3471 bytes .../views/contract_view.xml | 27 +++++ contract_recurring_payment_stripe/README.rst | 68 ++++++++++++ contract_recurring_payment_stripe/__init__.py | 1 + .../__manifest__.py | 19 ++++ .../models/__init__.py | 2 + .../models/account_payment_mode.py | 66 ++++++++++++ .../models/contract_contract.py | 7 ++ .../static/description/icon.png | Bin 0 -> 3471 bytes .../views/contract_view.xml | 22 ++++ .../odoo/addons/contract_recurring_payment | 1 + setup/contract_recurring_payment/setup.py | 6 ++ .../addons/contract_recurring_payment_ach | 1 + setup/contract_recurring_payment_ach/setup.py | 6 ++ .../addons/contract_recurring_payment_stripe | 1 + .../setup.py | 6 ++ 31 files changed, 575 insertions(+) create mode 100644 contract_recurring_payment/README.rst create mode 100644 contract_recurring_payment/__init__.py create mode 100644 contract_recurring_payment/__manifest__.py create mode 100644 contract_recurring_payment/data/ir_cron_recurring_payment.xml create mode 100644 contract_recurring_payment/models/__init__.py create mode 100644 contract_recurring_payment/models/account_payment.py create mode 100644 contract_recurring_payment/models/account_payment_mode.py create mode 100644 contract_recurring_payment/models/contract_contract.py create mode 100755 contract_recurring_payment/static/description/icon.png create mode 100644 contract_recurring_payment/views/contract_contract_view.xml create mode 100755 contract_recurring_payment_ach/__init__.py create mode 100755 contract_recurring_payment_ach/__manifest__.py create mode 100755 contract_recurring_payment_ach/models/__init__.py create mode 100644 contract_recurring_payment_ach/models/account_payment_mode.py create mode 100644 contract_recurring_payment_ach/models/contract_contract.py create mode 100755 contract_recurring_payment_ach/static/description/icon.png create mode 100755 contract_recurring_payment_ach/views/contract_view.xml create mode 100644 contract_recurring_payment_stripe/README.rst create mode 100755 contract_recurring_payment_stripe/__init__.py create mode 100755 contract_recurring_payment_stripe/__manifest__.py create mode 100755 contract_recurring_payment_stripe/models/__init__.py create mode 100644 contract_recurring_payment_stripe/models/account_payment_mode.py create mode 100644 contract_recurring_payment_stripe/models/contract_contract.py create mode 100755 contract_recurring_payment_stripe/static/description/icon.png create mode 100755 contract_recurring_payment_stripe/views/contract_view.xml create mode 120000 setup/contract_recurring_payment/odoo/addons/contract_recurring_payment create mode 100644 setup/contract_recurring_payment/setup.py create mode 120000 setup/contract_recurring_payment_ach/odoo/addons/contract_recurring_payment_ach create mode 100644 setup/contract_recurring_payment_ach/setup.py create mode 120000 setup/contract_recurring_payment_stripe/odoo/addons/contract_recurring_payment_stripe create mode 100644 setup/contract_recurring_payment_stripe/setup.py 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 0000000000000000000000000000000000000000..6754ecdd1893d70e566050a1f9c2b44a93a59900 GIT binary patch literal 3471 zcmV;A4RG>_P)|!b?m?(vXr4e?oxnx@1 z$ja(k8d|1inK!fNp<8O|_0h~!a?7=v$|Z|TQ!GnO4Mho!MHG>BkY)B6X5Rb9z4oxo z%$fHvp!d1w^PlH@f4}#~mvdg;?Fa}62nYxW2nYxW2nYxW2nYxW2nhUtK)SkteaGI5 z`lIS(AG^aL?H8L`G~uoVSNZ0mnt44@uMLOY(t_04=SfP6&6~A$Xz%B}DqKF+3#NJ* z64SNh-lwl{#dMjWS=-Vdu4^ux-DId4ZnIk?03a4g9RbR)Um}D0u6p~)BVT%whvw!= zaA^K>&|aAay*3#wRxtp;SFM9z!1stwT2AKeJK@dR>GH>taplY#XsR9ui_s4NKnS3Y zJcFR<4H)!VHp$4aJB#(Yp&F_YpW5{9ii*0z84hZ9nxB9mBDmlBp|`&{E90IyCRe%W zn(2Wk%-IUn! zkvEEH&8dy=E-J4*|B5R!0f2&{vi{=KUkld%%cVx65#x{j+uWK70Ct-crKdlj3x>YX ziL(FGX4deDC_BB{t(gE&cV!eV9Nb1vXG`1P@~WXLc0WG&TK&a2p6V;F7foOI*~rmO zZ=P8?1vO=3JxW9!1Qi8~>G~3R#`*`CW14H@@Nwqg)|b3yXzrQKYO}4m zFKe8^+!%1CkL=fUr_*Zsj@OM zGR4^DV(L@*@b%X-C$pKwIJrHUIWB+Kov;{X%yAN#9dXIOx4z^xLye|}NBP7lWC6R| zUn42wjPMQofmb1wMGrE^RF~ezt1bVqpGn4&^46EUV(79jCfwhkt7I-pE>+bhgrzzk zkjD&M3^9*azo~jM-TYFxr*f*97l7v4XPMg(OB{$wSknG-rwz^C|6;_&iXS()8^}9D zCib!P>@jdg<|A85o!uw%zB!55!LNgeC{saZeGq>8b`$+w!}$tRjYf>)yVk(a(37bR z0Eij5jJ$Q=yY`nmHT7Kb#X}DiT|2&3Z>neN(3SGQ@|ci8v$CE!vWH7c7Yvz-tA+2w zVhCa?E~MuHBo3QSUfOxdY2Gc`Fz@6_H3%fTP^V7rMZ)mNdl!_1j zfiQnD^r>;Axy`-RPsQT$*}uc2yB{r<{%F>QgF3hi_VLp}6}k&Sk%tg_=Q)TZ4wM~F zLQ`!zn(8uO)~Psi5CW(|Yao#oLhgG4iNmwWl#MP|oj46$wrBj^S1Zp=x3rkj_2%o* zEq1F%-Govec*Wm0bcfo%=druv#^#Mmd9})!E^`CDuecU7jAo2zFd|qkg+nRF#Q+7q zz9#{z7mOp<+Q-n(vf@#EBnfpD6VP1w2+T%bPipcQsoadf$UO*&-HpVwbL4MX7d;Ji zy?{X}nYU^1vI=eCESBjc07M2SY`in+i8+%}vzlFLvQJa#p2JvnxehZO)Oj6BIcgH4 zF?HKCvd?9mA5H3u{DV8uTr-5FMwj^a$j5;6No3aN1$@F?8v5qul=+vcPcLLm7#0|} z`Gd@&7o7Dktr@e7ZYZq*=L(Tb0`na)7{BFZa=?{_Zl0~iub<_hv6^}6+Xny83rKt6 zUh;I??uZ4aukVUoPb6H`7A#;*<|_}pmfUab94<4TT1_XG)#7pZFvzuPI39(qxVyrJ*YP$|wJ&8aUs7YK=nmJ#%7(-*;Ri z6Hjq!XrHhlo1_xC%k5oPG?{9i-F_m=pG(&(6str$RuSr&@$9M`>OTK~)DIw&8{KMJ zBbM3`owSC%8s|7QbmoK|h0!4c7PBTZm>YWK6=;@n>9$NGpY{#IC#?CKOo+%ik9F<$ zpjtzwZYx8~z&9b5a`&Y~MA0j0A(^|E*Bj$BrK=m1QZjdA(yGe(3oqFnEgt`oA|Qwg zNmzIHfGKm_9b(AMrIO{pVP#n@rnNXAW+}RND0V!Mh}o}?BU+dBzccJflou?Ap)ta# zz(?7D=*0PC;mLI@ZF9l-;w|}VT%5p5k#&Mkys<9e<}^LFte22D3I%XS)7qqp@lT z^oCG*=YDz> zGFW|OxFnOHB-kH0WA4EA7lx4z`MZNoEifm>tB7r5pTngP?w_()!7s)Y}xX#k)8PUu~J}(^* zecxme5i^bBTI^PNS@qei+jC!Wdmd@gW299jAIvtW3M-MV`JJ+F4|f8u zOflzX>^mH&iu#2$&TP{M{#NwEOfFsa6!KG;KMu3z*XnWm*SVO$8rK=THfQ&h2X@$B z_XMNmx<@DMlydOu533DeKeN;cyk=;aI{F-+IHRRG?&!H4fn2(%V4UHT-)2Q`K5=~U znxQGN51*Au6wGT$HoHZt@R!TEbfF3uxHQh76h3_7_~JD~(^E3FzH)ULYn)P{Zn`t} z(GEwe+f!19ethZ@i{SOayF0wP?kN)~nEx>ONR>wkA)HTktscYq)THu(S3hXr!mEZx zhxf~th-Dsclo1ew2ldb9(q-+UJsR|2K8)roL-Vji>|~AW3|=*K-ecdL2v^6g^i&@a z6u;d3pG2S;=zP_``+3XfE0Embf z0e(I5LsY#!eD}Fyn_R7uul|7s|5Sj{*P1X(t3!&XZA!$T!Aktpy9ZW&l1VrZqICsb z{%PXgyd{x^C5QT3ZFbr_IO*y$qgIr(zOT_x@qQh~R@7pc)sDV)ir#t?;*C~to(oX> zqC%y>H-1tSsZ}US>yLBKrxD(tK6-;LH`E<3Z=pl;%dx7i$@8ZW09106$4B7pNh9(3 zm^fnTs`~hF(+r)ohCZBM4vn>iZQ$*%z~y1F`0I)(q=Z}D{dC#>yE|rYr3nRBv6aH50f=hW=iOxh5L|SmSyG;M=v6Nw*HvS?P8|bBd|Ybv+*E6C0tzXWp*I z+jP63znsQEy%|c@I1vHvh(TUg)Z|UN-OySS+@DW!L7*Qh=ZwE~`_3D5yP@Gq@H)^c zlYsxpx^AS~4gKfiI1{8qhYu0B@R5S^sN}6cw;M_baj5;Uoh8Oc0@LkL*xFS!d6Szm zY#JVmW0 + + + + 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 0000000000000000000000000000000000000000..6754ecdd1893d70e566050a1f9c2b44a93a59900 GIT binary patch literal 3471 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 + + + + 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 0000000000000000000000000000000000000000..6754ecdd1893d70e566050a1f9c2b44a93a59900 GIT binary patch literal 3471 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 + + + + 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, +)